xref: /Lucene/dev-tools/scripts/buildAndPushRelease.py (revision f605b4a692789e6b76a784a616d871db710823a8)
187c131baSJan Høydahl#!/usr/bin/env python3
287c131baSJan Høydahl# -*- coding: utf-8 -*-
3d6ab323dSMichael McCandless# Licensed to the Apache Software Foundation (ASF) under one or more
4d6ab323dSMichael McCandless# contributor license agreements.  See the NOTICE file distributed with
5d6ab323dSMichael McCandless# this work for additional information regarding copyright ownership.
6d6ab323dSMichael McCandless# The ASF licenses this file to You under the Apache License, Version 2.0
7d6ab323dSMichael McCandless# (the "License"); you may not use this file except in compliance with
8d6ab323dSMichael McCandless# the License.  You may obtain a copy of the License at
9d6ab323dSMichael McCandless#
10d6ab323dSMichael McCandless#     http://www.apache.org/licenses/LICENSE-2.0
11d6ab323dSMichael McCandless#
12d6ab323dSMichael McCandless# Unless required by applicable law or agreed to in writing, software
13d6ab323dSMichael McCandless# distributed under the License is distributed on an "AS IS" BASIS,
14d6ab323dSMichael McCandless# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15d6ab323dSMichael McCandless# See the License for the specific language governing permissions and
16d6ab323dSMichael McCandless# limitations under the License.
17d6ab323dSMichael McCandless
1880e60002SRyan Ernstimport argparse
19d6ab323dSMichael McCandlessimport datetime
20d6ab323dSMichael McCandlessimport re
219c56dccfSMichael McCandlessimport time
22d6ab323dSMichael McCandlessimport os
23d6ab323dSMichael McCandlessimport sys
249c56dccfSMichael McCandlessimport subprocess
25df1775ffSJan Høydahlfrom subprocess import TimeoutExpired
2680e60002SRyan Ernstimport textwrap
27d60849f3SSteve Roweimport urllib.request, urllib.error, urllib.parse
28d60849f3SSteve Roweimport xml.etree.ElementTree as ET
29d6ab323dSMichael McCandless
3008e38d34SMike Drobimport scriptutil
3108e38d34SMike Drob
32d6ab323dSMichael McCandlessLOG = '/tmp/release.log'
33cdfa11b1SJan Høydahldev_mode = False
34d6ab323dSMichael McCandless
35d6ab323dSMichael McCandlessdef log(msg):
360178e8e6SSteven Rowe  f = open(LOG, mode='ab')
370178e8e6SSteven Rowe  f.write(msg.encode('utf-8'))
38d6ab323dSMichael McCandless  f.close()
39d6ab323dSMichael McCandless
40d6ab323dSMichael McCandlessdef run(command):
41d6ab323dSMichael McCandless  log('\n\n%s: RUN: %s\n' % (datetime.datetime.now(), command))
42d6ab323dSMichael McCandless  if os.system('%s >> %s 2>&1' % (command, LOG)):
43d6ab323dSMichael McCandless    msg = '    FAILED: %s [see log %s]' % (command, LOG)
440178e8e6SSteven Rowe    print(msg)
45d6ab323dSMichael McCandless    raise RuntimeError(msg)
46d6ab323dSMichael McCandless
47cdfa11b1SJan Høydahl
489c56dccfSMichael McCandlessdef runAndSendGPGPassword(command, password):
49fdff37f3SShalin Shekhar Mangar  p = subprocess.Popen(command, shell=True, bufsize=0, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE)
509c56dccfSMichael McCandless  f = open(LOG, 'ab')
519c56dccfSMichael McCandless  while True:
52d0b01a2bSShalin Shekhar Mangar    p.stdout.flush()
539c56dccfSMichael McCandless    line = p.stdout.readline()
549c56dccfSMichael McCandless    if len(line) == 0:
559c56dccfSMichael McCandless      break
569c56dccfSMichael McCandless    f.write(line)
579c56dccfSMichael McCandless    if line.find(b'Enter GPG keystore password:') != -1:
589c56dccfSMichael McCandless      time.sleep(1.0)
599c56dccfSMichael McCandless      p.stdin.write((password + '\n').encode('UTF-8'))
609c56dccfSMichael McCandless      p.stdin.write('\n'.encode('UTF-8'))
619c56dccfSMichael McCandless
622a13e783SSteve Rowe  try:
632a13e783SSteve Rowe    result = p.wait(timeout=120)
642a13e783SSteve Rowe    if result != 0:
659c56dccfSMichael McCandless      msg = '    FAILED: %s [see log %s]' % (command, LOG)
669c56dccfSMichael McCandless      print(msg)
679c56dccfSMichael McCandless      raise RuntimeError(msg)
682a13e783SSteve Rowe  except TimeoutExpired:
692a13e783SSteve Rowe    msg = '    FAILED: %s [timed out after 2 minutes; see log %s]' % (command, LOG)
702a13e783SSteve Rowe    print(msg)
712a13e783SSteve Rowe    raise RuntimeError(msg)
729c56dccfSMichael McCandless
73982ee393SJan Høydahldef load(urlString, encoding="utf-8"):
74d60849f3SSteve Rowe  try:
75982ee393SJan Høydahl    content = urllib.request.urlopen(urlString).read().decode(encoding)
76d60849f3SSteve Rowe  except Exception as e:
77d60849f3SSteve Rowe    print('Retrying download of url %s after exception: %s' % (urlString, e))
78982ee393SJan Høydahl    content = urllib.request.urlopen(urlString).read().decode(encoding)
79d60849f3SSteve Rowe  return content
80d60849f3SSteve Rowe
81f8be973bSMike McCandlessdef getGitRev():
82cdfa11b1SJan Høydahl  if not dev_mode:
83f8be973bSMike McCandless    status = os.popen('git status').read().strip()
84a6e14ec6SIshan Chattopadhyaya    if 'nothing to commit, working directory clean' not in status and 'nothing to commit, working tree clean' not in status:
85de9d4ac3SMike McCandless      raise RuntimeError('git clone is dirty:\n\n%s' % status)
86d23f37d0SDawid Weiss    if 'Your branch is ahead of' in status:
87d23f37d0SDawid Weiss      raise RuntimeError('Your local branch is ahead of the remote? git status says:\n%s' % status)
887509b9c9SMike McCandless    print('  git clone is clean')
89cdfa11b1SJan Høydahl  else:
90cdfa11b1SJan Høydahl    print('  Ignoring dirty git clone due to dev-mode')
917509b9c9SMike McCandless  return os.popen('git rev-parse HEAD').read().strip()
92d6ab323dSMichael McCandless
93cdfa11b1SJan Høydahl
94cdfa11b1SJan Høydahldef prepare(root, version, gpg_key_id, gpg_password, gpg_home=None, sign_gradle=False):
950178e8e6SSteven Rowe  print()
960178e8e6SSteven Rowe  print('Prepare release...')
97d6ab323dSMichael McCandless  if os.path.exists(LOG):
98d6ab323dSMichael McCandless    os.remove(LOG)
99d6ab323dSMichael McCandless
100cdfa11b1SJan Høydahl  if not dev_mode:
101d6ab323dSMichael McCandless    os.chdir(root)
1027263491dSnknize    print('  git pull...')
1037263491dSnknize    run('git pull')
104cdfa11b1SJan Høydahl  else:
105cdfa11b1SJan Høydahl    print('  Development mode, not running git pull')
106d6ab323dSMichael McCandless
107f8be973bSMike McCandless  rev = getGitRev()
108f8be973bSMike McCandless  print('  git rev: %s' % rev)
109f8be973bSMike McCandless  log('\nGIT rev: %s\n' % rev)
110d6ab323dSMichael McCandless
111d60849f3SSteve Rowe  print('  Check DOAP files')
112d60849f3SSteve Rowe  checkDOAPfiles(version)
113d60849f3SSteve Rowe
114cdfa11b1SJan Høydahl  if not dev_mode:
115*f605b4a6SAdrien Grand    print('  ./gradlew --no-daemon clean check')
116*f605b4a6SAdrien Grand    run('./gradlew --no-daemon clean check')
117cdfa11b1SJan Høydahl  else:
118cdfa11b1SJan Høydahl    print('  skipping precommit check due to dev-mode')
119d6ab323dSMichael McCandless
1203134f10aSMike Drob  print('  prepare-release')
121f5486d13SJan Høydahl  cmd = './gradlew --no-daemon assembleRelease' \
122cdfa11b1SJan Høydahl        ' -Dversion.release=%s' % version
123cdfa11b1SJan Høydahl  if dev_mode:
124cdfa11b1SJan Høydahl    cmd += ' -Pvalidation.git.failOnModified=false'
125cdfa11b1SJan Høydahl  if gpg_key_id is not None:
126f5486d13SJan Høydahl    cmd += ' -Psign --max-workers 2'
127cdfa11b1SJan Høydahl    if sign_gradle:
128cdfa11b1SJan Høydahl      print("  Signing method is gradle java-plugin")
129cdfa11b1SJan Høydahl      cmd += ' -Psigning.keyId="%s"' % gpg_key_id
130cdfa11b1SJan Høydahl      if gpg_home is not None:
131cdfa11b1SJan Høydahl        cmd += ' -Psigning.secretKeyRingFile="%s"' % os.path.join(gpg_home, 'secring.gpg')
132cdfa11b1SJan Høydahl      if gpg_password is not None:
133cdfa11b1SJan Høydahl        # Pass gpg passphrase as env.var to gradle rather than as plaintext argument
134cdfa11b1SJan Høydahl        os.environ['ORG_GRADLE_PROJECT_signingPassword'] = gpg_password
135cdfa11b1SJan Høydahl    else:
136cdfa11b1SJan Høydahl      print("  Signing method is gpg tool")
137cdfa11b1SJan Høydahl      cmd += ' -PuseGpg -Psigning.gnupg.keyName="%s"' % gpg_key_id
138cdfa11b1SJan Høydahl      if gpg_home is not None:
139cdfa11b1SJan Høydahl        cmd += ' -Psigning.gnupg.homeDir="%s"' % gpg_home
1409c56dccfSMichael McCandless
141cdfa11b1SJan Høydahl  print("  Running: %s" % cmd)
142cdfa11b1SJan Høydahl  if gpg_password is not None:
143cdfa11b1SJan Høydahl    runAndSendGPGPassword(cmd, gpg_password)
1449c56dccfSMichael McCandless  else:
145879e8250SMichael McCandless    run(cmd)
1469c56dccfSMichael McCandless
1470178e8e6SSteven Rowe  print('  done!')
1480178e8e6SSteven Rowe  print()
149d6ab323dSMichael McCandless  return rev
150d6ab323dSMichael McCandless
151d60849f3SSteve RowereVersion1 = re.compile(r'\>(\d+)\.(\d+)\.(\d+)(-alpha|-beta)?/\<', re.IGNORECASE)
152d60849f3SSteve RowereVersion2 = re.compile(r'-(\d+)\.(\d+)\.(\d+)(-alpha|-beta)?\.zip<', re.IGNORECASE)
153d60849f3SSteve RowereDoapRevision = re.compile(r'(\d+)\.(\d+)(?:\.(\d+))?(-alpha|-beta)?', re.IGNORECASE)
154d60849f3SSteve Rowedef checkDOAPfiles(version):
155674b66ddSJan Høydahl  # In Lucene DOAP file, verify presence of all releases less than the one being produced.
156d60849f3SSteve Rowe  errorMessages = []
157674b66ddSJan Høydahl  for product in ['lucene']:
158d60849f3SSteve Rowe    url = 'https://archive.apache.org/dist/lucene/%s' % ('java' if product == 'lucene' else product)
159d60849f3SSteve Rowe    distpage = load(url)
160d60849f3SSteve Rowe    releases = set()
161d60849f3SSteve Rowe    for regex in reVersion1, reVersion2:
162d60849f3SSteve Rowe      for tup in regex.findall(distpage):
163d60849f3SSteve Rowe        if tup[0] in ('1', '2'):                    # Ignore 1.X and 2.X releases
164d60849f3SSteve Rowe          continue
165d60849f3SSteve Rowe        releases.add(normalizeVersion(tup))
166d60849f3SSteve Rowe    doapNS = '{http://usefulinc.com/ns/doap#}'
167d60849f3SSteve Rowe    xpathRevision = '{0}Project/{0}release/{0}Version/{0}revision'.format(doapNS)
168d60849f3SSteve Rowe    doapFile = "dev-tools/doap/%s.rdf" % product
169d60849f3SSteve Rowe    treeRoot = ET.parse(doapFile).getroot()
170d60849f3SSteve Rowe    doapRevisions = set()
171d60849f3SSteve Rowe    for revision in treeRoot.findall(xpathRevision):
172d60849f3SSteve Rowe      match = reDoapRevision.match(revision.text)
173d60849f3SSteve Rowe      if (match is not None):
174d60849f3SSteve Rowe        if (match.group(1) not in ('0', '1', '2')): # Ignore 0.X, 1.X and 2.X revisions
175d60849f3SSteve Rowe          doapRevisions.add(normalizeVersion(match.groups()))
176d60849f3SSteve Rowe      else:
177d60849f3SSteve Rowe        errorMessages.append('ERROR: Failed to parse revision: %s in %s' % (revision.text, doapFile))
178d60849f3SSteve Rowe    missingDoapRevisions = set()
179d60849f3SSteve Rowe    for release in releases:
180d60849f3SSteve Rowe      if release not in doapRevisions and release < version: # Ignore releases greater than the one being produced
181d60849f3SSteve Rowe        missingDoapRevisions.add(release)
182d60849f3SSteve Rowe    if len(missingDoapRevisions) > 0:
183d60849f3SSteve Rowe      errorMessages.append('ERROR: Missing revision(s) in %s: %s' % (doapFile, ', '.join(sorted(missingDoapRevisions))))
184d60849f3SSteve Rowe  if (len(errorMessages) > 0):
185d60849f3SSteve Rowe    raise RuntimeError('\n%s\n(Hint: copy/paste from the stable branch version of the file(s).)'
186d60849f3SSteve Rowe                       % '\n'.join(errorMessages))
187d60849f3SSteve Rowe
188d60849f3SSteve Rowedef normalizeVersion(tup):
189d60849f3SSteve Rowe  suffix = ''
190d60849f3SSteve Rowe  if tup[-1] is not None and tup[-1].lower() == '-alpha':
191d60849f3SSteve Rowe    tup = tup[:(len(tup) - 1)]
192d60849f3SSteve Rowe    suffix = '-ALPHA'
193d60849f3SSteve Rowe  elif tup[-1] is not None and tup[-1].lower() == '-beta':
194d60849f3SSteve Rowe    tup = tup[:(len(tup) - 1)]
195d60849f3SSteve Rowe    suffix = '-BETA'
196d60849f3SSteve Rowe  while tup[-1] in ('', None):
197d60849f3SSteve Rowe    tup = tup[:(len(tup) - 1)]
198d60849f3SSteve Rowe  while len(tup) < 3:
199d60849f3SSteve Rowe    tup = tup + ('0',)
200d60849f3SSteve Rowe  return '.'.join(tup) + suffix
201d60849f3SSteve Rowe
202cdfa11b1SJan Høydahl
2030544819bSDawid Weissdef pushLocal(version, root, rcNum, localDir):
2040178e8e6SSteven Rowe  print('Push local [%s]...' % localDir)
205879e8250SMichael McCandless  os.makedirs(localDir)
206879e8250SMichael McCandless
2070544819bSDawid Weiss  lucene_dist_dir = '%s/lucene/distribution/build/release' % root
2080544819bSDawid Weiss  rev = open('%s/lucene/distribution/build/release/.gitrev' % root, encoding='UTF-8').read()
2090544819bSDawid Weiss
2100544819bSDawid Weiss  dir = 'lucene-%s-RC%d-rev-%s' % (version, rcNum, rev)
211879e8250SMichael McCandless  os.makedirs('%s/%s/lucene' % (localDir, dir))
2120178e8e6SSteven Rowe  print('  Lucene')
21308e38d34SMike Drob  os.chdir(lucene_dist_dir)
2145b8f0a5eSJan Høydahl  print('    archive...')
2155b8f0a5eSJan Høydahl  if os.path.exists('lucene.tar'):
2165b8f0a5eSJan Høydahl    os.remove('lucene.tar')
2175b8f0a5eSJan Høydahl  run('tar cf lucene.tar *')
218879e8250SMichael McCandless
219879e8250SMichael McCandless  os.chdir('%s/%s/lucene' % (localDir, dir))
2205b8f0a5eSJan Høydahl  print('    extract...')
2215b8f0a5eSJan Høydahl  run('tar xf "%s/lucene.tar"' % lucene_dist_dir)
2225b8f0a5eSJan Høydahl  os.remove('%s/lucene.tar' % lucene_dist_dir)
223674b66ddSJan Høydahl  os.chdir('..')
224879e8250SMichael McCandless
2250178e8e6SSteven Rowe  print('  chmod...')
226879e8250SMichael McCandless  run('chmod -R a+rX-w .')
227879e8250SMichael McCandless
2280178e8e6SSteven Rowe  print('  done!')
229879e8250SMichael McCandless  return 'file://%s/%s' % (os.path.abspath(localDir), dir)
230d6ab323dSMichael McCandless
231cdfa11b1SJan Høydahl
23280e60002SRyan Ernstdef read_version(path):
23308e38d34SMike Drob  return scriptutil.find_current_version()
234879e8250SMichael McCandless
235cdfa11b1SJan Høydahl
23680e60002SRyan Ernstdef parse_config():
23780e60002SRyan Ernst  epilogue = textwrap.dedent('''
23880e60002SRyan Ernst    Example usage for a Release Manager:
2398cb2773dSSteve Rowe    python3 -u dev-tools/scripts/buildAndPushRelease.py --push-local /tmp/releases/6.0.1 --sign 6E68DA61 --rc-num 1
24080e60002SRyan Ernst  ''')
24180e60002SRyan Ernst  description = 'Utility to build, push, and test a release.'
24280e60002SRyan Ernst  parser = argparse.ArgumentParser(description=description, epilog=epilogue,
24380e60002SRyan Ernst                                   formatter_class=argparse.RawDescriptionHelpFormatter)
24480e60002SRyan Ernst  parser.add_argument('--no-prepare', dest='prepare', default=True, action='store_false',
24580e60002SRyan Ernst                      help='Use the already built release in the provided checkout')
246982ee393SJan Høydahl  parser.add_argument('--local-keys', metavar='PATH',
247982ee393SJan Høydahl                      help='Uses local KEYS file to validate presence of RM\'s gpg key')
24880e60002SRyan Ernst  parser.add_argument('--push-local', metavar='PATH',
24980e60002SRyan Ernst                      help='Push the release to the local path')
25080e60002SRyan Ernst  parser.add_argument('--sign', metavar='KEYID',
25180e60002SRyan Ernst                      help='Sign the release with the given gpg key')
252cdfa11b1SJan Høydahl  parser.add_argument('--sign-method-gradle', dest='sign_method_gradle', default=False, action='store_true',
253cdfa11b1SJan Høydahl                      help='Use Gradle built-in GPG signing instead of gpg command for signing artifacts. '
254cdfa11b1SJan Høydahl                      ' This may require --gpg-secring argument if your keychain cannot be resolved automatically.')
255cdfa11b1SJan Høydahl  parser.add_argument('--gpg-pass-noprompt', dest='gpg_pass_noprompt', default=False, action='store_true',
256cdfa11b1SJan Høydahl                      help='Do not prompt for gpg passphrase. For the default gnupg method, this means your gpg-agent'
257cdfa11b1SJan Høydahl                      ' needs a non-TTY pin-entry program. For gradle signing method, passphrase must be provided'
258cdfa11b1SJan Høydahl                      ' in gradle.properties or by env.var/sysprop. See ./gradlew helpPublishing for more info')
259cdfa11b1SJan Høydahl  parser.add_argument('--gpg-home', metavar='PATH',
260cdfa11b1SJan Høydahl                      help='Path to gpg home containing your secring.gpg'
261cdfa11b1SJan Høydahl                      ' Optional, will use $HOME/.gnupg/secring.gpg by default')
26280e60002SRyan Ernst  parser.add_argument('--rc-num', metavar='NUM', type=int, default=1,
263dbb1fc68SSteve Rowe                      help='Release Candidate number.  Default: 1')
264dbb1fc68SSteve Rowe  parser.add_argument('--root', metavar='PATH', default='.',
265674b66ddSJan Høydahl                      help='Root of Git working tree for lucene.  Default: "." (the current directory)')
266df1775ffSJan Høydahl  parser.add_argument('--logfile', metavar='PATH',
267df1775ffSJan Høydahl                      help='Specify log file path (default /tmp/release.log)')
268cdfa11b1SJan Høydahl  parser.add_argument('--dev-mode', default=False, action='store_true',
269cdfa11b1SJan Høydahl                      help='Enable development mode, which disables some strict checks')
27080e60002SRyan Ernst  config = parser.parse_args()
271879e8250SMichael McCandless
27280e60002SRyan Ernst  if not config.prepare and config.sign:
27380e60002SRyan Ernst    parser.error('Cannot sign already built release')
27480e60002SRyan Ernst  if config.push_local is not None and os.path.exists(config.push_local):
27580e60002SRyan Ernst    parser.error('Cannot push to local path that already exists')
27680e60002SRyan Ernst  if config.rc_num <= 0:
27780e60002SRyan Ernst    parser.error('Release Candidate number must be a positive integer')
27880e60002SRyan Ernst  if not os.path.isdir(config.root):
2798cb2773dSSteve Rowe    parser.error('Root path "%s" is not a directory' % config.root)
280982ee393SJan Høydahl  if config.local_keys is not None and not os.path.exists(config.local_keys):
281982ee393SJan Høydahl    parser.error('Local KEYS file "%s" not found' % config.local_keys)
282cdfa11b1SJan Høydahl  if config.gpg_home and not os.path.exists(os.path.join(config.gpg_home, 'secring.gpg')):
283cdfa11b1SJan Høydahl    parser.error('Specified gpg home %s does not exist or does not contain a secring.gpg' % config.gpg_home)
284cdfa11b1SJan Høydahl  global dev_mode
285cdfa11b1SJan Høydahl  if config.dev_mode:
286cdfa11b1SJan Høydahl    print("Enabling development mode - DO NOT USE FOR ACTUAL RELEASE!")
287cdfa11b1SJan Høydahl    dev_mode = True
2888cb2773dSSteve Rowe  cwd = os.getcwd()
2898cb2773dSSteve Rowe  os.chdir(config.root)
2908cb2773dSSteve Rowe  config.root = os.getcwd() # Absolutize root dir
291674b66ddSJan Høydahl  if os.system('git rev-parse') or 2 != len([d for d in ('dev-tools','lucene') if os.path.isdir(d)]):
292674b66ddSJan Høydahl    parser.error('Root path "%s" is not a valid lucene checkout' % config.root)
2938cb2773dSSteve Rowe  os.chdir(cwd)
294df1775ffSJan Høydahl  global LOG
295df1775ffSJan Høydahl  if config.logfile:
296df1775ffSJan Høydahl    LOG = config.logfile
297cdfa11b1SJan Høydahl  print("Logfile is: %s" % LOG)
298879e8250SMichael McCandless
29980e60002SRyan Ernst  config.version = read_version(config.root)
30080e60002SRyan Ernst  print('Building version: %s' % config.version)
301879e8250SMichael McCandless
30280e60002SRyan Ernst  return config
303d6ab323dSMichael McCandless
3048cb2773dSSteve Rowedef check_cmdline_tools():  # Fail fast if there are cmdline tool problems
305532d07f1SSteve Rowe  if os.system('git --version >/dev/null 2>/dev/null'):
3068cb2773dSSteve Rowe    raise RuntimeError('"git --version" returned a non-zero exit code.')
307f2e3b109SAlan Woodward
308982ee393SJan Høydahldef check_key_in_keys(gpgKeyID, local_keys):
309982ee393SJan Høydahl  if gpgKeyID is not None:
310982ee393SJan Høydahl    print('  Verify your gpg key is in the main KEYS file')
311982ee393SJan Høydahl    if local_keys is not None:
312982ee393SJan Høydahl      print("    Using local KEYS file %s" % local_keys)
313982ee393SJan Høydahl      keysFileText = open(local_keys, encoding='iso-8859-1').read()
314982ee393SJan Høydahl      keysFileLocation = local_keys
315982ee393SJan Høydahl    else:
316982ee393SJan Høydahl      keysFileURL = "https://archive.apache.org/dist/lucene/KEYS"
317982ee393SJan Høydahl      keysFileLocation = keysFileURL
318982ee393SJan Høydahl      print("    Using online KEYS file %s" % keysFileURL)
319982ee393SJan Høydahl      keysFileText = load(keysFileURL, encoding='iso-8859-1')
320982ee393SJan Høydahl    if len(gpgKeyID) > 2 and gpgKeyID[0:2] == '0x':
321982ee393SJan Høydahl      gpgKeyID = gpgKeyID[2:]
322982ee393SJan Høydahl    if len(gpgKeyID) > 40:
323982ee393SJan Høydahl      gpgKeyID = gpgKeyID.replace(" ", "")
324982ee393SJan Høydahl    if len(gpgKeyID) == 8:
3255b96f89dSJan Høydahl      gpgKeyID8Char = "%s %s" % (gpgKeyID[0:4], gpgKeyID[4:8])
326daf14981SAlan Woodward      re_to_match = r"^pub .*\n\s+(\w{4} \w{4} \w{4} \w{4} \w{4}  \w{4} \w{4} \w{4} %s|\w{32}%s)" % (gpgKeyID8Char, gpgKeyID)
327982ee393SJan Høydahl    elif len(gpgKeyID) == 40:
328982ee393SJan Høydahl      gpgKeyID40Char = "%s %s %s %s %s  %s %s %s %s %s" % \
329982ee393SJan Høydahl                       (gpgKeyID[0:4], gpgKeyID[4:8], gpgKeyID[8:12], gpgKeyID[12:16], gpgKeyID[16:20],
330982ee393SJan Høydahl                       gpgKeyID[20:24], gpgKeyID[24:28], gpgKeyID[28:32], gpgKeyID[32:36], gpgKeyID[36:])
33155c06177SAlan Woodward      re_to_match = r"^pub .*\n\s+(%s|%s)" % (gpgKeyID40Char, gpgKeyID)
332982ee393SJan Høydahl    else:
33326a32d79SAlan Woodward      print('Invalid gpg key id format [%s]. Must be 8 byte short ID or 40 byte fingerprint, with or without 0x prefix, no spaces.' % gpgKeyID)
334982ee393SJan Høydahl      exit(2)
335982ee393SJan Høydahl    if re.search(re_to_match, keysFileText, re.MULTILINE):
336982ee393SJan Høydahl      print('    Found key %s in KEYS file at %s' % (gpgKeyID, keysFileLocation))
337982ee393SJan Høydahl    else:
338982ee393SJan Høydahl      print('    ERROR: Did not find your key %s in KEYS file at %s. Please add it and try again.' % (gpgKeyID, keysFileLocation))
339982ee393SJan Høydahl      if local_keys is not None:
340982ee393SJan Høydahl        print('           You are using a local KEYS file. Make sure it is up to date or validate against the online version')
341982ee393SJan Høydahl      exit(2)
342982ee393SJan Høydahl
343982ee393SJan Høydahl
344cdfa11b1SJan Høydahldef resolve_gpghome():
345cdfa11b1SJan Høydahl  for p in [
346cdfa11b1SJan Høydahl    # Linux, macos
347cdfa11b1SJan Høydahl    os.path.join(os.path.expanduser("~"), '.gnupg'),
348cdfa11b1SJan Høydahl    # Windows 10
349cdfa11b1SJan Høydahl    os.path.expandvars(r'%APPDATA%\GnuPG')
350cdfa11b1SJan Høydahl    # TODO: Should we support Cygwin?
351cdfa11b1SJan Høydahl  ]:
352cdfa11b1SJan Høydahl    if os.path.exists(os.path.join(p, 'secring.gpg')):
353cdfa11b1SJan Høydahl      return p
354cdfa11b1SJan Høydahl  return None
355cdfa11b1SJan Høydahl
356cdfa11b1SJan Høydahl
35780e60002SRyan Ernstdef main():
3588cb2773dSSteve Rowe  check_cmdline_tools()
3598cb2773dSSteve Rowe
36080e60002SRyan Ernst  c = parse_config()
361cdfa11b1SJan Høydahl  gpg_home = None
36280e60002SRyan Ernst
3635b96f89dSJan Høydahl  if c.sign:
3645b96f89dSJan Høydahl    sys.stdout.flush()
3655b96f89dSJan Høydahl    c.key_id = c.sign
366982ee393SJan Høydahl    check_key_in_keys(c.key_id, c.local_keys)
367cdfa11b1SJan Høydahl    if c.gpg_home is not None:
368cdfa11b1SJan Høydahl      print("Using custom gpg-home: %s" % c.gpg_home)
369cdfa11b1SJan Høydahl      gpg_home = c.gpg_home
370cdfa11b1SJan Høydahl    if c.sign_method_gradle:
371cdfa11b1SJan Høydahl      if gpg_home is None:
372cdfa11b1SJan Høydahl        resolved_gpg_home = resolve_gpghome()
373cdfa11b1SJan Høydahl        if resolved_gpg_home is not None:
374cdfa11b1SJan Høydahl          print("Resolved gpg home to %s" % resolved_gpg_home)
375cdfa11b1SJan Høydahl          gpg_home = resolved_gpg_home
376cdfa11b1SJan Høydahl        else:
377cdfa11b1SJan Høydahl          print("WARN: Could not locate your gpg secret keyring, and --gpg-home not specified.")
378cdfa11b1SJan Høydahl          print("      Falling back to location configured in gradle.properties.")
379cdfa11b1SJan Høydahl          print("      See 'gradlew helpPublishing' for details.")
380cdfa11b1SJan Høydahl          gpg_home = None
381cdfa11b1SJan Høydahl    if c.gpg_pass_noprompt:
382cdfa11b1SJan Høydahl      print("Will not prompt for gpg password. Make sure your signing setup supports this.")
383cdfa11b1SJan Høydahl      c.key_password = None
384cdfa11b1SJan Høydahl    else:
3855b96f89dSJan Høydahl      import getpass
3865b96f89dSJan Høydahl      c.key_password = getpass.getpass('Enter GPG keystore password: ')
3875b96f89dSJan Høydahl  else:
3885b96f89dSJan Høydahl    c.key_id = None
3895b96f89dSJan Høydahl    c.key_password = None
390982ee393SJan Høydahl
39180e60002SRyan Ernst  if c.prepare:
3920544819bSDawid Weiss    prepare(c.root, c.version, c.key_id, c.key_password, gpg_home=gpg_home, sign_gradle=c.sign_method_gradle)
393d6ab323dSMichael McCandless  else:
394b8e50f38SShalin Shekhar Mangar    os.chdir(c.root)
395d6ab323dSMichael McCandless
3968cb2773dSSteve Rowe  if c.push_local:
3970544819bSDawid Weiss    url = pushLocal(c.version, c.root, c.rc_num, c.push_local)
398879e8250SMichael McCandless  else:
3990178e8e6SSteven Rowe    url = None
400879e8250SMichael McCandless
401879e8250SMichael McCandless  if url is not None:
4020178e8e6SSteven Rowe    print('  URL: %s' % url)
4038cb2773dSSteve Rowe    print('Next run the smoker tester:')
4048cb2773dSSteve Rowe    p = re.compile(".*/")
40522a358ffSAnshum Gupta    m = p.match(sys.argv[0])
406ae956db4SJan Høydahl    if not c.sign:
407ae956db4SJan Høydahl      signed = "--not-signed"
408ae956db4SJan Høydahl    else:
409ae956db4SJan Høydahl      signed = ""
410ae956db4SJan Høydahl    print('%s -u %ssmokeTestRelease.py %s %s' % (sys.executable, m.group(), signed, url))
411d6ab323dSMichael McCandless
412d6ab323dSMichael McCandlessif __name__ == '__main__':
4130178e8e6SSteven Rowe  try:
414d6ab323dSMichael McCandless    main()
41580e60002SRyan Ernst  except KeyboardInterrupt:
41680e60002SRyan Ernst    print('Keyboard interrupt...exiting')
41780e60002SRyan Ernst
418