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