1 2# This file helps to compute a version number in source trees obtained from 3# git-archive tarball (such as those provided by githubs download-from-tag 4# feature). Distribution tarballs (built by setup.py sdist) and build 5# directories (produced by setup.py build) will contain a much shorter file 6# that just contains the computed version number. 7 8# This file is released into the public domain. Generated by 9# versioneer-0.18 (https://github.com/warner/python-versioneer) 10 11"""Git implementation of _version.py.""" 12 13import errno 14import os 15import re 16import subprocess 17import sys 18 19 20def get_keywords(): 21 """Get the keywords needed to look up the version information.""" 22 # these strings will be replaced by git during git-archive. 23 # setup.py/versioneer.py will grep for the variable names, so they must 24 # each be defined on a line of their own. _version.py will just call 25 # get_keywords(). 26 git_refnames = " (tag: 0.4.11)" 27 git_full = "fe416b437c28cd6cf383248b90005a2d516549f2" 28 git_date = "2021-01-29 22:33:25 -0800" 29 keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 30 return keywords 31 32 33class VersioneerConfig: 34 """Container for Versioneer configuration parameters.""" 35 36 37def get_config(): 38 """Create, populate and return the VersioneerConfig() object.""" 39 # these strings are filled in when 'setup.py versioneer' creates 40 # _version.py 41 cfg = VersioneerConfig() 42 cfg.VCS = "git" 43 cfg.style = "pep440-pre" 44 cfg.tag_prefix = "" 45 cfg.parentdir_prefix = "ffsubsync-" 46 cfg.versionfile_source = "ffsubsync/_version.py" 47 cfg.verbose = False 48 return cfg 49 50 51class NotThisMethod(Exception): 52 """Exception raised if a method is not valid for the current scenario.""" 53 54 55LONG_VERSION_PY = {} 56HANDLERS = {} 57 58 59def register_vcs_handler(vcs, method): # decorator 60 """Decorator to mark a method as the handler for a particular VCS.""" 61 def decorate(f): 62 """Store f in HANDLERS[vcs][method].""" 63 if vcs not in HANDLERS: 64 HANDLERS[vcs] = {} 65 HANDLERS[vcs][method] = f 66 return f 67 return decorate 68 69 70def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 71 env=None): 72 """Call the given command(s).""" 73 assert isinstance(commands, list) 74 p = None 75 for c in commands: 76 try: 77 dispcmd = str([c] + args) 78 # remember shell=False, so use git.cmd on windows, not just git 79 p = subprocess.Popen([c] + args, cwd=cwd, env=env, 80 stdout=subprocess.PIPE, 81 stderr=(subprocess.PIPE if hide_stderr 82 else None)) 83 break 84 except EnvironmentError: 85 e = sys.exc_info()[1] 86 if e.errno == errno.ENOENT: 87 continue 88 if verbose: 89 print("unable to run %s" % dispcmd) 90 print(e) 91 return None, None 92 else: 93 if verbose: 94 print("unable to find command, tried %s" % (commands,)) 95 return None, None 96 stdout = p.communicate()[0].strip() 97 if sys.version_info[0] >= 3: 98 stdout = stdout.decode() 99 if p.returncode != 0: 100 if verbose: 101 print("unable to run %s (error)" % dispcmd) 102 print("stdout was %s" % stdout) 103 return None, p.returncode 104 return stdout, p.returncode 105 106 107def versions_from_parentdir(parentdir_prefix, root, verbose): 108 """Try to determine the version from the parent directory name. 109 110 Source tarballs conventionally unpack into a directory that includes both 111 the project name and a version string. We will also support searching up 112 two directory levels for an appropriately named parent directory 113 """ 114 rootdirs = [] 115 116 for i in range(3): 117 dirname = os.path.basename(root) 118 if dirname.startswith(parentdir_prefix): 119 return {"version": dirname[len(parentdir_prefix):], 120 "full-revisionid": None, 121 "dirty": False, "error": None, "date": None} 122 else: 123 rootdirs.append(root) 124 root = os.path.dirname(root) # up a level 125 126 if verbose: 127 print("Tried directories %s but none started with prefix %s" % 128 (str(rootdirs), parentdir_prefix)) 129 raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 130 131 132@register_vcs_handler("git", "get_keywords") 133def git_get_keywords(versionfile_abs): 134 """Extract version information from the given file.""" 135 # the code embedded in _version.py can just fetch the value of these 136 # keywords. When used from setup.py, we don't want to import _version.py, 137 # so we do it with a regexp instead. This function is not used from 138 # _version.py. 139 keywords = {} 140 try: 141 f = open(versionfile_abs, "r") 142 for line in f.readlines(): 143 if line.strip().startswith("git_refnames ="): 144 mo = re.search(r'=\s*"(.*)"', line) 145 if mo: 146 keywords["refnames"] = mo.group(1) 147 if line.strip().startswith("git_full ="): 148 mo = re.search(r'=\s*"(.*)"', line) 149 if mo: 150 keywords["full"] = mo.group(1) 151 if line.strip().startswith("git_date ="): 152 mo = re.search(r'=\s*"(.*)"', line) 153 if mo: 154 keywords["date"] = mo.group(1) 155 f.close() 156 except EnvironmentError: 157 pass 158 return keywords 159 160 161@register_vcs_handler("git", "keywords") 162def git_versions_from_keywords(keywords, tag_prefix, verbose): 163 """Get version information from git keywords.""" 164 if not keywords: 165 raise NotThisMethod("no keywords at all, weird") 166 date = keywords.get("date") 167 if date is not None: 168 # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 169 # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 170 # -like" string, which we must then edit to make compliant), because 171 # it's been around since git-1.5.3, and it's too difficult to 172 # discover which version we're using, or to work around using an 173 # older one. 174 date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 175 refnames = keywords["refnames"].strip() 176 if refnames.startswith("$Format"): 177 if verbose: 178 print("keywords are unexpanded, not using") 179 raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 180 refs = set([r.strip() for r in refnames.strip("()").split(",")]) 181 # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 182 # just "foo-1.0". If we see a "tag: " prefix, prefer those. 183 TAG = "tag: " 184 tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 185 if not tags: 186 # Either we're using git < 1.8.3, or there really are no tags. We use 187 # a heuristic: assume all version tags have a digit. The old git %d 188 # expansion behaves like git log --decorate=short and strips out the 189 # refs/heads/ and refs/tags/ prefixes that would let us distinguish 190 # between branches and tags. By ignoring refnames without digits, we 191 # filter out many common branch names like "release" and 192 # "stabilization", as well as "HEAD" and "master". 193 tags = set([r for r in refs if re.search(r'\d', r)]) 194 if verbose: 195 print("discarding '%s', no digits" % ",".join(refs - tags)) 196 if verbose: 197 print("likely tags: %s" % ",".join(sorted(tags))) 198 for ref in sorted(tags): 199 # sorting will prefer e.g. "2.0" over "2.0rc1" 200 if ref.startswith(tag_prefix): 201 r = ref[len(tag_prefix):] 202 if verbose: 203 print("picking %s" % r) 204 return {"version": r, 205 "full-revisionid": keywords["full"].strip(), 206 "dirty": False, "error": None, 207 "date": date} 208 # no suitable tags, so version is "0+unknown", but full hex is still there 209 if verbose: 210 print("no suitable tags, using unknown + full revision id") 211 return {"version": "0+unknown", 212 "full-revisionid": keywords["full"].strip(), 213 "dirty": False, "error": "no suitable tags", "date": None} 214 215 216@register_vcs_handler("git", "pieces_from_vcs") 217def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 218 """Get version from 'git describe' in the root of the source tree. 219 220 This only gets called if the git-archive 'subst' keywords were *not* 221 expanded, and _version.py hasn't already been rewritten with a short 222 version string, meaning we're inside a checked out source tree. 223 """ 224 GITS = ["git"] 225 if sys.platform == "win32": 226 GITS = ["git.cmd", "git.exe"] 227 228 out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 229 hide_stderr=True) 230 if rc != 0: 231 if verbose: 232 print("Directory %s not under git control" % root) 233 raise NotThisMethod("'git rev-parse --git-dir' returned error") 234 235 # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 236 # if there isn't one, this yields HEX[-dirty] (no NUM) 237 describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 238 "--always", "--long", 239 "--match", "%s*" % tag_prefix], 240 cwd=root) 241 # --long was added in git-1.5.5 242 if describe_out is None: 243 raise NotThisMethod("'git describe' failed") 244 describe_out = describe_out.strip() 245 full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 246 if full_out is None: 247 raise NotThisMethod("'git rev-parse' failed") 248 full_out = full_out.strip() 249 250 pieces = {} 251 pieces["long"] = full_out 252 pieces["short"] = full_out[:7] # maybe improved later 253 pieces["error"] = None 254 255 # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 256 # TAG might have hyphens. 257 git_describe = describe_out 258 259 # look for -dirty suffix 260 dirty = git_describe.endswith("-dirty") 261 pieces["dirty"] = dirty 262 if dirty: 263 git_describe = git_describe[:git_describe.rindex("-dirty")] 264 265 # now we have TAG-NUM-gHEX or HEX 266 267 if "-" in git_describe: 268 # TAG-NUM-gHEX 269 mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 270 if not mo: 271 # unparseable. Maybe git-describe is misbehaving? 272 pieces["error"] = ("unable to parse git-describe output: '%s'" 273 % describe_out) 274 return pieces 275 276 # tag 277 full_tag = mo.group(1) 278 if not full_tag.startswith(tag_prefix): 279 if verbose: 280 fmt = "tag '%s' doesn't start with prefix '%s'" 281 print(fmt % (full_tag, tag_prefix)) 282 pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 283 % (full_tag, tag_prefix)) 284 return pieces 285 pieces["closest-tag"] = full_tag[len(tag_prefix):] 286 287 # distance: number of commits since tag 288 pieces["distance"] = int(mo.group(2)) 289 290 # commit: short hex revision ID 291 pieces["short"] = mo.group(3) 292 293 else: 294 # HEX: no tags 295 pieces["closest-tag"] = None 296 count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 297 cwd=root) 298 pieces["distance"] = int(count_out) # total number of commits 299 300 # commit date: see ISO-8601 comment in git_versions_from_keywords() 301 date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], 302 cwd=root)[0].strip() 303 pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 304 305 return pieces 306 307 308def plus_or_dot(pieces): 309 """Return a + if we don't already have one, else return a .""" 310 if "+" in pieces.get("closest-tag", ""): 311 return "." 312 return "+" 313 314 315def render_pep440(pieces): 316 """Build up version string, with post-release "local version identifier". 317 318 Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 319 get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 320 321 Exceptions: 322 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 323 """ 324 if pieces["closest-tag"]: 325 rendered = pieces["closest-tag"] 326 if pieces["distance"] or pieces["dirty"]: 327 rendered += plus_or_dot(pieces) 328 rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 329 if pieces["dirty"]: 330 rendered += ".dirty" 331 else: 332 # exception #1 333 rendered = "0+untagged.%d.g%s" % (pieces["distance"], 334 pieces["short"]) 335 if pieces["dirty"]: 336 rendered += ".dirty" 337 return rendered 338 339 340def render_pep440_pre(pieces): 341 """TAG[.post.devDISTANCE] -- No -dirty. 342 343 Exceptions: 344 1: no tags. 0.post.devDISTANCE 345 """ 346 if pieces["closest-tag"]: 347 rendered = pieces["closest-tag"] 348 if pieces["distance"]: 349 rendered += ".post.dev%d" % pieces["distance"] 350 else: 351 # exception #1 352 rendered = "0.post.dev%d" % pieces["distance"] 353 return rendered 354 355 356def render_pep440_post(pieces): 357 """TAG[.postDISTANCE[.dev0]+gHEX] . 358 359 The ".dev0" means dirty. Note that .dev0 sorts backwards 360 (a dirty tree will appear "older" than the corresponding clean one), 361 but you shouldn't be releasing software with -dirty anyways. 362 363 Exceptions: 364 1: no tags. 0.postDISTANCE[.dev0] 365 """ 366 if pieces["closest-tag"]: 367 rendered = pieces["closest-tag"] 368 if pieces["distance"] or pieces["dirty"]: 369 rendered += ".post%d" % pieces["distance"] 370 if pieces["dirty"]: 371 rendered += ".dev0" 372 rendered += plus_or_dot(pieces) 373 rendered += "g%s" % pieces["short"] 374 else: 375 # exception #1 376 rendered = "0.post%d" % pieces["distance"] 377 if pieces["dirty"]: 378 rendered += ".dev0" 379 rendered += "+g%s" % pieces["short"] 380 return rendered 381 382 383def render_pep440_old(pieces): 384 """TAG[.postDISTANCE[.dev0]] . 385 386 The ".dev0" means dirty. 387 388 Eexceptions: 389 1: no tags. 0.postDISTANCE[.dev0] 390 """ 391 if pieces["closest-tag"]: 392 rendered = pieces["closest-tag"] 393 if pieces["distance"] or pieces["dirty"]: 394 rendered += ".post%d" % pieces["distance"] 395 if pieces["dirty"]: 396 rendered += ".dev0" 397 else: 398 # exception #1 399 rendered = "0.post%d" % pieces["distance"] 400 if pieces["dirty"]: 401 rendered += ".dev0" 402 return rendered 403 404 405def render_git_describe(pieces): 406 """TAG[-DISTANCE-gHEX][-dirty]. 407 408 Like 'git describe --tags --dirty --always'. 409 410 Exceptions: 411 1: no tags. HEX[-dirty] (note: no 'g' prefix) 412 """ 413 if pieces["closest-tag"]: 414 rendered = pieces["closest-tag"] 415 if pieces["distance"]: 416 rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 417 else: 418 # exception #1 419 rendered = pieces["short"] 420 if pieces["dirty"]: 421 rendered += "-dirty" 422 return rendered 423 424 425def render_git_describe_long(pieces): 426 """TAG-DISTANCE-gHEX[-dirty]. 427 428 Like 'git describe --tags --dirty --always -long'. 429 The distance/hash is unconditional. 430 431 Exceptions: 432 1: no tags. HEX[-dirty] (note: no 'g' prefix) 433 """ 434 if pieces["closest-tag"]: 435 rendered = pieces["closest-tag"] 436 rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 437 else: 438 # exception #1 439 rendered = pieces["short"] 440 if pieces["dirty"]: 441 rendered += "-dirty" 442 return rendered 443 444 445def render(pieces, style): 446 """Render the given version pieces into the requested style.""" 447 if pieces["error"]: 448 return {"version": "unknown", 449 "full-revisionid": pieces.get("long"), 450 "dirty": None, 451 "error": pieces["error"], 452 "date": None} 453 454 if not style or style == "default": 455 style = "pep440" # the default 456 457 if style == "pep440": 458 rendered = render_pep440(pieces) 459 elif style == "pep440-pre": 460 rendered = render_pep440_pre(pieces) 461 elif style == "pep440-post": 462 rendered = render_pep440_post(pieces) 463 elif style == "pep440-old": 464 rendered = render_pep440_old(pieces) 465 elif style == "git-describe": 466 rendered = render_git_describe(pieces) 467 elif style == "git-describe-long": 468 rendered = render_git_describe_long(pieces) 469 else: 470 raise ValueError("unknown style '%s'" % style) 471 472 return {"version": rendered, "full-revisionid": pieces["long"], 473 "dirty": pieces["dirty"], "error": None, 474 "date": pieces.get("date")} 475 476 477def get_versions(): 478 """Get version information or return default if unable to do so.""" 479 # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 480 # __file__, we can work backwards from there to the root. Some 481 # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 482 # case we can only use expanded keywords. 483 484 cfg = get_config() 485 verbose = cfg.verbose 486 487 try: 488 return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 489 verbose) 490 except NotThisMethod: 491 pass 492 493 try: 494 root = os.path.realpath(__file__) 495 # versionfile_source is the relative path from the top of the source 496 # tree (where the .git directory might live) to this file. Invert 497 # this to find the root from __file__. 498 for i in cfg.versionfile_source.split('/'): 499 root = os.path.dirname(root) 500 except NameError: 501 return {"version": "0+unknown", "full-revisionid": None, 502 "dirty": None, 503 "error": "unable to find root of source tree", 504 "date": None} 505 506 try: 507 pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 508 return render(pieces, cfg.style) 509 except NotThisMethod: 510 pass 511 512 try: 513 if cfg.parentdir_prefix: 514 return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 515 except NotThisMethod: 516 pass 517 518 return {"version": "0+unknown", "full-revisionid": None, 519 "dirty": None, 520 "error": "unable to compute version", "date": None} 521