• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Lint as: python2, python3
2# Copyright 2016 The TensorFlow Authors. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15# ==============================================================================
16"""Help include git hash in tensorflow bazel build.
17
18This creates symlinks from the internal git repository directory so
19that the build system can see changes in the version state. We also
20remember what branch git was on so when the branch changes we can
21detect that the ref file is no longer correct (so we can suggest users
22run ./configure again).
23
24NOTE: this script is only used in opensource.
25
26"""
27from __future__ import absolute_import
28from __future__ import division
29from __future__ import print_function
30
31import argparse
32from builtins import bytes  # pylint: disable=redefined-builtin
33import json
34import os
35import shutil
36import subprocess
37
38import six
39
40
41def parse_branch_ref(filename):
42  """Given a filename of a .git/HEAD file return ref path.
43
44  In particular, if git is in detached head state, this will
45  return None. If git is in attached head, it will return
46  the branch reference. E.g. if on 'master', the HEAD will
47  contain 'ref: refs/heads/master' so 'refs/heads/master'
48  will be returned.
49
50  Example: parse_branch_ref(".git/HEAD")
51  Args:
52    filename: file to treat as a git HEAD file
53  Returns:
54    None if detached head, otherwise ref subpath
55  Raises:
56    RuntimeError: if the HEAD file is unparseable.
57  """
58
59  data = open(filename).read().strip()
60  items = data.split(" ")
61  if len(items) == 1:
62    return None
63  elif len(items) == 2 and items[0] == "ref:":
64    return items[1].strip()
65  else:
66    raise RuntimeError("Git directory has unparseable HEAD")
67
68
69def configure(src_base_path, gen_path, debug=False):
70  """Configure `src_base_path` to embed git hashes if available."""
71
72  # TODO(aselle): No files generated or symlinked here are deleted by
73  # the build system. I don't know of a way to do it in bazel. It
74  # should only be a problem if somebody moves a sandbox directory
75  # without running ./configure again.
76
77  git_path = os.path.join(src_base_path, ".git")
78
79  # Remove and recreate the path
80  if os.path.exists(gen_path):
81    if os.path.isdir(gen_path):
82      try:
83        shutil.rmtree(gen_path)
84      except OSError:
85        raise RuntimeError("Cannot delete directory %s due to permission "
86                           "error, inspect and remove manually" % gen_path)
87    else:
88      raise RuntimeError("Cannot delete non-directory %s, inspect ",
89                         "and remove manually" % gen_path)
90  os.makedirs(gen_path)
91
92  if not os.path.isdir(gen_path):
93    raise RuntimeError("gen_git_source.py: Failed to create dir")
94
95  # file that specifies what the state of the git repo is
96  spec = {}
97
98  # value file names will be mapped to the keys
99  link_map = {"head": None, "branch_ref": None}
100
101  if not os.path.isdir(git_path):
102    # No git directory
103    spec["git"] = False
104    open(os.path.join(gen_path, "head"), "w").write("")
105    open(os.path.join(gen_path, "branch_ref"), "w").write("")
106  else:
107    # Git directory, possibly detached or attached
108    spec["git"] = True
109    spec["path"] = src_base_path
110    git_head_path = os.path.join(git_path, "HEAD")
111    spec["branch"] = parse_branch_ref(git_head_path)
112    link_map["head"] = git_head_path
113    if spec["branch"] is not None:
114      # attached method
115      link_map["branch_ref"] = os.path.join(git_path, *
116                                            os.path.split(spec["branch"]))
117  # Create symlinks or dummy files
118  for target, src in link_map.items():
119    if src is None:
120      open(os.path.join(gen_path, target), "w").write("")
121    elif not os.path.exists(src):
122      # Git repo is configured in a way we don't support such as having
123      # packed refs. Even though in a git repo, tf.__git_version__ will not
124      # be accurate.
125      # TODO(mikecase): Support grabbing git info when using packed refs.
126      open(os.path.join(gen_path, target), "w").write("")
127      spec["git"] = False
128    else:
129      try:
130        # In python 3.5, symlink function exists even on Windows. But requires
131        # Windows Admin privileges, otherwise an OSError will be thrown.
132        if hasattr(os, "symlink"):
133          os.symlink(src, os.path.join(gen_path, target))
134        else:
135          shutil.copy2(src, os.path.join(gen_path, target))
136      except OSError:
137        shutil.copy2(src, os.path.join(gen_path, target))
138
139  json.dump(spec, open(os.path.join(gen_path, "spec.json"), "w"), indent=2)
140  if debug:
141    print("gen_git_source.py: list %s" % gen_path)
142    print("gen_git_source.py: %s" + repr(os.listdir(gen_path)))
143    print("gen_git_source.py: spec is %r" % spec)
144
145
146def get_git_version(git_base_path, git_tag_override):
147  """Get the git version from the repository.
148
149  This function runs `git describe ...` in the path given as `git_base_path`.
150  This will return a string of the form:
151  <base-tag>-<number of commits since tag>-<shortened sha hash>
152
153  For example, 'v0.10.0-1585-gbb717a6' means v0.10.0 was the last tag when
154  compiled. 1585 commits are after that commit tag, and we can get back to this
155  version by running `git checkout gbb717a6`.
156
157  Args:
158    git_base_path: where the .git directory is located
159    git_tag_override: Override the value for the git tag. This is useful for
160      releases where we want to build the release before the git tag is
161      created.
162  Returns:
163    A bytestring representing the git version
164  """
165  unknown_label = b"unknown"
166  try:
167    # Force to bytes so this works on python 2 and python 3
168    val = bytes(
169        subprocess.check_output([
170            "git",
171            str("--git-dir=%s/.git" % git_base_path),
172            str("--work-tree=" + six.ensure_str(git_base_path)), "describe",
173            "--long", "--tags"
174        ]).strip())
175    version_separator = b"-"
176    if git_tag_override and val:
177      split_val = val.split(version_separator)
178      if len(split_val) < 3:
179        raise Exception(
180            ("Expected git version in format 'TAG-COMMITS AFTER TAG-HASH' "
181             "but got '%s'") % val)
182      # There might be "-" in the tag name. But we can be sure that the final
183      # two "-" are those inserted by the git describe command.
184      abbrev_commit = split_val[-1]
185      val = version_separator.join(
186          [bytes(git_tag_override, "utf-8"), b"0", abbrev_commit])
187    return val if val else unknown_label
188  except (subprocess.CalledProcessError, OSError):
189    return unknown_label
190
191
192def write_version_info(filename, git_version):
193  """Write a c file that defines the version functions.
194
195  Args:
196    filename: filename to write to.
197    git_version: the result of a git describe.
198  """
199  if b"\"" in git_version or b"\\" in git_version:
200    git_version = b"git_version_is_invalid"  # do not cause build to fail!
201  contents = """/*  Generated by gen_git_source.py  */
202#include <string>
203const char* tf_git_version() {return "%s";}
204const char* tf_compiler_version() {
205#ifdef _MSC_VER
206#define STRINGIFY(x) #x
207#define TOSTRING(x) STRINGIFY(x)
208  return "MSVC " TOSTRING(_MSC_FULL_VER);
209#else
210  return __VERSION__;
211#endif
212}
213int tf_cxx11_abi_flag() {
214#ifdef _GLIBCXX_USE_CXX11_ABI
215  return _GLIBCXX_USE_CXX11_ABI;
216#else
217  return 0;
218#endif
219}
220int tf_monolithic_build() {
221#ifdef TENSORFLOW_MONOLITHIC_BUILD
222  return 1;
223#else
224  return 0;
225#endif
226}
227""" % git_version.decode("utf-8")
228  open(filename, "w").write(contents)
229
230
231def generate(arglist, git_tag_override=None):
232  """Generate version_info.cc as given `destination_file`.
233
234  Args:
235    arglist: should be a sequence that contains
236             spec, head_symlink, ref_symlink, destination_file.
237
238  `destination_file` is the filename where version_info.cc will be written
239
240  `spec` is a filename where the file contains a JSON dictionary
241    'git' bool that is true if the source is in a git repo
242    'path' base path of the source code
243    'branch' the name of the ref specification of the current branch/tag
244
245  `head_symlink` is a filename to HEAD that is cross-referenced against
246    what is contained in the json branch designation.
247
248  `ref_symlink` is unused in this script but passed, because the build
249    system uses that file to detect when commits happen.
250
251    git_tag_override: Override the value for the git tag. This is useful for
252      releases where we want to build the release before the git tag is
253      created.
254
255  Raises:
256    RuntimeError: If ./configure needs to be run, RuntimeError will be raised.
257  """
258
259  # unused ref_symlink arg
260  spec, head_symlink, _, dest_file = arglist
261  data = json.load(open(spec))
262  git_version = None
263  if not data["git"]:
264    git_version = b"unknown"
265  else:
266    old_branch = data["branch"]
267    new_branch = parse_branch_ref(head_symlink)
268    if new_branch != old_branch:
269      raise RuntimeError(
270          "Run ./configure again, branch was '%s' but is now '%s'" %
271          (old_branch, new_branch))
272    git_version = get_git_version(data["path"], git_tag_override)
273  write_version_info(dest_file, git_version)
274
275
276def raw_generate(output_file, source_dir, git_tag_override=None):
277  """Simple generator used for cmake/make build systems.
278
279  This does not create any symlinks. It requires the build system
280  to build unconditionally.
281
282  Args:
283    output_file: Output filename for the version info cc
284    source_dir: Base path of the source code
285    git_tag_override: Override the value for the git tag. This is useful for
286      releases where we want to build the release before the git tag is
287      created.
288  """
289
290  git_version = get_git_version(source_dir, git_tag_override)
291  write_version_info(output_file, git_version)
292
293
294parser = argparse.ArgumentParser(description="""Git hash injection into bazel.
295If used with --configure <path> will search for git directory and put symlinks
296into source so that a bazel genrule can call --generate""")
297
298parser.add_argument(
299    "--debug",
300    type=bool,
301    help="print debugging information about paths",
302    default=False)
303
304parser.add_argument(
305    "--configure", type=str,
306    help="Path to configure as a git repo dependency tracking sentinel")
307
308parser.add_argument(
309    "--gen_root_path", type=str,
310    help="Root path to place generated git files (created by --configure).")
311
312parser.add_argument(
313    "--git_tag_override", type=str,
314    help="Override git tag value in the __git_version__ string. Useful when "
315         "creating release builds before the release tag is created.")
316
317parser.add_argument(
318    "--generate",
319    type=str,
320    help="Generate given spec-file, HEAD-symlink-file, ref-symlink-file",
321    nargs="+")
322
323parser.add_argument(
324    "--raw_generate",
325    type=str,
326    help="Generate version_info.cc (simpler version used for cmake/make)")
327
328parser.add_argument(
329    "--source_dir",
330    type=str,
331    help="Base path of the source code (used for cmake/make)")
332
333args = parser.parse_args()
334
335if args.configure is not None:
336  if args.gen_root_path is None:
337    raise RuntimeError("Must pass --gen_root_path arg when running --configure")
338  configure(args.configure, args.gen_root_path, debug=args.debug)
339elif args.generate is not None:
340  generate(args.generate, args.git_tag_override)
341elif args.raw_generate is not None:
342  source_path = "."
343  if args.source_dir is not None:
344    source_path = args.source_dir
345  raw_generate(args.raw_generate, source_path, args.git_tag_override)
346else:
347  raise RuntimeError("--configure or --generate or --raw_generate "
348                     "must be used")
349