Home | History | Annotate | Download | only in git
      1 #!/usr/bin/env python
      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 
     18 This creates symlinks from the internal git repository directory so
     19 that the build system can see changes in the version state. We also
     20 remember what branch git was on so when the branch changes we can
     21 detect that the ref file is no longer correct (so we can suggest users
     22 run ./configure again).
     23 
     24 NOTE: this script is only used in opensource.
     25 
     26 """
     27 from __future__ import absolute_import
     28 from __future__ import division
     29 from __future__ import print_function
     30 import argparse
     31 import json
     32 import os
     33 import subprocess
     34 import shutil
     35 
     36 
     37 def parse_branch_ref(filename):
     38   """Given a filename of a .git/HEAD file return ref path.
     39 
     40   In particular, if git is in detached head state, this will
     41   return None. If git is in attached head, it will return
     42   the branch reference. E.g. if on 'master', the HEAD will
     43   contain 'ref: refs/heads/master' so 'refs/heads/master'
     44   will be returned.
     45 
     46   Example: parse_branch_ref(".git/HEAD")
     47   Args:
     48     filename: file to treat as a git HEAD file
     49   Returns:
     50     None if detached head, otherwise ref subpath
     51   Raises:
     52     RuntimeError: if the HEAD file is unparseable.
     53   """
     54 
     55   data = open(filename).read().strip()
     56   items = data.split(" ")
     57   if len(items) == 1:
     58     return None
     59   elif len(items) == 2 and items[0] == "ref:":
     60     return items[1].strip()
     61   else:
     62     raise RuntimeError("Git directory has unparseable HEAD")
     63 
     64 
     65 def configure(src_base_path, gen_path, debug=False):
     66   """Configure `src_base_path` to embed git hashes if available."""
     67 
     68   # TODO(aselle): No files generated or symlinked here are deleted by
     69   # the build system. I don't know of a way to do it in bazel. It
     70   # should only be a problem if somebody moves a sandbox directory
     71   # without running ./configure again.
     72 
     73   git_path = os.path.join(src_base_path, ".git")
     74 
     75   # Remove and recreate the path
     76   if os.path.exists(gen_path):
     77     if os.path.isdir(gen_path):
     78       try:
     79         shutil.rmtree(gen_path)
     80       except OSError:
     81         raise RuntimeError("Cannot delete directory %s due to permission "
     82                            "error, inspect and remove manually" % gen_path)
     83     else:
     84       raise RuntimeError("Cannot delete non-directory %s, inspect ",
     85                          "and remove manually" % gen_path)
     86   os.makedirs(gen_path)
     87 
     88   if not os.path.isdir(gen_path):
     89     raise RuntimeError("gen_git_source.py: Failed to create dir")
     90 
     91   # file that specifies what the state of the git repo is
     92   spec = {}
     93 
     94   # value file names will be mapped to the keys
     95   link_map = {"head": None, "branch_ref": None}
     96 
     97   if not os.path.isdir(git_path):
     98     # No git directory
     99     spec["git"] = False
    100     open(os.path.join(gen_path, "head"), "w").write("")
    101     open(os.path.join(gen_path, "branch_ref"), "w").write("")
    102   else:
    103     # Git directory, possibly detached or attached
    104     spec["git"] = True
    105     spec["path"] = src_base_path
    106     git_head_path = os.path.join(git_path, "HEAD")
    107     spec["branch"] = parse_branch_ref(git_head_path)
    108     link_map["head"] = git_head_path
    109     if spec["branch"] is not None:
    110       # attached method
    111       link_map["branch_ref"] = os.path.join(git_path, *
    112                                             os.path.split(spec["branch"]))
    113   # Create symlinks or dummy files
    114   for target, src in link_map.items():
    115     if src is None:
    116       open(os.path.join(gen_path, target), "w").write("")
    117     else:
    118       try:
    119         # In python 3.5, symlink function exists even on Windows. But requires
    120         # Windows Admin privileges, otherwise an OSError will be thrown.
    121         if hasattr(os, 'symlink'):
    122           os.symlink(src, os.path.join(gen_path, target))
    123         else:
    124           shutil.copy2(src, os.path.join(gen_path, target))
    125       except OSError:
    126         shutil.copy2(src, os.path.join(gen_path, target))
    127 
    128   json.dump(spec, open(os.path.join(gen_path, "spec.json"), "w"), indent=2)
    129   if debug:
    130     print("gen_git_source.py: list %s" % gen_path)
    131     print("gen_git_source.py: %s" + repr(os.listdir(gen_path)))
    132     print("gen_git_source.py: spec is %r" % spec)
    133 
    134 
    135 def get_git_version(git_base_path):
    136   """Get the git version from the repository.
    137 
    138   This function runs `git describe ...` in the path given as `git_base_path`.
    139   This will return a string of the form:
    140   <base-tag>-<number of commits since tag>-<shortened sha hash>
    141 
    142   For example, 'v0.10.0-1585-gbb717a6' means v0.10.0 was the last tag when
    143   compiled. 1585 commits are after that commit tag, and we can get back to this
    144   version by running `git checkout gbb717a6`.
    145 
    146   Args:
    147     git_base_path: where the .git directory is located
    148   Returns:
    149     A bytestring representing the git version
    150   """
    151   unknown_label = b"unknown"
    152   try:
    153     val = bytes(subprocess.check_output([
    154         "git", str("--git-dir=%s/.git" % git_base_path),
    155         str("--work-tree=" + git_base_path), "describe", "--long", "--tags"
    156     ]).strip())
    157     return val if val else unknown_label
    158   except subprocess.CalledProcessError:
    159     return unknown_label
    160 
    161 
    162 def write_version_info(filename, git_version):
    163   """Write a c file that defines the version functions.
    164 
    165   Args:
    166     filename: filename to write to.
    167     git_version: the result of a git describe.
    168   """
    169   if b"\"" in git_version or b"\\" in git_version:
    170     git_version = "git_version_is_invalid"  # do not cause build to fail!
    171   contents = """/*  Generated by gen_git_source.py  */
    172 #include <string>
    173 const char* tf_git_version() {return "%s";}
    174 const char* tf_compiler_version() {return __VERSION__;}
    175 const int tf_cxx11_abi_flag() {
    176 #ifdef _GLIBCXX_USE_CXX11_ABI
    177   return _GLIBCXX_USE_CXX11_ABI;
    178 #else
    179   return 0;
    180 #endif
    181 }
    182 const int tf_monolithic_build() {
    183 #ifdef TENSORFLOW_MONOLITHIC_BUILD
    184   return 1;
    185 #else
    186   return 0;
    187 #endif
    188 }
    189 """ % git_version
    190   open(filename, "w").write(contents)
    191 
    192 
    193 def generate(arglist):
    194   """Generate version_info.cc as given `destination_file`.
    195 
    196   Args:
    197     arglist: should be a sequence that contains
    198              spec, head_symlink, ref_symlink, destination_file.
    199 
    200   `destination_file` is the filename where version_info.cc will be written
    201 
    202   `spec` is a filename where the file contains a JSON dictionary
    203     'git' bool that is true if the source is in a git repo
    204     'path' base path of the source code
    205     'branch' the name of the ref specification of the current branch/tag
    206 
    207   `head_symlink` is a filename to HEAD that is cross-referenced against
    208     what is contained in the json branch designation.
    209 
    210   `ref_symlink` is unused in this script but passed, because the build
    211     system uses that file to detect when commits happen.
    212 
    213   Raises:
    214     RuntimeError: If ./configure needs to be run, RuntimeError will be raised.
    215   """
    216 
    217   # unused ref_symlink arg
    218   spec, head_symlink, _, dest_file = arglist
    219   data = json.load(open(spec))
    220   git_version = None
    221   if not data["git"]:
    222     git_version = b"unknown"
    223   else:
    224     old_branch = data["branch"]
    225     new_branch = parse_branch_ref(head_symlink)
    226     if new_branch != old_branch:
    227       raise RuntimeError(
    228           "Run ./configure again, branch was '%s' but is now '%s'" %
    229           (old_branch, new_branch))
    230     git_version = get_git_version(data["path"])
    231   write_version_info(dest_file, git_version)
    232 
    233 
    234 def raw_generate(output_file):
    235   """Simple generator used for cmake/make build systems.
    236 
    237   This does not create any symlinks. It requires the build system
    238   to build unconditionally.
    239 
    240   Args:
    241     output_file: Output filename for the version info cc
    242   """
    243 
    244   git_version = get_git_version(".")
    245   write_version_info(output_file, git_version)
    246 
    247 
    248 parser = argparse.ArgumentParser(description="""Git hash injection into bazel.
    249 If used with --configure <path> will search for git directory and put symlinks
    250 into source so that a bazel genrule can call --generate""")
    251 
    252 parser.add_argument(
    253     "--debug",
    254     type=bool,
    255     help="print debugging information about paths",
    256     default=False)
    257 
    258 parser.add_argument(
    259     "--configure", type=str,
    260     help="Path to configure as a git repo dependency tracking sentinel")
    261 
    262 parser.add_argument(
    263     "--gen_root_path", type=str,
    264     help="Root path to place generated git files (created by --configure).")
    265 
    266 parser.add_argument(
    267     "--generate",
    268     type=str,
    269     help="Generate given spec-file, HEAD-symlink-file, ref-symlink-file",
    270     nargs="+")
    271 
    272 parser.add_argument(
    273     "--raw_generate",
    274     type=str,
    275     help="Generate version_info.cc (simpler version used for cmake/make)")
    276 
    277 args = parser.parse_args()
    278 
    279 if args.configure is not None:
    280   if args.gen_root_path is None:
    281     raise RuntimeError("Must pass --gen_root_path arg when running --configure")
    282   configure(args.configure, args.gen_root_path, debug=args.debug)
    283 elif args.generate is not None:
    284   generate(args.generate)
    285 elif args.raw_generate is not None:
    286   raw_generate(args.raw_generate)
    287 else:
    288   raise RuntimeError("--configure or --generate or --raw_generate "
    289                      "must be used")
    290