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