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