chromium의 배치 도구 depottools 및 gclient

27076 단어
depot_tools는 gclient, gcl, gn,ninja 등의 도구를 포함하는 도구 패키지입니다.그 중에서 gclient는 코드 획득 도구로 svn과git를 이용합니다.주요 depottools 폴더 아래의 파일은 다음과 같습니다: gclient, gclient.py、subcommand.py、gclient_utils.py.
gclient 파일은 bash 스크립트입니다.
#########glcient###########
/usr/bin/bash 
base_dir=$(dirname "$0")
 
 if [[ "#grep#fetch#cleanup#diff#" != *"#$1#"* ]]; then
   "$base_dir"/update_depot_tools
 fi
 
 PYTHONDONTWRITEBYTECODE=1 exec python "$base_dir/gclient.py" "$@"

우선, 스크립트의 디렉터리를 가져와base 에 값을 부여합니다dir, 그리고 명령 매개 변수 1이grep|fetch|cleanup|diff인지 판단하고basedir의 updatdepot_tools 스크립트, 이 스크립트는git, svn 도구를 업데이트합니다.마지막으로 현재 스크립트 디렉터리 (depot tools) 의python 스크립트 gclient를 호출합니다.py, 이 스크립트에 매개 변수를 전달합니다.
###############glcient.py#######
#     

def Main(argv):
      .....
   dispatcher = subcommand.CommandDispatcher(__name__)
   try:
     return dispatcher.execute(OptionParser(), argv)
    ......
if '__main__' == __name__:
   sys.exit(Main(sys.argv[1:]))

gclient에서.py 스크립트가 시작되고 함수Main을 호출합니다. 파라미터는 bash 스크립트에 전달됩니다.Main은 주로 두 가지 과정을 수행한다. 하나는 디스패치 대상을 만들고 파라미터는 현재 모듈이다.그리고 디스패치의 excute 방법을 호출합니다. 매개 변수는 OptionParser 대상이고 매개 변수는 Main에 전달되는 매개 변수입니다.
다음은subcommand 모듈 아래의 클래스CommandDispatcher에 들어가execute 방법을 분석합니다.
################# subcommand.py ###########
class CommandDispatcher(object):
  def __init__(self, module):
    """module is the name of the main python module where to look for commands.

    The python builtin variable __name__ MUST be used for |module|. If the
    script is executed in the form 'python script.py', __name__ == '__main__'
    and sys.modules['script'] doesn't exist. On the other hand if it is unit
    tested, __main__ will be the unit test's module so it has to reference to
    itself with 'script'. __name__ always match the right value.
    """
    self.module = sys.modules[module]

  def enumerate_commands(self):
    """Returns a dict of command and their handling function.

    The commands must be in the '__main__' modules. To import a command from a
    submodule, use:
      from mysubcommand import CMDfoo

    Automatically adds 'help' if not already defined.

    A command can be effectively disabled by defining a global variable to None,
    e.g.:
      CMDhelp = None
    """
    cmds = dict(
        (fn[3:], getattr(self.module, fn))
        for fn in dir(self.module) if fn.startswith('CMD'))
    cmds.setdefault('help', CMDhelp)
    return cmds

  def find_nearest_command(self, name):
    """Retrieves the function to handle a command.

    It automatically tries to guess the intended command by handling typos or
    incomplete names.
    """
    # Implicitly replace foo-bar to foo_bar since foo-bar is not a valid python
    # symbol but it's faster to type.
    name = name.replace('-', '_')
    commands = self.enumerate_commands()
    if name in commands:
      return commands[name]

    # An exact match was not found. Try to be smart and look if there's
    # something similar.
    commands_with_prefix = [c for c in commands if c.startswith(name)]
    if len(commands_with_prefix) == 1:
      return commands[commands_with_prefix[0]]

    # A #closeenough approximation of levenshtein distance.
    def close_enough(a, b):
      return difflib.SequenceMatcher(a=a, b=b).ratio()

    hamming_commands = sorted(
        ((close_enough(c, name), c) for c in commands),
        reverse=True)
    if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3:
      # Too ambiguous.
      return

    if hamming_commands[0][0] < 0.8:
      # Not similar enough. Don't be a fool and run a random command.
      return

    return commands[hamming_commands[0][1]]

  def _gen_commands_list(self):
    """Generates the short list of supported commands."""
    commands = self.enumerate_commands()
    docs = sorted(
        (name, self._create_command_summary(name, handler))
        for name, handler in commands.iteritems())
    # Skip commands without a docstring.
    docs = [i for i in docs if i[1]]
    # Then calculate maximum length for alignment:
    length = max(len(c) for c in commands)

    # Look if color is supported.
    colors = _get_color_module()
    green = reset = ''
    if colors:
      green = colors.Fore.GREEN
      reset = colors.Fore.RESET
    return (
        'Commands are:
' + ''.join( ' %s%-*s%s %s
' % (green, length, name, reset, doc) for name, doc in docs)) def _add_command_usage(self, parser, command): """Modifies an OptionParser object with the function's documentation.""" name = command.__name__[3:] if name == 'help': name = '' # Use the module's docstring as the description for the 'help' command if # available. parser.description = (self.module.__doc__ or '').rstrip() if parser.description: parser.description += '

' parser.description += self._gen_commands_list() # Do not touch epilog. else: # Use the command's docstring if available. For commands, unlike module # docstring, realign. lines = (command.__doc__ or '').rstrip().splitlines() if lines[:1]: rest = textwrap.dedent('
'.join(lines[1:])) parser.description = '
'.join((lines[0], rest)) else: parser.description = lines[0] if parser.description: parser.description += '
' parser.epilog = getattr(command, 'epilog', None) if parser.epilog: parser.epilog = '
' + parser.epilog.strip() + '
' more = getattr(command, 'usage_more', '') parser.set_usage( 'usage: %%prog %s [options]%s' % (name, '' if not more else ' ' + more)) @staticmethod def _create_command_summary(name, command): """Creates a oneline summary from the command's docstring.""" if name != command.__name__[3:]: # Skip aliases. return '' doc = command.__doc__ or '' line = doc.split('
', 1)[0].rstrip('.') if not line: return line return (line[0].lower() + line[1:]).strip() def execute(self, parser, args): """Dispatches execution to the right command. Fallbacks to 'help' if not disabled. """ # Unconditionally disable format_description() and format_epilog(). # Technically, a formatter should be used but it's not worth (yet) the # trouble. parser.format_description = lambda _: parser.description or '' parser.format_epilog = lambda _: parser.epilog or '' if args: if args[0] in ('-h', '--help') and len(args) > 1: # Inverse the argument order so 'tool --help cmd' is rewritten to # 'tool cmd --help'. args = [args[1], args[0]] + args[2:] command = self.find_nearest_command(args[0]) if command: if command.__name__ == 'CMDhelp' and len(args) > 1: # Inverse the arguments order so 'tool help cmd' is rewritten to # 'tool cmd --help'. Do it here since we want 'tool hel cmd' to work # too. args = [args[1], '--help'] + args[2:] command = self.find_nearest_command(args[0]) or command # "fix" the usage and the description now that we know the subcommand. self._add_command_usage(parser, command) return command(parser, args[1:]) cmdhelp = self.enumerate_commands().get('help') if cmdhelp: # Not a known command. Default to help. self._add_command_usage(parser, cmdhelp) return cmdhelp(parser, args) # Nothing can be done. return 2

gcient.py에서 dispatcher = subcomamnd.CommandDispatcher(__name__),전송된 파라미터가 gclient임을 알 수 있습니다.py 이 모듈.CommandDispatcher의init__중,self.module는 gclent입니다.그걸 알면 뒤에 있는 엔umrate에 대해commands 함수는 유용합니다.그 이름에서 알 수 있듯이 지원하는 모든 명령을 일일이 열거하고, 되돌아오는 결과는 dict이며, 키는 명령 이름이고, 값은 대응하는 처리 함수이다. 예를 들어 {"sync": CMDsync}.기타 함수add_command_usage, _create_command_summary, _gen_commands_list는 보조 함수로 기능도 비교적 명확하다.비교적 복잡한 것은findnearest_command, 이것은enumerate 에서commands에서 생성한 dict 사전에서 명령을 찾습니다. 정확하게 일치하는 명령이 없으면 모호한 검색을 통해 명령에 대응하는 처리 함수를 되돌려줍니다.
############# gclient.py ##################
class OptionParser(optparse.OptionParser):
  gclientfile_default = os.environ.get('GCLIENT_FILE', '.gclient')

  def __init__(self, **kwargs):
    optparse.OptionParser.__init__(
        self, version='%prog ' + __version__, **kwargs)

    # Some arm boards have issues with parallel sync.
    if platform.machine().startswith('arm'):
      jobs = 1
    else:
      jobs = max(8, gclient_utils.NumLocalCpus())
    # cmp: 2013/06/19
    # Temporary workaround to lower bot-load on SVN server.
    # Bypassed if a bot_update flag is detected.
    if (os.environ.get('CHROME_HEADLESS') == '1' and
        not os.path.exists('update.flag')):
      jobs = 1

    self.add_option(
        '-j', '--jobs', default=jobs, type='int',
        help='Specify how many SCM commands can run in parallel; defaults to '
             '%default on this machine')
    self.add_option(
        '-v', '--verbose', action='count', default=0,
        help='Produces additional output for diagnostics. Can be used up to '
             'three times for more logging info.')
    self.add_option(
        '--gclientfile', dest='config_filename',
        help='Specify an alternate %s file' % self.gclientfile_default)
    self.add_option(
        '--spec',
        help='create a gclient file containing the provided string. Due to '
            'Cygwin/Python brokenness, it can\'t contain any newlines.')
    self.add_option(
        '--no-nag-max', default=False, action='store_true',
        help='Ignored for backwards compatibility.')

OptionParser 매개변수는 gclient입니다.py의 한 종류로 서브 모듈의optparser를 계승합니다.OptionParser.주요 기능은 파라미터를 해석하고tuple(options,args)을 되돌려 주는 것이다. OptionParser라는 예에 대해 명확하게 설명한다.반환 값은 입력 매개 변수가 포맷 분석된 결과입니다.이 함수 처리는 두 가지 측면으로 나뉘는데, 하나는 조회를 돕는 것이고, 하나는 명령을 실행하는 것이다.도움말 질의에서 사용자가 입력한 명령은 gclient --help sync 등입니다.사실,execute에 전달될 때 매개 변수는 --help sync로 변한다. 그러면 먼저 매개 변수의 위치를 바꾸어서sync--help, 즉args[0]를 "sync",args[1]을 --help로 하고 self를 호출한다.fine_nearest_command 함수, 정확한 명령이나 가장 비슷한 모호한 일치하는 명령을 찾고, 결과 (명령 처리 함수는 CMD로 시작) 를 command 변수에 부여합니다.
execute()에 들어가기, 매개변수 해석,findnearest_commands () 명령을 받은 후 도움말 명령인지 판단하고, 그렇지 않으면 대응하는 처리 함수인returncommand (parser,args[1:]) 를 직접 호출합니다.위 예제에서는 CMDsync 함수를 호출하고 매개변수는 parser 및 sync 이후의 매개변수입니다.
CMDsync가 입력 매개변수를 어떻게 처리하는지 확인합니다.
#############gclient.py#############
def CMDsync(parser, args):
  """Checkout/update all modules."""
  parser.add_option('-f', '--force', action='store_true',
                    help='force update even for unchanged modules')
  parser.add_option('-n', '--nohooks', action='store_true',
                    help='don\'t run hooks after the update is complete')
  parser.add_option('-p', '--noprehooks', action='store_true',
                    help='don\'t run pre-DEPS hooks', default=False)
  parser.add_option('-r', '--revision', action='append',
                    dest='revisions', metavar='REV', default=[],
                    help='Enforces revision/hash for the solutions with the '
                         'format src@rev. The src@ part is optional and can be '
                         'skipped. -r can be used multiple times when .gclient '
                         'has multiple solutions configured and will work even '
                         'if the src@ part is skipped. Note that specifying '
                         '--revision means your safesync_url gets ignored.')
  parser.add_option('--with_branch_heads', action='store_true',
                    help='Clone git "branch_heads" refspecs in addition to '
                         'the default refspecs. This adds about 1/2GB to a '
                         'full checkout. (git only)')
  parser.add_option('-t', '--transitive', action='store_true',
                    help='When a revision is specified (in the DEPS file or '
                          'with the command-line flag), transitively update '
                          'the dependencies to the date of the given revision. '
                          'Only supported for SVN repositories.')
  parser.add_option('-H', '--head', action='store_true',
                    help='skips any safesync_urls specified in '
                         'configured solutions and sync to head instead')
  parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
                    help='Deletes from the working copy any dependencies that '
                         'have been removed since the last sync, as long as '
                         'there are no local modifications. When used with '
                         '--force, such dependencies are removed even if they '
                         'have local modifications. When used with --reset, '
                         'all untracked directories are removed from the '
                         'working copy, excluding those which are explicitly '
                         'ignored in the repository.')
  parser.add_option('-R', '--reset', action='store_true',
                    help='resets any local changes before updating (git only)')
  parser.add_option('-M', '--merge', action='store_true',
                    help='merge upstream changes instead of trying to '
                         'fast-forward or rebase')
  parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
                    help='override deps for the specified (comma-separated) '
                         'platform(s); \'all\' will process all deps_os '
                         'references')
  parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
                    help='Skip svn up whenever possible by requesting '
                         'actual HEAD revision from the repository')
  parser.add_option('--upstream', action='store_true',
                    help='Make repo state match upstream branch.')
  parser.add_option('--output-json',
                    help='Output a json document to this path containing '
                         'summary information about the sync.')
  (options, args) = parser.parse_args(args)
  client = GClient.LoadCurrentConfig(options)

  if not client:
    raise gclient_utils.Error('client not configured; see \'gclient config\'')

  if options.revisions and options.head:
    # TODO(maruel): Make it a parser.error if it doesn't break any builder.
    print('Warning: you cannot use both --head and --revision')

  if options.verbose:
    # Print out the .gclient file.  This is longer than if we just printed the
    # client dict, but more legible, and it might contain helpful comments.
    print(client.config_content)
  ret = client.RunOnDeps('update', args)
  if options.output_json:
    slns = {}
    for d in client.subtree(True):
      normed = d.name.replace('\\', '/').rstrip('/') + '/'
      slns[normed] = {
          'revision': d.got_revision,
          'scm': d.used_scm.name if d.used_scm else None,
      }
    with open(options.output_json, 'wb') as f:
      json.dump({'solutions': slns}, f)
  return ret


CMDupdate = CMDsync

options, args = parser.parse_args(args), CMDsync 명령 매개변수를 확인한 다음 Gclient를 호출합니다.LoadCurrentConfig(options) - 쉽게 볼 때 gclient sync를 입력하면 options와args가 비어 있습니다.
############glcient.py#############
def LoadCurrentConfig(options):
    """Searches for and loads a .gclient file relative to the current working
    dir. Returns a GClient object."""
    if options.spec:
      client = GClient('.', options)
      client.SetConfig(options.spec)
    else:
      path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
      if not path:
        return None
      client = GClient(path, options)
      client.SetConfig(gclient_utils.FileRead(
          os.path.join(path, options.config_filename)))

    if (options.revisions and
        len(client.dependencies) > 1 and
        any('@' not in r for r in options.revisions)):
      print >> sys.stderr, (
          'You must specify the full solution name like --revision %s@%s
' 'when you have multiple solutions setup in your .gclient file.
' 'Other solutions present are: %s.') % ( client.dependencies[0].name, options.revisions[0], ', '.join(s.name for s in client.dependencies[1:])) return client

이 함수는 현재 디렉터리에 조회됩니다.glcient 파일, 최종적으로client 대상을 되돌려줍니다.이 함수는 GClient(path, options)에 호출됩니다.
############## gclient.py ##################
class GClient(Dependency):
  """Object that represent a gclient checkout. A tree of Dependency(), one per
  solution or DEPS entry."""

  DEPS_OS_CHOICES = {
    "win32": "win",
    "win": "win",
    "cygwin": "win",
    "darwin": "mac",
    "mac": "mac",
    "unix": "unix",
    "linux": "unix",
    "linux2": "unix",
    "linux3": "unix",
    "android": "android",
  }

  DEFAULT_CLIENT_FILE_TEXT = ("""\
solutions = [
  { "name"        : "%(solution_name)s",
    "url"         : "%(solution_url)s",
    "deps_file"   : "%(deps_file)s",
    "managed"     : %(managed)s,
    "custom_deps" : {
    },
    "safesync_url": "%(safesync_url)s",
  },
]
cache_dir = %(cache_dir)r
""")

  DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
  { "name"        : "%(solution_name)s",
    "url"         : "%(solution_url)s",
    "deps_file"   : "%(deps_file)s",
    "managed"     : %(managed)s,
    "custom_deps" : {
%(solution_deps)s    },
    "safesync_url": "%(safesync_url)s",
  },
""")

  DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
# Snapshot generated with gclient revinfo --snapshot
solutions = [
%(solution_list)s]
""")

  def __init__(self, root_dir, options):
    # Do not change previous behavior. Only solution level and immediate DEPS
    # are processed.
    self._recursion_limit = 2
    Dependency.__init__(self, None, None, None, None, True, None, None, None,
                        'unused', True)
    self._options = options
    if options.deps_os:
      enforced_os = options.deps_os.split(',')
    else:
      enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
    if 'all' in enforced_os:
      enforced_os = self.DEPS_OS_CHOICES.itervalues()
    self._enforced_os = tuple(set(enforced_os))
    self._root_dir = root_dir
    self.config_content = None

  def SetConfig(self, content):
    assert not self.dependencies
    config_dict = {}
    self.config_content = content
    try:
      exec(content, config_dict)
    except SyntaxError, e:
      gclient_utils.SyntaxErrorToError('.gclient', e)

    # Append any target OS that is not already being enforced to the tuple.
    target_os = config_dict.get('target_os', [])
    if config_dict.get('target_os_only', False):
      self._enforced_os = tuple(set(target_os))
    else:
      self._enforced_os = tuple(set(self._enforced_os).union(target_os))

    gclient_scm.GitWrapper.cache_dir = config_dict.get('cache_dir')

    if not target_os and config_dict.get('target_os_only', False):
      raise gclient_utils.Error('Can\'t use target_os_only if target_os is '
                                'not specified')

    deps_to_add = []
    for s in config_dict.get('solutions', []):
      try:
        deps_to_add.append(Dependency(
            self, s['name'], s['url'],
            s.get('safesync_url', None),
            s.get('managed', True),
            s.get('custom_deps', {}),
            s.get('custom_vars', {}),
            s.get('custom_hooks', []),
            s.get('deps_file', 'DEPS'),
            True))
      except KeyError:
        raise gclient_utils.Error('Invalid .gclient file. Solution is '
                                  'incomplete: %s' % s)
    self.add_dependencies_and_close(deps_to_add, config_dict.get('hooks', []))
    logging.info('SetConfig() done')

  def SaveConfig(self):
    gclient_utils.FileWrite(os.path.join(self.root_dir,
                                         self._options.config_filename),
                            self.config_content)

      

  def RunOnDeps(self, command, args, ignore_requirements=False, progress=True):
    """Runs a command on each dependency in a client and its dependencies.

    Args:
      command: The command to use (e.g., 'status' or 'diff')
      args: list of str - extra arguments to add to the command line.
    """
    if not self.dependencies:
      raise gclient_utils.Error('No solution specified')
    revision_overrides = {}
    # It's unnecessary to check for revision overrides for 'recurse'.
    # Save a few seconds by not calling _EnforceRevisions() in that case.
    if command not in ('diff', 'recurse', 'runhooks', 'status'):
      revision_overrides = self._EnforceRevisions()
    pm = None
    # Disable progress for non-tty stdout.
    if (sys.stdout.isatty() and not self._options.verbose and progress):
      if command in ('update', 'revert'):
        pm = Progress('Syncing projects', 1)
      elif command == 'recurse':
        pm = Progress(' '.join(args), 1)
    work_queue = gclient_utils.ExecutionQueue(
        self._options.jobs, pm, ignore_requirements=ignore_requirements)
    for s in self.dependencies:
      work_queue.enqueue(s)
    work_queue.flush(revision_overrides, command, args, options=self._options)

    # Once all the dependencies have been processed, it's now safe to run the
    # hooks.
    if not self._options.nohooks:
      self.RunHooksRecursively(self._options)

    if command == 'update':
      # Notify the user if there is an orphaned entry in their working copy.
      # Only delete the directory if there are no changes in it, and
      # delete_unversioned_trees is set to true.
      entries = [i.name for i in self.root.subtree(False) if i.url]
      full_entries = [os.path.join(self.root_dir, e.replace('/', os.path.sep))
                      for e in entries]

      for entry, prev_url in self._ReadEntries().iteritems():
        if not prev_url:
          # entry must have been overridden via .gclient custom_deps
          continue
        # Fix path separator on Windows.
        entry_fixed = entry.replace('/', os.path.sep)
        e_dir = os.path.join(self.root_dir, entry_fixed)

        def _IsParentOfAny(parent, path_list):
          parent_plus_slash = parent + '/'
          return any(
              path[:len(parent_plus_slash)] == parent_plus_slash
              for path in path_list)

        # Use entry and not entry_fixed there.
        if (entry not in entries and
            (not any(path.startswith(entry + '/') for path in entries)) and
            os.path.exists(e_dir)):
          scm = gclient_scm.CreateSCM(prev_url, self.root_dir, entry_fixed)

          # Check to see if this directory is now part of a higher-up checkout.
          if scm.GetCheckoutRoot() in full_entries:
            logging.info('%s is part of a higher level checkout, not '
                         'removing.', scm.GetCheckoutRoot())
            continue

          file_list = []
          scm.status(self._options, [], file_list)
          modified_files = file_list != []
          if (not self._options.delete_unversioned_trees or
              (modified_files and not self._options.force)):
            # There are modified files in this entry. Keep warning until
            # removed.
            print(('
WARNING: \'%s\' is no longer part of this client. ' 'It is recommended that you manually remove it.
') % entry_fixed) else: # Delete the entry print('
________ deleting \'%s\' in \'%s\'' % ( entry_fixed, self.root_dir)) gclient_utils.rmtree(e_dir) # record the current list of entries for next time self._SaveEntries() return 0

client.SetConfig () 설정 파일의 Solutions를 읽고 dependencies의list 변수 depsto_dd, 그리고 호출.add_dependencies_and_close, 매개 변수는 deps 를 포함합니다to_add.adddependencies_and_close 함수에서 deps를 검증합니다. 유효하면 의존 트리에 추가합니다.
CMDsync 함수에서 client를 실행합니다.RunOnDeps().이것은 dependengcies의 모든 작업을 gclient 에 추가합니다tuils.ExecutionQueue 대기열을 실행하고모든 작업을 완료한 후 runhook을 실행합니다.
######## src/DEPS #############

hooks = [
  {
    # A change to a .gyp, .gypi, or to GYP itself should run the generator.
    "pattern": ".",
    "action": ["python", "src/build/gyp_chromium"],
  },
]

위쪽은 src 아래의 DEPS에서 hooks에 관한 부분입니다. 즉,sync 명령을 실행한 후 파일 업데이트는 src/build/gypchromium 파일.

좋은 웹페이지 즐겨찾기