PageRenderTime 57ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/src/tito/common.py

https://github.com/iNecas/tito
Python | 557 lines | 518 code | 10 blank | 29 comment | 6 complexity | 5b7e85e7a2b7712e775c6e5abeb92b87 MD5 | raw file
Possible License(s): GPL-2.0
  1. # Copyright (c) 2008-2010 Red Hat, Inc.
  2. #
  3. # This software is licensed to you under the GNU General Public License,
  4. # version 2 (GPLv2). There is NO WARRANTY for this software, express or
  5. # implied, including the implied warranties of MERCHANTABILITY or FITNESS
  6. # FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
  7. # along with this software; if not, see
  8. # http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
  9. #
  10. # Red Hat trademarks are not licensed under GPLv2. No permission is
  11. # granted to use or replicate Red Hat trademarks that are incorporated
  12. # in this software or its documentation.
  13. """
  14. Common operations.
  15. """
  16. import os
  17. import re
  18. import sys
  19. import commands
  20. import traceback
  21. DEFAULT_BUILD_DIR = "/tmp/tito"
  22. DEFAULT_BUILDER = "default_builder"
  23. DEFAULT_TAGGER = "default_tagger"
  24. GLOBALCONFIG_SECTION = "globalconfig"
  25. # Define some shortcuts to fully qualified Builder classes to make things
  26. # a little more concise for CLI users. Mock is probably the only one this
  27. # is relevant for at this time.
  28. BUILDER_SHORTCUTS = {
  29. 'mock': 'tito.builder.MockBuilder'
  30. }
  31. def extract_sources(spec_file_lines):
  32. """
  33. Returns a list of sources from the given spec file.
  34. Some of these will be URL's, which is fine they will be ignored.
  35. We're really just after relative filenames that might live in the same
  36. location as the spec file, mostly used with NoTgzBuilder packages.
  37. """
  38. filenames = []
  39. source_pattern = re.compile('^Source\d+?:\s*(.*)')
  40. for line in spec_file_lines:
  41. match = source_pattern.match(line)
  42. if match:
  43. filenames.append(match.group(1))
  44. return filenames
  45. def extract_bzs(output):
  46. """
  47. Parses the output of CVS diff or a series of git commit log entries,
  48. looking for new lines which look like a commit of the format:
  49. ######: Commit message
  50. Returns a list of lines of text similar to:
  51. Resolves: #XXXXXX - Commit message
  52. """
  53. regex = re.compile(r"^- (\d*)\s?[:-]+\s?(.*)")
  54. diff_regex = re.compile(r"^(\+- )+(\d*)\s?[:-]+\s?(.*)")
  55. bzs = []
  56. for line in output.split("\n"):
  57. match = re.match(regex, line)
  58. match2 = re.match(diff_regex, line)
  59. if match:
  60. bzs.append((match.group(1), match.group(2)))
  61. elif match2:
  62. bzs.append((match2.group(2), match2.group(3)))
  63. output = []
  64. for bz in bzs:
  65. output.append("Resolves: #%s - %s" % (bz[0], bz[1]))
  66. return output
  67. #BZ = {}
  68. #result = None
  69. #for line in reversed(output.split('\n')):
  70. # m = re.match("(\d+)\s+-\s+(.*)", line)
  71. # if m:
  72. # bz_number = m.group(1)
  73. # if bz_number in BZ:
  74. # line = "Related: #%s - %s" % (bz_number, m.group(2))
  75. # else:
  76. # line = "Resolves: #%s - %s" % (bz_number, m.group(2))
  77. # BZ[bz_number] = 1
  78. # if result:
  79. # result = line + "\n" + result
  80. # else:
  81. # result = line
  82. def error_out(error_msgs):
  83. """
  84. Print the given error message (or list of messages) and exit.
  85. """
  86. print
  87. if isinstance(error_msgs, list):
  88. for line in error_msgs:
  89. print("ERROR: %s" % line)
  90. else:
  91. print("ERROR: %s" % error_msgs)
  92. print
  93. if 'DEBUG' in os.environ:
  94. traceback.print_stack()
  95. sys.exit(1)
  96. def create_builder(package_name, build_tag, build_version,
  97. pkg_config, build_dir, global_config, user_config, args,
  98. builder_class=None, **kwargs):
  99. """
  100. Create (but don't run) the builder class. Builder object may be
  101. used by other objects without actually having run() called.
  102. """
  103. # Allow some shorter names for builders for CLI users.
  104. if builder_class in BUILDER_SHORTCUTS:
  105. builder_class = BUILDER_SHORTCUTS[builder_class]
  106. if builder_class is None:
  107. debug("---- Builder class is None")
  108. if pkg_config.has_option("buildconfig", "builder"):
  109. builder_class = get_class_by_name(pkg_config.get("buildconfig",
  110. "builder"))
  111. else:
  112. debug("---- Global config")
  113. builder_class = get_class_by_name(global_config.get(
  114. GLOBALCONFIG_SECTION, DEFAULT_BUILDER))
  115. else:
  116. # We were given an explicit builder class as a str, get the actual
  117. # class reference:
  118. builder_class = get_class_by_name(builder_class)
  119. debug("Using builder class: %s" % builder_class)
  120. # Instantiate the builder:
  121. builder = builder_class(
  122. name=package_name,
  123. version=build_version,
  124. tag=build_tag,
  125. build_dir=build_dir,
  126. pkg_config=pkg_config,
  127. global_config=global_config,
  128. user_config=user_config,
  129. args=args,
  130. **kwargs)
  131. return builder
  132. def find_spec_file(in_dir=None):
  133. """
  134. Find the first spec file in the current directory. (hopefully there's
  135. only one)
  136. Returns only the file name, rather than the full path.
  137. """
  138. if in_dir == None:
  139. in_dir = os.getcwd()
  140. for f in os.listdir(in_dir):
  141. if f.endswith(".spec"):
  142. return f
  143. error_out(["Unable to locate a spec file in %s" % in_dir])
  144. def find_git_root():
  145. """
  146. Find the top-level directory for this git repository.
  147. Returned as a full path.
  148. """
  149. (status, cdup) = commands.getstatusoutput("git rev-parse --show-cdup")
  150. if status > 0:
  151. error_out(["%s does not appear to be within a git checkout." % \
  152. os.getcwd()])
  153. if cdup.strip() == "":
  154. cdup = "./"
  155. return os.path.abspath(cdup)
  156. def run_command(command):
  157. debug(command)
  158. (status, output) = commands.getstatusoutput(command)
  159. if status > 0:
  160. sys.stderr.write("\n########## ERROR ############\n")
  161. sys.stderr.write("Error running command: %s\n" % command)
  162. sys.stderr.write("Status code: %s\n" % status)
  163. sys.stderr.write("Command output: %s\n" % output)
  164. raise Exception("Error running command")
  165. return output
  166. def tag_exists_locally(tag):
  167. (status, output) = commands.getstatusoutput("git tag | grep %s" % tag)
  168. if status > 0:
  169. return False
  170. else:
  171. return True
  172. def tag_exists_remotely(tag):
  173. """ Returns True if the tag exists in the remote git repo. """
  174. try:
  175. repo_url = get_git_repo_url()
  176. except:
  177. sys.stderr.write('Warning: remote.origin do not exist. Assuming --offline, for remote tag checking.')
  178. return False
  179. sha1 = get_remote_tag_sha1(tag)
  180. debug("sha1 = %s" % sha1)
  181. if sha1 == "":
  182. return False
  183. return True
  184. def get_local_tag_sha1(tag):
  185. tag_sha1 = run_command(
  186. "git ls-remote ./. --tag %s | awk '{ print $1 ; exit }'"
  187. % tag)
  188. return tag_sha1
  189. def head_points_to_tag(tag):
  190. """
  191. Ensure the current git head is the same commit as tag.
  192. For some reason the git commands we normally use to fetch SHA1 for a tag
  193. do not work when comparing to the HEAD SHA1. Using a different command
  194. for now.
  195. """
  196. debug("Checking that HEAD commit is %s" % tag)
  197. head_sha1 = run_command("git rev-list --max-count=1 HEAD")
  198. tag_sha1 = run_command("git rev-list --max-count=1 %s" % tag)
  199. debug(" head_sha1 = %s" % head_sha1)
  200. debug(" tag_sha1 = %s" % tag_sha1)
  201. return head_sha1 == tag_sha1
  202. def undo_tag(tag):
  203. """
  204. Executes git commands to delete the given tag and undo the most recent
  205. commit. Assumes you have taken necessary precautions to ensure this is
  206. what you want to do.
  207. """
  208. # Using --merge here as it appears to undo the changes in the commit,
  209. # but preserve any modified files:
  210. output = run_command("git tag -d %s && git reset --merge HEAD^1" % tag)
  211. print(output)
  212. def get_remote_tag_sha1(tag):
  213. """
  214. Get the SHA1 referenced by this git tag in the remote git repo.
  215. Will return "" if the git tag does not exist remotely.
  216. """
  217. # TODO: X11 forwarding messages can appear in this output, find a better way
  218. repo_url = get_git_repo_url()
  219. print("Checking for tag [%s] in git repo [%s]" % (tag, repo_url))
  220. cmd = "git ls-remote %s --tag %s | awk '{ print $1 ; exit }'" % \
  221. (repo_url, tag)
  222. upstream_tag_sha1 = run_command(cmd)
  223. return upstream_tag_sha1
  224. def check_tag_exists(tag, offline=False):
  225. """
  226. Check that the given git tag exists in a git repository.
  227. """
  228. if not tag_exists_locally(tag):
  229. error_out("Tag does not exist locally: [%s]" % tag)
  230. if offline:
  231. return
  232. tag_sha1 = get_local_tag_sha1(tag)
  233. debug("Local tag SHA1: %s" % tag_sha1)
  234. try:
  235. repo_url = get_git_repo_url()
  236. except:
  237. sys.stderr.write('Warning: remote.origin do not exist. Assuming --offline, for remote tag checking.')
  238. return
  239. upstream_tag_sha1 = get_remote_tag_sha1(tag)
  240. if upstream_tag_sha1 == "":
  241. error_out(["Tag does not exist in remote git repo: %s" % tag,
  242. "You must tag, then git push and git push --tags"])
  243. debug("Remote tag SHA1: %s" % upstream_tag_sha1)
  244. if upstream_tag_sha1 != tag_sha1:
  245. error_out("Tag %s references %s locally but %s upstream." % (tag,
  246. tag_sha1, upstream_tag_sha1))
  247. def debug(text):
  248. """
  249. Print the text if --debug was specified.
  250. """
  251. if 'DEBUG' in os.environ:
  252. print(text)
  253. def get_spec_version_and_release(sourcedir, spec_file_name):
  254. command = ("""rpm -q --qf '%%{version}-%%{release}\n' --define """
  255. """"_sourcedir %s" --define 'dist %%undefined' --specfile """
  256. """%s 2> /dev/null | head -1""" % (sourcedir, spec_file_name))
  257. return run_command(command)
  258. def get_project_name(tag=None):
  259. """
  260. Extract the project name from the specified tag or a spec file in the
  261. current working directory. Error out if neither is present.
  262. """
  263. if tag != None:
  264. p = re.compile('(.*?)-(\d.*)')
  265. m = p.match(tag)
  266. if not m:
  267. error_out("Unable to determine project name in tag: %s" % tag)
  268. return m.group(1)
  269. else:
  270. spec_file_path = os.path.join(os.getcwd(), find_spec_file())
  271. if not os.path.exists(spec_file_path):
  272. error_out("spec file: %s does not exist" % spec_file_path)
  273. output = run_command(
  274. "rpm -q --qf '%%{name}\n' --specfile %s 2> /dev/null | head -1" %
  275. spec_file_path)
  276. if not output:
  277. error_out(["Unable to determine project name from spec file: %s" % spec_file_path,
  278. "Try rpm -q --specfile %s" % spec_file_path,
  279. "Try rpmlint -i %s" % spec_file_path])
  280. return output
  281. def replace_version(line, new_version):
  282. """
  283. Attempts to replace common setup.py version formats in the given line,
  284. and return the modified line. If no version is present the line is
  285. returned as is.
  286. Looking for things like version="x.y.z" with configurable case,
  287. whitespace, and optional use of single/double quotes.
  288. """
  289. # Mmmmm pretty regex!
  290. ver_regex = re.compile("(\s*)(version)(\s*)(=)(\s*)(['\"])(.*)(['\"])(.*)",
  291. re.IGNORECASE)
  292. m = ver_regex.match(line)
  293. if m:
  294. result_tuple = list(m.group(1, 2, 3, 4, 5, 6))
  295. result_tuple.append(new_version)
  296. result_tuple.extend(list(m.group(8, 9)))
  297. new_line = "%s%s%s%s%s%s%s%s%s\n" % tuple(result_tuple)
  298. return new_line
  299. else:
  300. return line
  301. def get_relative_project_dir(project_name, commit):
  302. """
  303. Return the project's sub-directory relative to the git root.
  304. This could be a different directory than where the project currently
  305. resides, so we export a copy of the project's metadata from
  306. rel-eng/packages/ at the point in time of the tag we are building.
  307. """
  308. cmd = "git show %s:rel-eng/packages/%s" % (commit,
  309. project_name)
  310. pkg_metadata = run_command(cmd).strip()
  311. tokens = pkg_metadata.split(" ")
  312. debug("Got package metadata: %s" % tokens)
  313. return tokens[1]
  314. def get_build_commit(tag, test=False):
  315. """ Return the git commit we should build. """
  316. if test:
  317. return get_latest_commit(".")
  318. else:
  319. tag_sha1 = run_command(
  320. "git ls-remote ./. --tag %s | awk '{ print $1 ; exit }'"
  321. % tag)
  322. commit_id = run_command('git rev-list --max-count=1 %s' % tag_sha1)
  323. return commit_id
  324. def get_commit_count(tag, commit_id):
  325. """ Return the number of commits between the tag and commit_id"""
  326. # git describe returns either a tag-commitcount-gSHA1 OR
  327. # just the tag.
  328. #
  329. # so we need to pass in the tag as well.
  330. # output = run_command("git describe --match=%s %s" % (tag, commit_id))
  331. # if tag == output:
  332. # return 0
  333. # else:
  334. # parse the count from the output
  335. output = run_command("git describe --match=%s %s" % (tag, commit_id))
  336. debug("tag - %s" % tag)
  337. debug("output - %s" % output)
  338. if tag != output:
  339. # tag-commitcount-gSHA1, we want the penultimate value
  340. cnt = output.split("-")[-2]
  341. return cnt
  342. return 0
  343. def get_latest_commit(path="."):
  344. """ Return the latest git commit for the given path. """
  345. commit_id = run_command("git log --pretty=format:%%H --max-count=1 %s" % path)
  346. return commit_id
  347. def get_commit_timestamp(sha1_or_tag):
  348. """
  349. Get the timestamp of the git commit or tag we're building. Used to
  350. keep the hash the same on all .tar.gz's we generate for a particular
  351. version regardless of when they are generated.
  352. """
  353. output = run_command(
  354. "git rev-list --timestamp --max-count=1 %s | awk '{print $1}'"
  355. % sha1_or_tag)
  356. return output
  357. def create_tgz(git_root, prefix, commit, relative_dir, rel_eng_dir,
  358. dest_tgz):
  359. """
  360. Create a .tar.gz from a projects source in git.
  361. """
  362. os.chdir(os.path.abspath(git_root))
  363. timestamp = get_commit_timestamp(commit)
  364. timestamp_script = get_script_path("tar-fixup-stamp-comment.pl")
  365. #if not os.path.exists(timestamp_script):
  366. # error_out("Unable to locate required script: %s" % timestamp_script)
  367. # Accomodate standalone projects with specfile in root of git repo:
  368. relative_git_dir = "%s" % relative_dir
  369. if relative_git_dir == '/':
  370. relative_git_dir = ""
  371. archive_cmd = ('git archive --format=tar --prefix=%s/ %s:%s '
  372. '| %s %s %s | gzip -n -c - | tee %s' % (
  373. prefix, commit, relative_git_dir, timestamp_script,
  374. timestamp, commit, dest_tgz))
  375. debug(archive_cmd)
  376. run_command(archive_cmd)
  377. def get_git_repo_url():
  378. """
  379. Return the url of this git repo.
  380. Uses ~/.git/config remote origin url.
  381. """
  382. return run_command("git config remote.origin.url")
  383. def get_latest_tagged_version(package_name):
  384. """
  385. Return the latest git tag for this package in the current branch.
  386. Uses the info in rel-eng/packages/package-name.
  387. Returns None if file does not exist.
  388. """
  389. git_root = find_git_root()
  390. rel_eng_dir = os.path.join(git_root, "rel-eng")
  391. file_path = "%s/packages/%s" % (rel_eng_dir, package_name)
  392. debug("Getting latest package info from: %s" % file_path)
  393. if not os.path.exists(file_path):
  394. return None
  395. output = run_command("awk '{ print $1 ; exit }' %s" % file_path)
  396. if output == None or output.strip() == "":
  397. error_out("Error looking up latest tagged version in: %s" % file_path)
  398. return output
  399. def normalize_class_name(name):
  400. """
  401. Just a hack to accomodate tito config files with builder/tagger
  402. classes referenced in the spacewalk.releng namespace, which has
  403. since been renamed to just tito.
  404. """
  405. look_for = "spacewalk.releng."
  406. if name.startswith(look_for):
  407. name = "%s%s" % ("tito.", name[len(look_for):])
  408. return name
  409. def get_script_path(scriptname):
  410. """
  411. Hack to accomodate functional tests running from source, rather than
  412. requiring tito to actually be installed. This variable is only set by
  413. test scripts, normally we assume scripts are on PATH.
  414. """
  415. # TODO: Would be nice to get rid of this hack.
  416. scriptpath = scriptname # assume on PATH by default
  417. if 'TITO_SRC_BIN_DIR' in os.environ:
  418. bin_dir = os.environ['TITO_SRC_BIN_DIR']
  419. scriptpath = os.path.join(bin_dir, scriptname)
  420. return scriptpath
  421. def get_class_by_name(name):
  422. """
  423. Get a Python class specified by it's fully qualified name.
  424. NOTE: Does not actually create an instance of the object, only returns
  425. a Class object.
  426. """
  427. name = normalize_class_name(name)
  428. # Split name into module and class name:
  429. tokens = name.split(".")
  430. class_name = tokens[-1]
  431. module = '.'.join(tokens[0:-1])
  432. debug("Importing %s" % name)
  433. mod = __import__(module, globals(), locals(), [class_name])
  434. return getattr(mod, class_name)
  435. def increase_version(version_string):
  436. regex = re.compile(r"^(%.*)|(.+\.)?([0-9]+)(\..*|%.*|$)")
  437. match = re.match(regex, version_string)
  438. if match:
  439. matches = list(match.groups())
  440. # Increment the number in the third match group, if there is one
  441. if matches[2]:
  442. matches[2] = str(int(matches[2]) + 1)
  443. # Join everything back up, skipping match groups with None
  444. return "".join([x for x in matches if x])
  445. # If no match, return an empty string
  446. return ""
  447. def reset_release(release_string):
  448. regex = re.compile(r"(^|\.)([.0-9]+)(\.|%|$)")
  449. return regex.sub(r"\g<1>1\g<3>", release_string)
  450. def increase_zstream(release_string):
  451. # If we do not have zstream, create .0 and then bump the version
  452. regex = re.compile(r"^(.*%{\?dist})$")
  453. bumped_string = regex.sub(r"\g<1>.0", release_string)
  454. return increase_version(bumped_string)