1#!/usr/bin/env python
2# Copyright 2017 the V8 project authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""
7Use this script to update V8 in a Node.js checkout.
8
9Requirements:
10  - Node.js checkout in which V8 should be updated.
11  - V8 checkout at the commit to which Node.js should be updated.
12
13Usage:
14  $ update_node.py <path_to_v8> <path_to_node>
15
16  This will synchronize the content of <path_to_node>/deps/v8 with <path_to_v8>,
17  and a few V8 dependencies require in Node.js. It will also update .gitignore
18  appropriately.
19
20Optional flags:
21  --gclient     Run `gclient sync` on the V8 checkout before updating.
22  --commit      Create commit with the updated V8 in the Node.js checkout.
23  --with-patch  Also include currently staged files in the V8 checkout.
24"""
25
26import argparse
27import os
28import shutil
29import subprocess
30import sys
31import stat
32import node_common
33
34TARGET_SUBDIR = os.path.join("deps", "v8")
35
36SUB_REPOSITORIES = [ ["base", "trace_event", "common"],
37                     ["third_party", "googletest", "src"],
38                     ["third_party", "jinja2"],
39                     ["third_party", "markupsafe"] ]
40
41DELETE_FROM_GITIGNORE = [ "/base",
42                          "/third_party/googletest/src",
43                          "/third_party/jinja2",
44                          "/third_party/markupsafe" ]
45
46# Node.js requires only a single header file from gtest to build V8.
47# Both jinja2 and markupsafe are required to generate part of the inspector.
48ADD_TO_GITIGNORE = [ "/third_party/googletest/*",
49                     "!/third_party/googletest/BUILD.gn",
50                     "!/third_party/googletest/src",
51                     "/third_party/googletest/src/*",
52                     "!/third_party/googletest/src/googletest",
53                     "/third_party/googletest/src/googletest/*",
54                     "!/third_party/googletest/src/googletest/include",
55                     "/third_party/googletest/src/googletest/include/*",
56                     "!/third_party/googletest/src/googletest/include/gtest",
57                     "/third_party/googletest/src/googletest/include/gtest/*",
58                     "!/third_party/googletest/src/googletest/include/gtest/gtest_prod.h",
59                     "!/third_party/jinja2",
60                     "!/third_party/markupsafe" ]
61
62# Node.js owns deps/v8/gypfiles in their downstream repository.
63FILES_TO_KEEP = [ "gypfiles" ]
64
65def RunGclient(path):
66  assert os.path.isdir(path)
67  print ">> Running gclient sync"
68  subprocess.check_call(["gclient", "sync", "--nohooks"], cwd=path)
69
70def CommitPatch(options):
71  """Makes a dummy commit for the changes in the index.
72
73  On trybots, bot_updated applies the patch to the index. We commit it to make
74  the fake git clone fetch it into node.js. We can leave the commit, as
75  bot_update will ensure a clean state on each run.
76  """
77  print ">> Committing patch"
78  subprocess.check_call(
79      ["git", "-c", "user.name=fake", "-c", "user.email=fake@chromium.org",
80       "commit", "--allow-empty", "-m", "placeholder-commit"],
81      cwd=options.v8_path,
82  )
83
84def UpdateTarget(repository, options, files_to_keep):
85  source = os.path.join(options.v8_path, *repository)
86  target = os.path.join(options.node_path, TARGET_SUBDIR, *repository)
87  print ">> Updating target directory %s" % target
88  print ">>     from active branch at %s" % source
89  if not os.path.exists(target):
90    os.makedirs(target)
91  # Remove possible remnants of previous incomplete runs.
92  node_common.UninitGit(target)
93
94  git_args = []
95  git_args.append(["init"])                       # initialize target repo
96
97  if files_to_keep:
98    git_args.append(["add"] + files_to_keep)            # add and commit
99    git_args.append(["commit", "-m", "keep files"])     # files we want to keep
100
101  git_args.append(["remote", "add", "source", source])  # point to source repo
102  git_args.append(["fetch", "source", "HEAD"])          # sync to current branch
103  git_args.append(["checkout", "-f", "FETCH_HEAD"])     # switch to that branch
104  git_args.append(["clean", "-fd"])                     # delete removed files
105
106  if files_to_keep:
107    git_args.append(["cherry-pick", "master"])    # restore kept files
108
109  try:
110    for args in git_args:
111      subprocess.check_call(["git"] + args, cwd=target)
112  except:
113    raise
114  finally:
115    node_common.UninitGit(target)
116
117def UpdateGitIgnore(options):
118  file_name = os.path.join(options.node_path, TARGET_SUBDIR, ".gitignore")
119  assert os.path.isfile(file_name)
120  print ">> Updating .gitignore with lines"
121  with open(file_name) as gitignore:
122    content = gitignore.readlines()
123  content = [x.strip() for x in content]
124  for x in DELETE_FROM_GITIGNORE:
125    if x in content:
126      print "- %s" % x
127      content.remove(x)
128  for x in ADD_TO_GITIGNORE:
129    if x not in content:
130      print "+ %s" % x
131      content.append(x)
132  content.sort(key=lambda x: x[1:] if x.startswith("!") else x)
133  with open(file_name, "w") as gitignore:
134    for x in content:
135      gitignore.write("%s\n" % x)
136
137def CreateCommit(options):
138  print ">> Creating commit."
139  # Find git hash from source.
140  githash = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"],
141                                    cwd=options.v8_path).strip()
142  # Create commit at target.
143  git_commands = [
144    ["git", "checkout", "-b", "update_v8_to_%s" % githash],  # new branch
145    ["git", "add", "."],                                     # add files
146    ["git", "commit", "-m", "Update V8 to %s" % githash]     # new commit
147  ]
148  for command in git_commands:
149    subprocess.check_call(command, cwd=options.node_path)
150
151def ParseOptions(args):
152  parser = argparse.ArgumentParser(description="Update V8 in Node.js")
153  parser.add_argument("v8_path", help="Path to V8 checkout")
154  parser.add_argument("node_path", help="Path to Node.js checkout")
155  parser.add_argument("--gclient", action="store_true", help="Run gclient sync")
156  parser.add_argument("--commit", action="store_true", help="Create commit")
157  parser.add_argument("--with-patch", action="store_true",
158                      help="Apply also staged files")
159  options = parser.parse_args(args)
160  assert os.path.isdir(options.v8_path)
161  options.v8_path = os.path.abspath(options.v8_path)
162  assert os.path.isdir(options.node_path)
163  options.node_path = os.path.abspath(options.node_path)
164  return options
165
166def Main(args):
167  options = ParseOptions(args)
168  if options.gclient:
169    RunGclient(options.v8_path)
170  # Commit patch on trybots to main V8 repository.
171  if options.with_patch:
172    CommitPatch(options)
173  # Update main V8 repository.
174  UpdateTarget([""], options, FILES_TO_KEEP)
175  # Patch .gitignore before updating sub-repositories.
176  UpdateGitIgnore(options)
177  for repo in SUB_REPOSITORIES:
178    UpdateTarget(repo, options, None)
179  if options.commit:
180    CreateCommit(options)
181
182if __name__ == "__main__":
183  Main(sys.argv[1:])
184