1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# Licensed to the Apache Software Foundation (ASF) under one or more 4# contributor license agreements. See the NOTICE file distributed with 5# this work for additional information regarding copyright ownership. 6# The ASF licenses this file to You under the Apache License, Version 2.0 7# (the "License"); you may not use this file except in compliance with 8# the License. You may obtain a copy of the License at 9# 10# http://www.apache.org/licenses/LICENSE-2.0 11# 12# Unless required by applicable law or agreed to in writing, software 13# distributed under the License is distributed on an "AS IS" BASIS, 14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15# See the License for the specific language governing permissions and 16# limitations under the License. 17 18import argparse 19import codecs 20import datetime 21import filecmp 22import glob 23import hashlib 24import http.client 25import os 26import platform 27import re 28import shutil 29import subprocess 30import sys 31import textwrap 32import traceback 33import urllib.error 34import urllib.parse 35import urllib.parse 36import urllib.request 37import xml.etree.ElementTree as ET 38import zipfile 39from collections import namedtuple 40import scriptutil 41 42# This tool expects to find /lucene off the base URL. You 43# must have a working gpg, tar, unzip in your path. This has been 44# tested on Linux and on Cygwin under Windows 7. 45 46cygwin = platform.system().lower().startswith('cygwin') 47cygwinWindowsRoot = os.popen('cygpath -w /').read().strip().replace('\\','/') if cygwin else '' 48 49 50def unshortenURL(url): 51 parsed = urllib.parse.urlparse(url) 52 if parsed[0] in ('http', 'https'): 53 h = http.client.HTTPConnection(parsed.netloc) 54 h.request('HEAD', parsed.path) 55 response = h.getresponse() 56 if int(response.status/100) == 3 and response.getheader('Location'): 57 return response.getheader('Location') 58 return url 59 60# TODO 61# - make sure jars exist inside bin release 62# - make sure docs exist 63 64reHREF = re.compile('<a href="(.*?)">(.*?)</a>') 65 66# Set to False to avoid re-downloading the packages... 67FORCE_CLEAN = True 68 69 70def getHREFs(urlString): 71 72 # Deref any redirects 73 while True: 74 url = urllib.parse.urlparse(urlString) 75 if url.scheme == "http": 76 h = http.client.HTTPConnection(url.netloc) 77 elif url.scheme == "https": 78 h = http.client.HTTPSConnection(url.netloc) 79 else: 80 raise RuntimeError("Unknown protocol: %s" % url.scheme) 81 h.request('HEAD', url.path) 82 r = h.getresponse() 83 newLoc = r.getheader('location') 84 if newLoc is not None: 85 urlString = newLoc 86 else: 87 break 88 89 links = [] 90 try: 91 html = load(urlString) 92 except: 93 print('\nFAILED to open url %s' % urlString) 94 traceback.print_exc() 95 raise 96 97 for subUrl, text in reHREF.findall(html): 98 fullURL = urllib.parse.urljoin(urlString, subUrl) 99 links.append((text, fullURL)) 100 return links 101 102 103def load(urlString): 104 try: 105 content = urllib.request.urlopen(urlString).read().decode('utf-8') 106 except Exception as e: 107 print('Retrying download of url %s after exception: %s' % (urlString, e)) 108 content = urllib.request.urlopen(urlString).read().decode('utf-8') 109 return content 110 111 112def noJavaPackageClasses(desc, file): 113 with zipfile.ZipFile(file) as z2: 114 for name2 in z2.namelist(): 115 if name2.endswith('.class') and (name2.startswith('java/') or name2.startswith('javax/')): 116 raise RuntimeError('%s contains sheisty class "%s"' % (desc, name2)) 117 118 119def decodeUTF8(bytes): 120 return codecs.getdecoder('UTF-8')(bytes)[0] 121 122 123MANIFEST_FILE_NAME = 'META-INF/MANIFEST.MF' 124NOTICE_FILE_NAME = 'META-INF/NOTICE.txt' 125LICENSE_FILE_NAME = 'META-INF/LICENSE.txt' 126 127 128def checkJARMetaData(desc, jarFile, gitRevision, version): 129 130 with zipfile.ZipFile(jarFile, 'r') as z: 131 for name in (MANIFEST_FILE_NAME, NOTICE_FILE_NAME, LICENSE_FILE_NAME): 132 try: 133 # The Python docs state a KeyError is raised ... so this None 134 # check is just defensive: 135 if z.getinfo(name) is None: 136 raise RuntimeError('%s is missing %s' % (desc, name)) 137 except KeyError: 138 raise RuntimeError('%s is missing %s' % (desc, name)) 139 140 s = decodeUTF8(z.read(MANIFEST_FILE_NAME)) 141 142 for verify in ( 143 'Specification-Vendor: The Apache Software Foundation', 144 'Implementation-Vendor: The Apache Software Foundation', 145 'Specification-Title: Lucene Search Engine:', 146 'Implementation-Title: org.apache.lucene', 147 'X-Compile-Source-JDK: 17', 148 'X-Compile-Target-JDK: 17', 149 'Specification-Version: %s' % version, 150 'X-Build-JDK: 17.', 151 'Extension-Name: org.apache.lucene'): 152 if type(verify) is not tuple: 153 verify = (verify,) 154 for x in verify: 155 if s.find(x) != -1: 156 break 157 else: 158 if len(verify) == 1: 159 raise RuntimeError('%s is missing "%s" inside its META-INF/MANIFEST.MF' % (desc, verify[0])) 160 else: 161 raise RuntimeError('%s is missing one of "%s" inside its META-INF/MANIFEST.MF' % (desc, verify)) 162 163 if gitRevision != 'skip': 164 # Make sure this matches the version and git revision we think we are releasing: 165 match = re.search("Implementation-Version: (.+\r\n .+)", s, re.MULTILINE) 166 if match: 167 implLine = match.group(1).replace("\r\n ", "") 168 verifyRevision = '%s %s' % (version, gitRevision) 169 if implLine.find(verifyRevision) == -1: 170 raise RuntimeError('%s is missing "%s" inside its META-INF/MANIFEST.MF (wrong git revision?)' % \ 171 (desc, verifyRevision)) 172 else: 173 raise RuntimeError('%s is missing Implementation-Version inside its META-INF/MANIFEST.MF' % desc) 174 175 notice = decodeUTF8(z.read(NOTICE_FILE_NAME)) 176 lucene_license = decodeUTF8(z.read(LICENSE_FILE_NAME)) 177 178 if LUCENE_LICENSE is None: 179 raise RuntimeError('BUG in smokeTestRelease!') 180 if LUCENE_NOTICE is None: 181 raise RuntimeError('BUG in smokeTestRelease!') 182 if notice != LUCENE_NOTICE: 183 raise RuntimeError('%s: %s contents doesn\'t match main NOTICE.txt' % \ 184 (desc, NOTICE_FILE_NAME)) 185 if lucene_license != LUCENE_LICENSE: 186 raise RuntimeError('%s: %s contents doesn\'t match main LICENSE.txt' % \ 187 (desc, LICENSE_FILE_NAME)) 188 189 190def normSlashes(path): 191 return path.replace(os.sep, '/') 192 193 194def checkAllJARs(topDir, gitRevision, version): 195 print(' verify JAR metadata/identity/no javax.* or java.* classes...') 196 for root, dirs, files in os.walk(topDir): 197 198 normRoot = normSlashes(root) 199 200 for file in files: 201 if file.lower().endswith('.jar'): 202 if normRoot.endswith('/replicator/lib') and file.startswith('javax.servlet'): 203 continue 204 fullPath = '%s/%s' % (root, file) 205 noJavaPackageClasses('JAR file "%s"' % fullPath, fullPath) 206 if file.lower().find('lucene') != -1: 207 checkJARMetaData('JAR file "%s"' % fullPath, fullPath, gitRevision, version) 208 209 210def checkSigs(urlString, version, tmpDir, isSigned, keysFile): 211 print(' test basics...') 212 ents = getDirEntries(urlString) 213 artifact = None 214 changesURL = None 215 mavenURL = None 216 artifactURL = None 217 expectedSigs = [] 218 if isSigned: 219 expectedSigs.append('asc') 220 expectedSigs.extend(['sha512']) 221 sigs = [] 222 artifacts = [] 223 224 for text, subURL in ents: 225 if text == 'KEYS': 226 raise RuntimeError('lucene: release dir should not contain a KEYS file - only toplevel /dist/lucene/KEYS is used') 227 elif text == 'maven/': 228 mavenURL = subURL 229 elif text.startswith('changes'): 230 if text not in ('changes/', 'changes-%s/' % version): 231 raise RuntimeError('lucene: found %s vs expected changes-%s/' % (text, version)) 232 changesURL = subURL 233 elif artifact is None: 234 artifact = text 235 artifactURL = subURL 236 expected = 'lucene-%s' % version 237 if not artifact.startswith(expected): 238 raise RuntimeError('lucene: unknown artifact %s: expected prefix %s' % (text, expected)) 239 sigs = [] 240 elif text.startswith(artifact + '.'): 241 sigs.append(text[len(artifact)+1:]) 242 else: 243 if sigs != expectedSigs: 244 raise RuntimeError('lucene: artifact %s has wrong sigs: expected %s but got %s' % (artifact, expectedSigs, sigs)) 245 artifacts.append((artifact, artifactURL)) 246 artifact = text 247 artifactURL = subURL 248 sigs = [] 249 250 if sigs != []: 251 artifacts.append((artifact, artifactURL)) 252 if sigs != expectedSigs: 253 raise RuntimeError('lucene: artifact %s has wrong sigs: expected %s but got %s' % (artifact, expectedSigs, sigs)) 254 255 expected = ['lucene-%s-src.tgz' % version, 256 'lucene-%s.tgz' % version] 257 258 actual = [x[0] for x in artifacts] 259 if expected != actual: 260 raise RuntimeError('lucene: wrong artifacts: expected %s but got %s' % (expected, actual)) 261 262 # Set up clean gpg world; import keys file: 263 gpgHomeDir = '%s/lucene.gpg' % tmpDir 264 if os.path.exists(gpgHomeDir): 265 shutil.rmtree(gpgHomeDir) 266 os.makedirs(gpgHomeDir, 0o700) 267 run('gpg --homedir %s --import %s' % (gpgHomeDir, keysFile), 268 '%s/lucene.gpg.import.log' % tmpDir) 269 270 if mavenURL is None: 271 raise RuntimeError('lucene is missing maven') 272 273 if changesURL is None: 274 raise RuntimeError('lucene is missing changes-%s' % version) 275 testChanges(version, changesURL) 276 277 for artifact, urlString in artifacts: 278 print(' download %s...' % artifact) 279 scriptutil.download(artifact, urlString, tmpDir, force_clean=FORCE_CLEAN) 280 verifyDigests(artifact, urlString, tmpDir) 281 282 if isSigned: 283 print(' verify sig') 284 # Test sig (this is done with a clean brand-new GPG world) 285 scriptutil.download(artifact + '.asc', urlString + '.asc', tmpDir, force_clean=FORCE_CLEAN) 286 sigFile = '%s/%s.asc' % (tmpDir, artifact) 287 artifactFile = '%s/%s' % (tmpDir, artifact) 288 logFile = '%s/lucene.%s.gpg.verify.log' % (tmpDir, artifact) 289 run('gpg --homedir %s --display-charset utf-8 --verify %s %s' % (gpgHomeDir, sigFile, artifactFile), 290 logFile) 291 # Forward any GPG warnings, except the expected one (since it's a clean world) 292 with open(logFile) as f: 293 print("File: %s" % logFile) 294 for line in f.readlines(): 295 if line.lower().find('warning') != -1 \ 296 and line.find('WARNING: This key is not certified with a trusted signature') == -1: 297 print(' GPG: %s' % line.strip()) 298 299 # Test trust (this is done with the real users config) 300 run('gpg --import %s' % (keysFile), 301 '%s/lucene.gpg.trust.import.log' % tmpDir) 302 print(' verify trust') 303 logFile = '%s/lucene.%s.gpg.trust.log' % (tmpDir, artifact) 304 run('gpg --display-charset utf-8 --verify %s %s' % (sigFile, artifactFile), logFile) 305 # Forward any GPG warnings: 306 with open(logFile) as f: 307 for line in f.readlines(): 308 if line.lower().find('warning') != -1: 309 print(' GPG: %s' % line.strip()) 310 311 312def testChanges(version, changesURLString): 313 print(' check changes HTML...') 314 changesURL = None 315 for text, subURL in getDirEntries(changesURLString): 316 if text == 'Changes.html': 317 changesURL = subURL 318 319 if changesURL is None: 320 raise RuntimeError('did not see Changes.html link from %s' % changesURLString) 321 322 s = load(changesURL) 323 checkChangesContent(s, version, changesURL, True) 324 325 326def testChangesText(dir, version): 327 "Checks all CHANGES.txt under this dir." 328 for root, dirs, files in os.walk(dir): 329 330 # NOTE: O(N) but N should be smallish: 331 if 'CHANGES.txt' in files: 332 fullPath = '%s/CHANGES.txt' % root 333 #print 'CHECK %s' % fullPath 334 checkChangesContent(open(fullPath, encoding='UTF-8').read(), version, fullPath, False) 335 336reChangesSectionHREF = re.compile('<a id="(.*?)".*?>(.*?)</a>', re.IGNORECASE) 337reUnderbarNotDashHTML = re.compile(r'<li>(\s*(LUCENE)_\d\d\d\d+)') 338reUnderbarNotDashTXT = re.compile(r'\s+((LUCENE)_\d\d\d\d+)', re.MULTILINE) 339 340 341def checkChangesContent(s, version, name, isHTML): 342 currentVersionTuple = versionToTuple(version, name) 343 344 if isHTML and s.find('Release %s' % version) == -1: 345 raise RuntimeError('did not see "Release %s" in %s' % (version, name)) 346 347 if isHTML: 348 r = reUnderbarNotDashHTML 349 else: 350 r = reUnderbarNotDashTXT 351 352 m = r.search(s) 353 if m is not None: 354 raise RuntimeError('incorrect issue (_ instead of -) in %s: %s' % (name, m.group(1))) 355 356 if s.lower().find('not yet released') != -1: 357 raise RuntimeError('saw "not yet released" in %s' % name) 358 359 if not isHTML: 360 sub = 'Lucene %s' % version 361 if s.find(sub) == -1: 362 # benchmark never seems to include release info: 363 if name.find('/benchmark/') == -1: 364 raise RuntimeError('did not see "%s" in %s' % (sub, name)) 365 366 if isHTML: 367 # Make sure that a section only appears once under each release, 368 # and that each release is not greater than the current version 369 seenIDs = set() 370 seenText = set() 371 372 release = None 373 for id, text in reChangesSectionHREF.findall(s): 374 if text.lower().startswith('release '): 375 release = text[8:].strip() 376 seenText.clear() 377 releaseTuple = versionToTuple(release, name) 378 if releaseTuple > currentVersionTuple: 379 raise RuntimeError('Future release %s is greater than %s in %s' % (release, version, name)) 380 if id in seenIDs: 381 raise RuntimeError('%s has duplicate section "%s" under release "%s"' % (name, text, release)) 382 seenIDs.add(id) 383 if text in seenText: 384 raise RuntimeError('%s has duplicate section "%s" under release "%s"' % (name, text, release)) 385 seenText.add(text) 386 387 388reVersion = re.compile(r'(\d+)\.(\d+)(?:\.(\d+))?\s*(-alpha|-beta|final|RC\d+)?\s*(?:\[.*\])?', re.IGNORECASE) 389 390 391def versionToTuple(version, name): 392 versionMatch = reVersion.match(version) 393 if versionMatch is None: 394 raise RuntimeError('Version %s in %s cannot be parsed' % (version, name)) 395 versionTuple = versionMatch.groups() 396 while versionTuple[-1] is None or versionTuple[-1] == '': 397 versionTuple = versionTuple[:-1] 398 if versionTuple[-1].lower() == '-alpha': 399 versionTuple = versionTuple[:-1] + ('0',) 400 elif versionTuple[-1].lower() == '-beta': 401 versionTuple = versionTuple[:-1] + ('1',) 402 elif versionTuple[-1].lower() == 'final': 403 versionTuple = versionTuple[:-2] + ('100',) 404 elif versionTuple[-1].lower()[:2] == 'rc': 405 versionTuple = versionTuple[:-2] + (versionTuple[-1][2:],) 406 return tuple(int(x) if x is not None and x.isnumeric() else x for x in versionTuple) 407 408 409reUnixPath = re.compile(r'\b[a-zA-Z_]+=(?:"(?:\\"|[^"])*"' + '|(?:\\\\.|[^"\'\\s])*' + r"|'(?:\\'|[^'])*')" \ 410 + r'|(/(?:\\.|[^"\'\s])*)' \ 411 + r'|("/(?:\\.|[^"])*")' \ 412 + r"|('/(?:\\.|[^'])*')") 413 414 415def unix2win(matchobj): 416 if matchobj.group(1) is not None: return cygwinWindowsRoot + matchobj.group() 417 if matchobj.group(2) is not None: return '"%s%s' % (cygwinWindowsRoot, matchobj.group().lstrip('"')) 418 if matchobj.group(3) is not None: return "'%s%s" % (cygwinWindowsRoot, matchobj.group().lstrip("'")) 419 return matchobj.group() 420 421 422def cygwinifyPaths(command): 423 # The problem: Native Windows applications running under Cygwin can't 424 # handle Cygwin's Unix-style paths. However, environment variable 425 # values are automatically converted, so only paths outside of 426 # environment variable values should be converted to Windows paths. 427 # Assumption: all paths will be absolute. 428 if '; gradlew ' in command: command = reUnixPath.sub(unix2win, command) 429 return command 430 431 432def printFileContents(fileName): 433 434 # Assume log file was written in system's default encoding, but 435 # even if we are wrong, we replace errors ... the ASCII chars 436 # (which is what we mostly care about eg for the test seed) should 437 # still survive: 438 txt = codecs.open(fileName, 'r', encoding=sys.getdefaultencoding(), errors='replace').read() 439 440 # Encode to our output encoding (likely also system's default 441 # encoding): 442 bytes = txt.encode(sys.stdout.encoding, errors='replace') 443 444 # Decode back to string and print... we should hit no exception here 445 # since all errors have been replaced: 446 print(codecs.getdecoder(sys.stdout.encoding)(bytes)[0]) 447 print() 448 449 450def run(command, logFile): 451 if cygwin: command = cygwinifyPaths(command) 452 if os.system('%s > %s 2>&1' % (command, logFile)): 453 logPath = os.path.abspath(logFile) 454 print('\ncommand "%s" failed:' % command) 455 printFileContents(logFile) 456 raise RuntimeError('command "%s" failed; see log file %s' % (command, logPath)) 457 458 459def verifyDigests(artifact, urlString, tmpDir): 460 print(' verify sha512 digest') 461 sha512Expected, t = load(urlString + '.sha512').strip().split() 462 if t != '*'+artifact: 463 raise RuntimeError('SHA512 %s.sha512 lists artifact %s but expected *%s' % (urlString, t, artifact)) 464 465 s512 = hashlib.sha512() 466 f = open('%s/%s' % (tmpDir, artifact), 'rb') 467 while True: 468 x = f.read(65536) 469 if len(x) == 0: 470 break 471 s512.update(x) 472 f.close() 473 sha512Actual = s512.hexdigest() 474 if sha512Actual != sha512Expected: 475 raise RuntimeError('SHA512 digest mismatch for %s: expected %s but got %s' % (artifact, sha512Expected, sha512Actual)) 476 477 478def getDirEntries(urlString): 479 if urlString.startswith('file:/') and not urlString.startswith('file://'): 480 # stupid bogus ant URI 481 urlString = "file:///" + urlString[6:] 482 483 if urlString.startswith('file://'): 484 path = urlString[7:] 485 if path.endswith('/'): 486 path = path[:-1] 487 if cygwin: # Convert Windows path to Cygwin path 488 path = re.sub(r'^/([A-Za-z]):/', r'/cygdrive/\1/', path) 489 l = [] 490 for ent in os.listdir(path): 491 entPath = '%s/%s' % (path, ent) 492 if os.path.isdir(entPath): 493 entPath += '/' 494 ent += '/' 495 l.append((ent, 'file://%s' % entPath)) 496 l.sort() 497 return l 498 else: 499 links = getHREFs(urlString) 500 for i, (text, subURL) in enumerate(links): 501 if text == 'Parent Directory' or text == '..': 502 return links[(i+1):] 503 504 505def unpackAndVerify(java, tmpDir, artifact, gitRevision, version, testArgs): 506 destDir = '%s/unpack' % tmpDir 507 if os.path.exists(destDir): 508 shutil.rmtree(destDir) 509 os.makedirs(destDir) 510 os.chdir(destDir) 511 print(' unpack %s...' % artifact) 512 unpackLogFile = '%s/lucene-unpack-%s.log' % (tmpDir, artifact) 513 if artifact.endswith('.tar.gz') or artifact.endswith('.tgz'): 514 run('tar xzf %s/%s' % (tmpDir, artifact), unpackLogFile) 515 elif artifact.endswith('.zip'): 516 run('unzip %s/%s' % (tmpDir, artifact), unpackLogFile) 517 518 # make sure it unpacks to proper subdir 519 l = os.listdir(destDir) 520 expected = 'lucene-%s' % version 521 if l != [expected]: 522 raise RuntimeError('unpack produced entries %s; expected only %s' % (l, expected)) 523 524 unpackPath = '%s/%s' % (destDir, expected) 525 verifyUnpacked(java, artifact, unpackPath, gitRevision, version, testArgs) 526 return unpackPath 527 528LUCENE_NOTICE = None 529LUCENE_LICENSE = None 530 531 532def is_in_list(in_folder, files, indent=4): 533 for fileName in files: 534 print("%sChecking %s" % (" "*indent, fileName)) 535 found = False 536 for f in [fileName, fileName + '.txt', fileName + '.md']: 537 if f in in_folder: 538 in_folder.remove(f) 539 found = True 540 if not found: 541 raise RuntimeError('file "%s" is missing' % fileName) 542 543 544def verifyUnpacked(java, artifact, unpackPath, gitRevision, version, testArgs): 545 global LUCENE_NOTICE 546 global LUCENE_LICENSE 547 548 os.chdir(unpackPath) 549 isSrc = artifact.find('-src') != -1 550 551 # Check text files in release 552 print(" %s" % artifact) 553 in_root_folder = list(filter(lambda x: x[0] != '.', os.listdir(unpackPath))) 554 in_lucene_folder = [] 555 if isSrc: 556 in_lucene_folder.extend(os.listdir(os.path.join(unpackPath, 'lucene'))) 557 is_in_list(in_root_folder, ['LICENSE', 'NOTICE', 'README']) 558 is_in_list(in_lucene_folder, ['JRE_VERSION_MIGRATION', 'CHANGES', 'MIGRATE', 'SYSTEM_REQUIREMENTS']) 559 else: 560 is_in_list(in_root_folder, ['LICENSE', 'NOTICE', 'README', 'JRE_VERSION_MIGRATION', 'CHANGES', 561 'MIGRATE', 'SYSTEM_REQUIREMENTS']) 562 563 if LUCENE_NOTICE is None: 564 LUCENE_NOTICE = open('%s/NOTICE.txt' % unpackPath, encoding='UTF-8').read() 565 if LUCENE_LICENSE is None: 566 LUCENE_LICENSE = open('%s/LICENSE.txt' % unpackPath, encoding='UTF-8').read() 567 568 # if not isSrc: 569 # # TODO: we should add verifyModule/verifySubmodule (e.g. analysis) here and recurse through 570 # expectedJARs = () 571 # 572 # for fileName in expectedJARs: 573 # fileName += '.jar' 574 # if fileName not in l: 575 # raise RuntimeError('lucene: file "%s" is missing from artifact %s' % (fileName, artifact)) 576 # in_root_folder.remove(fileName) 577 578 expected_folders = ['analysis', 'analysis.tests', 'backward-codecs', 'benchmark', 'classification', 'codecs', 'core', 'core.tests', 579 'distribution.tests', 'demo', 'expressions', 'facet', 'grouping', 'highlighter', 'join', 580 'luke', 'memory', 'misc', 'monitor', 'queries', 'queryparser', 'replicator', 581 'sandbox', 'spatial-extras', 'spatial-test-fixtures', 'spatial3d', 'suggest', 'test-framework', 'licenses'] 582 if isSrc: 583 expected_src_root_files = ['build.gradle', 'buildSrc', 'CONTRIBUTING.md', 'dev-docs', 'dev-tools', 'gradle', 'gradlew', 584 'gradlew.bat', 'help', 'lucene', 'settings.gradle', 'versions.lock', 'versions.props'] 585 expected_src_lucene_files = ['build.gradle', 'documentation', 'distribution', 'dev-docs'] 586 is_in_list(in_root_folder, expected_src_root_files) 587 is_in_list(in_lucene_folder, expected_folders) 588 is_in_list(in_lucene_folder, expected_src_lucene_files) 589 if len(in_lucene_folder) > 0: 590 raise RuntimeError('lucene: unexpected files/dirs in artifact %s lucene/ folder: %s' % (artifact, in_lucene_folder)) 591 else: 592 is_in_list(in_root_folder, ['bin', 'docs', 'licenses', 'modules', 'modules-thirdparty', 'modules-test-framework']) 593 594 if len(in_root_folder) > 0: 595 raise RuntimeError('lucene: unexpected files/dirs in artifact %s: %s' % (artifact, in_root_folder)) 596 597 if isSrc: 598 print(' make sure no JARs/WARs in src dist...') 599 lines = os.popen('find . -name \\*.jar').readlines() 600 if len(lines) != 0: 601 print(' FAILED:') 602 for line in lines: 603 print(' %s' % line.strip()) 604 raise RuntimeError('source release has JARs...') 605 lines = os.popen('find . -name \\*.war').readlines() 606 if len(lines) != 0: 607 print(' FAILED:') 608 for line in lines: 609 print(' %s' % line.strip()) 610 raise RuntimeError('source release has WARs...') 611 612 validateCmd = './gradlew --no-daemon check -p lucene/documentation' 613 print(' run "%s"' % validateCmd) 614 java.run_java17(validateCmd, '%s/validate.log' % unpackPath) 615 616 print(" run tests w/ Java 17 and testArgs='%s'..." % testArgs) 617 java.run_java17('./gradlew --no-daemon test %s' % testArgs, '%s/test.log' % unpackPath) 618 print(" compile jars w/ Java 17") 619 java.run_java17('./gradlew --no-daemon jar -Dversion.release=%s' % version, '%s/compile.log' % unpackPath) 620 testDemo(java.run_java17, isSrc, version, '17') 621 622 if java.run_java18: 623 print(" run tests w/ Java 18 and testArgs='%s'..." % testArgs) 624 java.run_java18('./gradlew --no-daemon test %s' % testArgs, '%s/test.log' % unpackPath) 625 print(" compile jars w/ Java 18") 626 java.run_java18('./gradlew --no-daemon jar -Dversion.release=%s' % version, '%s/compile.log' % unpackPath) 627 testDemo(java.run_java18, isSrc, version, '18') 628 629 print(' confirm all releases have coverage in TestBackwardsCompatibility') 630 confirmAllReleasesAreTestedForBackCompat(version, unpackPath) 631 632 else: 633 634 checkAllJARs(os.getcwd(), gitRevision, version) 635 636 testDemo(java.run_java17, isSrc, version, '17') 637 if java.run_java18: 638 testDemo(java.run_java18, isSrc, version, '18') 639 640 testChangesText('.', version) 641 642 643def testDemo(run_java, isSrc, version, jdk): 644 if os.path.exists('index'): 645 shutil.rmtree('index') # nuke any index from any previous iteration 646 647 print(' test demo with %s...' % jdk) 648 sep = ';' if cygwin else ':' 649 if isSrc: 650 # For source release, use the classpath for each module. 651 classPath = ['lucene/core/build/libs/lucene-core-%s.jar' % version, 652 'lucene/demo/build/libs/lucene-demo-%s.jar' % version, 653 'lucene/analysis/common/build/libs/lucene-analyzers-common-%s.jar' % version, 654 'lucene/queryparser/build/libs/lucene-queryparser-%s.jar' % version] 655 cp = sep.join(classPath) 656 docsDir = 'lucene/core/src' 657 checkIndexCmd = 'java -ea -cp "%s" org.apache.lucene.index.CheckIndex index' % cp 658 indexFilesCmd = 'java -cp "%s" -Dsmoketester=true org.apache.lucene.demo.IndexFiles -index index -docs %s' % (cp, docsDir) 659 searchFilesCmd = 'java -cp "%s" org.apache.lucene.demo.SearchFiles -index index -query lucene' % cp 660 else: 661 # For binary release, set up module path. 662 cp = "--module-path %s" % (sep.join(["modules", "modules-thirdparty"])) 663 docsDir = 'docs' 664 checkIndexCmd = 'java -ea %s --module org.apache.lucene.core/org.apache.lucene.index.CheckIndex index' % cp 665 indexFilesCmd = 'java -Dsmoketester=true %s --module org.apache.lucene.demo/org.apache.lucene.demo.IndexFiles -index index -docs %s' % (cp, docsDir) 666 searchFilesCmd = 'java %s --module org.apache.lucene.demo/org.apache.lucene.demo.SearchFiles -index index -query lucene' % cp 667 668 run_java(indexFilesCmd, 'index.log') 669 run_java(searchFilesCmd, 'search.log') 670 reMatchingDocs = re.compile('(\d+) total matching documents') 671 m = reMatchingDocs.search(open('search.log', encoding='UTF-8').read()) 672 if m is None: 673 raise RuntimeError('lucene demo\'s SearchFiles found no results') 674 else: 675 numHits = int(m.group(1)) 676 if numHits < 100: 677 raise RuntimeError('lucene demo\'s SearchFiles found too few results: %s' % numHits) 678 print(' got %d hits for query "lucene"' % numHits) 679 680 print(' checkindex with %s...' % jdk) 681 run_java(checkIndexCmd, 'checkindex.log') 682 s = open('checkindex.log').read() 683 m = re.search(r'^\s+version=(.*?)$', s, re.MULTILINE) 684 if m is None: 685 raise RuntimeError('unable to locate version=NNN output from CheckIndex; see checkindex.log') 686 actualVersion = m.group(1) 687 if removeTrailingZeros(actualVersion) != removeTrailingZeros(version): 688 raise RuntimeError('wrong version from CheckIndex: got "%s" but expected "%s"' % (actualVersion, version)) 689 690 691def removeTrailingZeros(version): 692 return re.sub(r'(\.0)*$', '', version) 693 694 695def checkMaven(baseURL, tmpDir, gitRevision, version, isSigned, keysFile): 696 print(' download artifacts') 697 artifacts = [] 698 artifactsURL = '%s/lucene/maven/org/apache/lucene/' % baseURL 699 targetDir = '%s/maven/org/apache/lucene' % tmpDir 700 if not os.path.exists(targetDir): 701 os.makedirs(targetDir) 702 crawl(artifacts, artifactsURL, targetDir) 703 print() 704 verifyPOMperBinaryArtifact(artifacts, version) 705 verifyMavenDigests(artifacts) 706 checkJavadocAndSourceArtifacts(artifacts, version) 707 verifyDeployedPOMsCoordinates(artifacts, version) 708 if isSigned: 709 verifyMavenSigs(tmpDir, artifacts, keysFile) 710 711 distFiles = getBinaryDistFiles(tmpDir, version, baseURL) 712 checkIdenticalMavenArtifacts(distFiles, artifacts, version) 713 714 checkAllJARs('%s/maven/org/apache/lucene' % tmpDir, gitRevision, version) 715 716 717def getBinaryDistFiles(tmpDir, version, baseURL): 718 distribution = 'lucene-%s.tgz' % version 719 if not os.path.exists('%s/%s' % (tmpDir, distribution)): 720 distURL = '%s/lucene/%s' % (baseURL, distribution) 721 print(' download %s...' % distribution, end=' ') 722 scriptutil.download(distribution, distURL, tmpDir, force_clean=FORCE_CLEAN) 723 destDir = '%s/unpack-lucene-getBinaryDistFiles' % tmpDir 724 if os.path.exists(destDir): 725 shutil.rmtree(destDir) 726 os.makedirs(destDir) 727 os.chdir(destDir) 728 print(' unpack %s...' % distribution) 729 unpackLogFile = '%s/unpack-%s-getBinaryDistFiles.log' % (tmpDir, distribution) 730 run('tar xzf %s/%s' % (tmpDir, distribution), unpackLogFile) 731 distributionFiles = [] 732 for root, dirs, files in os.walk(destDir): 733 distributionFiles.extend([os.path.join(root, file) for file in files]) 734 return distributionFiles 735 736 737def checkJavadocAndSourceArtifacts(artifacts, version): 738 print(' check for javadoc and sources artifacts...') 739 for artifact in artifacts: 740 if artifact.endswith(version + '.jar'): 741 javadocJar = artifact[:-4] + '-javadoc.jar' 742 if javadocJar not in artifacts: 743 raise RuntimeError('missing: %s' % javadocJar) 744 sourcesJar = artifact[:-4] + '-sources.jar' 745 if sourcesJar not in artifacts: 746 raise RuntimeError('missing: %s' % sourcesJar) 747 748 749def getZipFileEntries(fileName): 750 entries = [] 751 with zipfile.ZipFile(fileName) as zf: 752 for zi in zf.infolist(): 753 entries.append(zi.filename) 754 # Sort by name: 755 entries.sort() 756 return entries 757 758 759def checkIdenticalMavenArtifacts(distFiles, artifacts, version): 760 print(' verify that Maven artifacts are same as in the binary distribution...') 761 reJarWar = re.compile(r'%s\.[wj]ar$' % version) # exclude *-javadoc.jar and *-sources.jar 762 distFilenames = dict() 763 for file in distFiles: 764 baseName = os.path.basename(file) 765 distFilenames[baseName] = file 766 for artifact in artifacts: 767 if reJarWar.search(artifact): 768 artifactFilename = os.path.basename(artifact) 769 if artifactFilename not in distFilenames: 770 raise RuntimeError('Maven artifact %s is not present in lucene binary distribution' % artifact) 771 else: 772 identical = filecmp.cmp(artifact, distFilenames[artifactFilename], shallow=False) 773 if not identical: 774 raise RuntimeError('Maven artifact %s is not identical to %s in lucene binary distribution' 775 % (artifact, distFilenames[artifactFilename])) 776 777 778def verifyMavenDigests(artifacts): 779 print(" verify Maven artifacts' md5/sha1 digests...") 780 reJarWarPom = re.compile(r'\.(?:[wj]ar|pom)$') 781 for artifactFile in [a for a in artifacts if reJarWarPom.search(a)]: 782 if artifactFile + '.md5' not in artifacts: 783 raise RuntimeError('missing: MD5 digest for %s' % artifactFile) 784 if artifactFile + '.sha1' not in artifacts: 785 raise RuntimeError('missing: SHA1 digest for %s' % artifactFile) 786 with open(artifactFile + '.md5', encoding='UTF-8') as md5File: 787 md5Expected = md5File.read().strip() 788 with open(artifactFile + '.sha1', encoding='UTF-8') as sha1File: 789 sha1Expected = sha1File.read().strip() 790 md5 = hashlib.md5() 791 sha1 = hashlib.sha1() 792 inputFile = open(artifactFile, 'rb') 793 while True: 794 bytes = inputFile.read(65536) 795 if len(bytes) == 0: 796 break 797 md5.update(bytes) 798 sha1.update(bytes) 799 inputFile.close() 800 md5Actual = md5.hexdigest() 801 sha1Actual = sha1.hexdigest() 802 if md5Actual != md5Expected: 803 raise RuntimeError('MD5 digest mismatch for %s: expected %s but got %s' 804 % (artifactFile, md5Expected, md5Actual)) 805 if sha1Actual != sha1Expected: 806 raise RuntimeError('SHA1 digest mismatch for %s: expected %s but got %s' 807 % (artifactFile, sha1Expected, sha1Actual)) 808 809 810def getPOMcoordinate(treeRoot): 811 namespace = '{http://maven.apache.org/POM/4.0.0}' 812 groupId = treeRoot.find('%sgroupId' % namespace) 813 if groupId is None: 814 groupId = treeRoot.find('{0}parent/{0}groupId'.format(namespace)) 815 groupId = groupId.text.strip() 816 artifactId = treeRoot.find('%sartifactId' % namespace).text.strip() 817 version = treeRoot.find('%sversion' % namespace) 818 if version is None: 819 version = treeRoot.find('{0}parent/{0}version'.format(namespace)) 820 version = version.text.strip() 821 packaging = treeRoot.find('%spackaging' % namespace) 822 packaging = 'jar' if packaging is None else packaging.text.strip() 823 return groupId, artifactId, packaging, version 824 825 826def verifyMavenSigs(tmpDir, artifacts, keysFile): 827 print(' verify maven artifact sigs', end=' ') 828 829 # Set up clean gpg world; import keys file: 830 gpgHomeDir = '%s/lucene.gpg' % tmpDir 831 if os.path.exists(gpgHomeDir): 832 shutil.rmtree(gpgHomeDir) 833 os.makedirs(gpgHomeDir, 0o700) 834 run('gpg --homedir %s --import %s' % (gpgHomeDir, keysFile), 835 '%s/lucene.gpg.import.log' % tmpDir) 836 837 reArtifacts = re.compile(r'\.(?:pom|[jw]ar)$') 838 for artifactFile in [a for a in artifacts if reArtifacts.search(a)]: 839 artifact = os.path.basename(artifactFile) 840 sigFile = '%s.asc' % artifactFile 841 # Test sig (this is done with a clean brand-new GPG world) 842 logFile = '%s/lucene.%s.gpg.verify.log' % (tmpDir, artifact) 843 run('gpg --display-charset utf-8 --homedir %s --verify %s %s' % (gpgHomeDir, sigFile, artifactFile), 844 logFile) 845 846 # Forward any GPG warnings, except the expected one (since it's a clean world) 847 print_warnings_in_file(logFile) 848 849 # Test trust (this is done with the real users config) 850 run('gpg --import %s' % keysFile, 851 '%s/lucene.gpg.trust.import.log' % tmpDir) 852 logFile = '%s/lucene.%s.gpg.trust.log' % (tmpDir, artifact) 853 run('gpg --display-charset utf-8 --verify %s %s' % (sigFile, artifactFile), logFile) 854 # Forward any GPG warnings: 855 print_warnings_in_file(logFile) 856 857 sys.stdout.write('.') 858 print() 859 860 861def print_warnings_in_file(file): 862 with open(file) as f: 863 for line in f.readlines(): 864 if line.lower().find('warning') != -1 \ 865 and line.find('WARNING: This key is not certified with a trusted signature') == -1 \ 866 and line.find('WARNING: using insecure memory') == -1: 867 print(' GPG: %s' % line.strip()) 868 869 870def verifyPOMperBinaryArtifact(artifacts, version): 871 print(' verify that each binary artifact has a deployed POM...') 872 reBinaryJarWar = re.compile(r'%s\.[jw]ar$' % re.escape(version)) 873 for artifact in [a for a in artifacts if reBinaryJarWar.search(a)]: 874 POM = artifact[:-4] + '.pom' 875 if POM not in artifacts: 876 raise RuntimeError('missing: POM for %s' % artifact) 877 878 879def verifyDeployedPOMsCoordinates(artifacts, version): 880 """ 881 verify that each POM's coordinate (drawn from its content) matches 882 its filepath, and verify that the corresponding artifact exists. 883 """ 884 print(" verify deployed POMs' coordinates...") 885 for POM in [a for a in artifacts if a.endswith('.pom')]: 886 treeRoot = ET.parse(POM).getroot() 887 groupId, artifactId, packaging, POMversion = getPOMcoordinate(treeRoot) 888 POMpath = '%s/%s/%s/%s-%s.pom' \ 889 % (groupId.replace('.', '/'), artifactId, version, artifactId, version) 890 if not POM.endswith(POMpath): 891 raise RuntimeError("Mismatch between POM coordinate %s:%s:%s and filepath: %s" 892 % (groupId, artifactId, POMversion, POM)) 893 # Verify that the corresponding artifact exists 894 artifact = POM[:-3] + packaging 895 if artifact not in artifacts: 896 raise RuntimeError('Missing corresponding .%s artifact for POM %s' % (packaging, POM)) 897 898 899def crawl(downloadedFiles, urlString, targetDir, exclusions=set()): 900 for text, subURL in getDirEntries(urlString): 901 if text not in exclusions: 902 path = os.path.join(targetDir, text) 903 if text.endswith('/'): 904 if not os.path.exists(path): 905 os.makedirs(path) 906 crawl(downloadedFiles, subURL, path, exclusions) 907 else: 908 if not os.path.exists(path) or FORCE_CLEAN: 909 scriptutil.download(text, subURL, targetDir, quiet=True, force_clean=FORCE_CLEAN) 910 downloadedFiles.append(path) 911 sys.stdout.write('.') 912 913 914def make_java_config(parser, java18_home): 915 def _make_runner(java_home, version): 916 print('Java %s JAVA_HOME=%s' % (version, java_home)) 917 if cygwin: 918 java_home = subprocess.check_output('cygpath -u "%s"' % java_home, shell=True).decode('utf-8').strip() 919 cmd_prefix = 'export JAVA_HOME="%s" PATH="%s/bin:$PATH" JAVACMD="%s/bin/java"' % \ 920 (java_home, java_home, java_home) 921 s = subprocess.check_output('%s; java -version' % cmd_prefix, 922 shell=True, stderr=subprocess.STDOUT).decode('utf-8') 923 if s.find(' version "%s' % version) == -1: 924 parser.error('got wrong version for java %s:\n%s' % (version, s)) 925 def run_java(cmd, logfile): 926 run('%s; %s' % (cmd_prefix, cmd), logfile) 927 return run_java 928 java17_home = os.environ.get('JAVA_HOME') 929 if java17_home is None: 930 parser.error('JAVA_HOME must be set') 931 run_java17 = _make_runner(java17_home, '17') 932 run_java18 = None 933 if java18_home is not None: 934 run_java18 = _make_runner(java18_home, '18') 935 936 jc = namedtuple('JavaConfig', 'run_java17 java17_home run_java18 java18_home') 937 return jc(run_java17, java17_home, run_java18, java18_home) 938 939version_re = re.compile(r'(\d+\.\d+\.\d+(-ALPHA|-BETA)?)') 940revision_re = re.compile(r'rev-([a-f\d]+)') 941def parse_config(): 942 epilogue = textwrap.dedent(''' 943 Example usage: 944 python3 -u dev-tools/scripts/smokeTestRelease.py https://dist.apache.org/repos/dist/dev/lucene/lucene-9.0.0-RC1-rev-c7510a0... 945 ''') 946 description = 'Utility to test a release.' 947 parser = argparse.ArgumentParser(description=description, epilog=epilogue, 948 formatter_class=argparse.RawDescriptionHelpFormatter) 949 parser.add_argument('--tmp-dir', metavar='PATH', 950 help='Temporary directory to test inside, defaults to /tmp/smoke_lucene_$version_$revision') 951 parser.add_argument('--not-signed', dest='is_signed', action='store_false', default=True, 952 help='Indicates the release is not signed') 953 parser.add_argument('--local-keys', metavar='PATH', 954 help='Uses local KEYS file instead of fetching from https://archive.apache.org/dist/lucene/KEYS') 955 parser.add_argument('--revision', 956 help='GIT revision number that release was built with, defaults to that in URL') 957 parser.add_argument('--version', metavar='X.Y.Z(-ALPHA|-BETA)?', 958 help='Version of the release, defaults to that in URL') 959 parser.add_argument('--test-java18', metavar='java18_home', 960 help='Path to Java home directory, to run tests with if specified') 961 parser.add_argument('--download-only', action='store_true', default=False, 962 help='Only perform download and sha hash check steps') 963 parser.add_argument('url', help='Url pointing to release to test') 964 parser.add_argument('test_args', nargs=argparse.REMAINDER, 965 help='Arguments to pass to gradle for testing, e.g. -Dwhat=ever.') 966 c = parser.parse_args() 967 968 if c.version is not None: 969 if not version_re.match(c.version): 970 parser.error('version "%s" does not match format X.Y.Z[-ALPHA|-BETA]' % c.version) 971 else: 972 version_match = version_re.search(c.url) 973 if version_match is None: 974 parser.error('Could not find version in URL') 975 c.version = version_match.group(1) 976 977 if c.revision is None: 978 revision_match = revision_re.search(c.url) 979 if revision_match is None: 980 parser.error('Could not find revision in URL') 981 c.revision = revision_match.group(1) 982 print('Revision: %s' % c.revision) 983 984 if c.local_keys is not None and not os.path.exists(c.local_keys): 985 parser.error('Local KEYS file "%s" not found' % c.local_keys) 986 987 c.java = make_java_config(parser, c.test_java18) 988 989 if c.tmp_dir: 990 c.tmp_dir = os.path.abspath(c.tmp_dir) 991 else: 992 tmp = '/tmp/smoke_lucene_%s_%s' % (c.version, c.revision) 993 c.tmp_dir = tmp 994 i = 1 995 while os.path.exists(c.tmp_dir): 996 c.tmp_dir = tmp + '_%d' % i 997 i += 1 998 999 return c 1000 1001reVersion1 = re.compile(r'\>(\d+)\.(\d+)\.(\d+)(-alpha|-beta)?/\<', re.IGNORECASE) 1002reVersion2 = re.compile(r'-(\d+)\.(\d+)\.(\d+)(-alpha|-beta)?\.', re.IGNORECASE) 1003 1004def getAllLuceneReleases(): 1005 s = load('https://archive.apache.org/dist/lucene/java') 1006 1007 releases = set() 1008 for r in reVersion1, reVersion2: 1009 for tup in r.findall(s): 1010 if tup[-1].lower() == '-alpha': 1011 tup = tup[:3] + ('0',) 1012 elif tup[-1].lower() == '-beta': 1013 tup = tup[:3] + ('1',) 1014 elif tup[-1] == '': 1015 tup = tup[:3] 1016 else: 1017 raise RuntimeError('failed to parse version: %s' % tup[-1]) 1018 releases.add(tuple(int(x) for x in tup)) 1019 1020 l = list(releases) 1021 l.sort() 1022 return l 1023 1024 1025def confirmAllReleasesAreTestedForBackCompat(smokeVersion, unpackPath): 1026 1027 print(' find all past Lucene releases...') 1028 allReleases = getAllLuceneReleases() 1029 #for tup in allReleases: 1030 # print(' %s' % '.'.join(str(x) for x in tup)) 1031 1032 testedIndicesPaths = glob.glob('%s/lucene/backward-codecs/src/test/org/apache/lucene/backward_index/*-cfs.zip' % unpackPath) 1033 testedIndices = set() 1034 1035 reIndexName = re.compile(r'^[^.]*.(.*?)-cfs.zip') 1036 for name in testedIndicesPaths: 1037 basename = os.path.basename(name) 1038 version = reIndexName.fullmatch(basename).group(1) 1039 tup = tuple(version.split('.')) 1040 if len(tup) == 3: 1041 # ok 1042 tup = tuple(int(x) for x in tup) 1043 elif tup == ('4', '0', '0', '1'): 1044 # CONFUSING: this is the 4.0.0-alpha index?? 1045 tup = 4, 0, 0, 0 1046 elif tup == ('4', '0', '0', '2'): 1047 # CONFUSING: this is the 4.0.0-beta index?? 1048 tup = 4, 0, 0, 1 1049 elif basename == 'unsupported.5x-with-4x-segments-cfs.zip': 1050 # Mixed version test case; ignore it for our purposes because we only 1051 # tally up the "tests single Lucene version" indices 1052 continue 1053 elif basename == 'unsupported.5.0.0.singlesegment-cfs.zip': 1054 tup = 5, 0, 0 1055 else: 1056 raise RuntimeError('could not parse version %s' % name) 1057 1058 testedIndices.add(tup) 1059 1060 l = list(testedIndices) 1061 l.sort() 1062 if False: 1063 for release in l: 1064 print(' %s' % '.'.join(str(x) for x in release)) 1065 1066 allReleases = set(allReleases) 1067 1068 for x in testedIndices: 1069 if x not in allReleases: 1070 # Curious: we test 1.9.0 index but it's not in the releases (I think it was pulled because of nasty bug?) 1071 if x != (1, 9, 0): 1072 raise RuntimeError('tested version=%s but it was not released?' % '.'.join(str(y) for y in x)) 1073 1074 notTested = [] 1075 for x in allReleases: 1076 if x not in testedIndices: 1077 releaseVersion = '.'.join(str(y) for y in x) 1078 if releaseVersion in ('1.4.3', '1.9.1', '2.3.1', '2.3.2'): 1079 # Exempt the dark ages indices 1080 continue 1081 if x >= tuple(int(y) for y in smokeVersion.split('.')): 1082 # Exempt versions not less than the one being smoke tested 1083 print(' Backcompat testing not required for release %s because it\'s not less than %s' 1084 % (releaseVersion, smokeVersion)) 1085 continue 1086 notTested.append(x) 1087 1088 if len(notTested) > 0: 1089 notTested.sort() 1090 print('Releases that don\'t seem to be tested:') 1091 failed = True 1092 for x in notTested: 1093 print(' %s' % '.'.join(str(y) for y in x)) 1094 raise RuntimeError('some releases are not tested by TestBackwardsCompatibility?') 1095 else: 1096 print(' success!') 1097 1098 1099def main(): 1100 c = parse_config() 1101 1102 # Pick <major>.<minor> part of version and require script to be from same branch 1103 scriptVersion = re.search(r'((\d+).(\d+)).(\d+)', scriptutil.find_current_version()).group(1).strip() 1104 if not c.version.startswith(scriptVersion + '.'): 1105 raise RuntimeError('smokeTestRelease.py for %s.X is incompatible with a %s release.' % (scriptVersion, c.version)) 1106 1107 print('NOTE: output encoding is %s' % sys.stdout.encoding) 1108 smokeTest(c.java, c.url, c.revision, c.version, c.tmp_dir, c.is_signed, c.local_keys, ' '.join(c.test_args), 1109 downloadOnly=c.download_only) 1110 1111 1112def smokeTest(java, baseURL, gitRevision, version, tmpDir, isSigned, local_keys, testArgs, downloadOnly=False): 1113 startTime = datetime.datetime.now() 1114 1115 # Tests annotated @Nightly are more resource-intensive but often cover 1116 # important code paths. They're disabled by default to preserve a good 1117 # developer experience, but we enable them for smoke tests where we want good 1118 # coverage. 1119 testArgs = '-Dtests.nightly=true %s' % testArgs 1120 1121 # We also enable GUI tests in smoke tests (LUCENE-10531) 1122 testArgs = '-Dtests.gui=true %s' % testArgs 1123 1124 if FORCE_CLEAN: 1125 if os.path.exists(tmpDir): 1126 raise RuntimeError('temp dir %s exists; please remove first' % tmpDir) 1127 1128 if not os.path.exists(tmpDir): 1129 os.makedirs(tmpDir) 1130 1131 lucenePath = None 1132 print() 1133 print('Load release URL "%s"...' % baseURL) 1134 newBaseURL = unshortenURL(baseURL) 1135 if newBaseURL != baseURL: 1136 print(' unshortened: %s' % newBaseURL) 1137 baseURL = newBaseURL 1138 1139 for text, subURL in getDirEntries(baseURL): 1140 if text.lower().find('lucene') != -1: 1141 lucenePath = subURL 1142 1143 if lucenePath is None: 1144 raise RuntimeError('could not find lucene subdir') 1145 1146 print() 1147 print('Get KEYS...') 1148 if local_keys is not None: 1149 print(" Using local KEYS file %s" % local_keys) 1150 keysFile = local_keys 1151 else: 1152 keysFileURL = "https://archive.apache.org/dist/lucene/KEYS" 1153 print(" Downloading online KEYS file %s" % keysFileURL) 1154 scriptutil.download('KEYS', keysFileURL, tmpDir, force_clean=FORCE_CLEAN) 1155 keysFile = '%s/KEYS' % (tmpDir) 1156 1157 print() 1158 print('Test Lucene...') 1159 checkSigs(lucenePath, version, tmpDir, isSigned, keysFile) 1160 if not downloadOnly: 1161 unpackAndVerify(java, tmpDir, 'lucene-%s.tgz' % version, gitRevision, version, testArgs) 1162 unpackAndVerify(java, tmpDir, 'lucene-%s-src.tgz' % version, gitRevision, version, testArgs) 1163 print() 1164 print('Test Maven artifacts...') 1165 checkMaven(baseURL, tmpDir, gitRevision, version, isSigned, keysFile) 1166 else: 1167 print("\nLucene test done (--download-only specified)") 1168 1169 print('\nSUCCESS! [%s]\n' % (datetime.datetime.now() - startTime)) 1170 1171 1172if __name__ == '__main__': 1173 try: 1174 main() 1175 except KeyboardInterrupt: 1176 print('Keyboard interrupt...exiting') 1177 1178