| # Copyright 2018 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import datetime, re |
| |
| from recipe_engine.post_process import ( |
| DoesNotRunRE, |
| StepCommandContains, |
| StepCommandRE, |
| ) |
| |
| DEPS = [ |
| 'dart', |
| 'depot_tools/git', |
| 'depot_tools/gsutil', |
| 'recipe_engine/buildbucket', |
| 'recipe_engine/context', |
| 'recipe_engine/file', |
| 'recipe_engine/path', |
| 'recipe_engine/platform', |
| 'recipe_engine/properties', |
| 'recipe_engine/raw_io', |
| 'recipe_engine/step', |
| 'recipe_engine/time', |
| 'recipe_engine/url', |
| ] |
| |
| PACKAGE = 'dart-sdk' |
| RELEASE_BUCKET = 'gs://dart-archive/channels/%s/release/' |
| CHANNELS = ['dev', 'beta', 'stable'] |
| INSTALLER_NAME = 'install.ps1' |
| INSTALLER = 'https://chocolatey.org/%s' % INSTALLER_NAME |
| POWERSHELL = ( |
| 'C:\\\\WINDOWS\\\\system32\\\\WindowsPowerShell\\\\v1.0\\\\powershell.exe') |
| CHECKSUM = ( |
| 'https://storage.googleapis.com/dart-archive/' |
| 'channels/%s/release/%s/sdk/dartsdk-windows-%s-release.zip.sha256sum') |
| CHOCOLATEY_VERSION_PATTERN = r'^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:.(?P<prerelease>(?:0|[1-9]\d*)))?(?:-c-(?P<prerelease_patch>(?:[0-9]\d*))-(?P<channel>(?:[a-z]*)))?$' |
| SEMANTIC_VERSION_PATTERN = r'^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' |
| MODERN_VERSIONS = { |
| 'dev': '3.0.0', |
| 'beta': '3.0.0', |
| 'stable': '3.0.0', |
| } |
| |
| PYTHON_VERSION_COMPATIBILITY = 'PY3' |
| |
| |
| def _is_version(api, version): |
| 'Whether the string is a valid semantic version' |
| return (re.match(SEMANTIC_VERSION_PATTERN, version) and |
| # The version class isn't able to extract channels for old versions. |
| api.dart.Version(version=version).fields['CHANNEL'] in CHANNELS) |
| |
| |
| def _is_modern_version(api, version_str): |
| 'Whether the version is new enough to synchronize to chocolatey' |
| if not _is_version(api, version_str): |
| return False |
| |
| version = api.dart.Version(version=version_str) |
| modern_version = api.dart.Version( |
| version=MODERN_VERSIONS[version.fields['CHANNEL']]) |
| return version > modern_version |
| |
| |
| def _channel_of_version(version): |
| for channel in CHANNELS: |
| if channel in version: |
| return channel |
| return 'stable' |
| |
| |
| def _parse_gsurl_release(line): |
| return line.split('/')[6] |
| |
| |
| def _parse_chocolatey_release(line: str) -> str: |
| return line.split(' ')[1] |
| |
| |
| def _decode_chocolatey_version(match: re.Match): |
| major = match.group('major') |
| minor = match.group('minor') |
| patch = match.group('patch') |
| prerelease = match.group('prerelease') |
| prerelease_patch = match.group('prerelease_patch') |
| channel = match.group('channel') |
| |
| if any((prerelease, prerelease_patch, channel)): |
| if not prerelease: |
| prerelease = '0' |
| return f'{major}.{minor}.{patch}-{prerelease}.{int(prerelease_patch)}.{channel}' |
| else: |
| return f'{major}.{minor}.{patch}' |
| |
| |
| def _encode_chocolatey_version(version): |
| '2.1.4-22.13.dev -> 2.1.4.22-c-013-dev' |
| if '.dev' in version or '.beta' in version: |
| # todo(athom): remove when chocolatey supports semver 2.0.0 |
| # See https://github.com/chocolatey/choco/issues/1610 |
| (version, build) = version.split('-') |
| parts = build.split('.') |
| # pad with zeros, because chocolatey compares pre-release alphabetically |
| version = "%s.%s-c-%s-%s" % ( |
| (version, parts.pop(0)) + tuple(part.zfill(3) for part in parts)) |
| return version |
| |
| |
| def RunSteps(api): |
| # No tryjob since it uses a secret key, that restriction could be alleviated. |
| assert not api.buildbucket.builder_name.endswith( |
| '-try'), 'tryjob is not supported' |
| choco_installer = api.path['cleanup'].join(INSTALLER_NAME) |
| api.url.get_file(INSTALLER, choco_installer, 'download chocolatey installer') |
| |
| choco_home = api.path['cleanup'].join('chocolatey') |
| bin_root = api.path['cleanup'].join('bin_root') |
| env = {'ChocolateyInstall': choco_home, 'ChocolateyBinRoot': bin_root} |
| with api.context(env=env): |
| api.step('install chocolatey', [POWERSHELL, choco_installer]) |
| |
| choco = choco_home.join('choco') |
| api.step('choco --version', [choco, '--version']) |
| |
| cache = api.path['cleanup'].join('cache') |
| api.step('choco set package directory', |
| [choco, 'config', 'set', 'cacheLocation', cache]) |
| |
| # List the released versions via cloud storage. |
| released_versions = set() |
| for channel in CHANNELS: |
| versions_data = api.gsutil.list( |
| RELEASE_BUCKET % channel, |
| name='list %s releases' % channel, |
| stdout=api.raw_io.output_text(add_output_log=True)) |
| for line in versions_data.stdout.splitlines(): |
| version = _parse_gsurl_release(line) |
| if _is_modern_version(api, version): |
| released_versions.add(version) |
| |
| # List the versions already released on chocolatey. |
| published_data = api.step( |
| 'list published versions', [ |
| choco, |
| 'search', |
| '--all', |
| '--exact', |
| '--pre', |
| PACKAGE, |
| ], |
| stdout=api.raw_io.output_text(add_output_log=True)) |
| published_versions = set() |
| for line in published_data.stdout.splitlines(): |
| if line.startswith(PACKAGE + ' '): |
| choco_version = _parse_chocolatey_release(line) |
| match = re.match(CHOCOLATEY_VERSION_PATTERN, choco_version) |
| if match and int(match.group('major')) >= 3: |
| version = _decode_chocolatey_version(match) |
| else: |
| match = re.match(SEMANTIC_VERSION_PATTERN, choco_version) |
| version = choco_version |
| if match and int(match.group('major')) >= 3: |
| if _is_modern_version(api, version): |
| published_versions.add(version) |
| |
| # Stop if chocolatey is up to date. |
| new_versions = sorted(released_versions - published_versions) |
| if not new_versions: |
| api.step.empty('chocolatey is up to date') |
| return |
| |
| # Check out the chocolatey package installer template. |
| api.git.checkout( |
| url='https://github.com/dart-lang/chocolatey-packages.git', |
| ref='refs/heads/main') |
| |
| # Read the installer template. |
| chocolatey_dir = api.path['start_dir'].join('chocolatey-packages') |
| package_dir = chocolatey_dir.join(PACKAGE) |
| installer_path = package_dir.join('chocolateyInstall.ps1') |
| installer = api.file.read_text('read installer', installer_path) |
| |
| # Decode the private key for chocolatey uploads. |
| chocolatey_key = api.dart.get_secret('chocolatey') |
| |
| # Release the new versions on chocolatey. |
| for version in new_versions: |
| channel = _channel_of_version(version) |
| _publish_version(api, channel, version, choco, package_dir, installer, |
| installer_path, chocolatey_key) |
| |
| |
| def _publish_version(api, channel, version, choco, package_dir, installer, |
| installer_path, chocolatey_key): |
| checksum = api.url.get_text( |
| CHECKSUM % (channel, version, 'ia32'), |
| default_test_data='abc *should-not-see-this') |
| checksum = checksum.output.split()[0] |
| checksum64 = api.url.get_text( |
| CHECKSUM % (channel, version, 'x64'), |
| default_test_data='def *should-not-see-this') |
| checksum64 = checksum64.output.split()[0] |
| |
| installer = installer.replace('$version$', version) |
| installer = installer.replace('$channel$', channel) |
| installer = installer.replace('$checksum$', checksum) |
| installer = installer.replace('$checksum64$', checksum64) |
| |
| api.file.write_text('write installer %s %s' % (channel, version), |
| installer_path, installer) |
| |
| with api.context(cwd=package_dir): |
| choco_version = _encode_chocolatey_version(version) |
| |
| api.step('choco pack %s %s' % (channel, version), |
| [choco, 'pack', 'version=%s' % choco_version]) |
| |
| api.step('verify with choco install %s %s' % (channel, version), |
| [choco, 'install', PACKAGE, '--pre', '-y', '-dv', '-s', '.']) |
| |
| choco_push = (f'$secret = cat {chocolatey_key}; ' + |
| f'{choco} push --source https://push.chocolatey.org/ ' + |
| f'-k="$secret" {PACKAGE}.{choco_version}.nupkg') |
| |
| @api.time.exponential_retry(2, datetime.timedelta(minutes=1)) |
| def push_retry(): |
| api.step('choco push %s %s' % (channel, version), |
| [POWERSHELL, '-Command', choco_push]) |
| |
| push_retry() |
| |
| |
| DEV_RELEASES = '\n'.join( |
| map(lambda v: (RELEASE_BUCKET + '%s/') % ('dev', v), [ |
| '1.11.0-dev.3.0', '2.0.0-dev.0.0', '2.16.0-80.0.dev', '2.18.0-1.0.dev', |
| '2.18.0-162.0.dev', '2.18.0-18.0.dev', '2.18.0-7.0.dev', |
| '3.0.0-0.0.dev', '3.0.0-18.0.dev', '3.1.0-15.0.dev', '45519', |
| '4.1.0-18.0.dev', 'latest' |
| ])) + '\n' |
| |
| BETA_RELEASES = '\n'.join( |
| map(lambda v: (RELEASE_BUCKET + '%s/') % ('beta', v), [ |
| '2.10.0-110.1.beta', '2.16.0-80.1.beta', '2.16.0', '2.16.1', |
| '2.18.0-69.1.beta', '2.19.0-444.2.beta', '3.1.0-417.4.beta', 'latest' |
| ])) + '\n' |
| |
| STABLE_RELEASES = '\n'.join( |
| map(lambda v: (RELEASE_BUCKET + '%s/') % ('stable', v), [ |
| '1.11.0', '2.0.0', '2.15.1', '2.16.0', '2.16.1', '2.18.0', '2.18.6', |
| '2.2.0', '3.1.1', '45692', 'latest' |
| ])) + '\n' |
| |
| CHOCOLATEY_SYNCHRONIZED = ''' |
| Chocolatey v2.0.0 |
| dart-sdk 4.1.0-18.0.dev [Approved] Uses proper semantic version |
| dart-sdk 3.1.2 [Approved] Downloads cached for licensed users |
| dart-sdk 3.1.1 [Approved] Downloads cached for licensed users |
| dart-sdk 3.1.0.417-c-004-beta |
| dart-sdk 3.1.0.15-c-000-dev [Approved] Downloads cached for licensed users |
| dart-sdk 3.1.0.11-c-000-dev - Possibly broken |
| dart-sdk 3.1.0.2-c-000-dev [Approved] Downloads cached for licensed users |
| dart-sdk 3.1.0-c-000-dev [Approved] Downloads cached for licensed users |
| dart-sdk 3.0.0 [Approved] Downloads cached for licensed users |
| dart-sdk 3.0.0.21-c-000-dev [Approved] Downloads cached for licensed users |
| dart-sdk 3.0.0-c-000-dev [Approved] Downloads cached for licensed users |
| dart-sdk 2.19.6 [Approved] Downloads cached for licensed users |
| dart-sdk 2.19.0.444-c-006-beta [Approved] |
| dart-sdk 2.19.0.444-c-000-dev Downloads cached for licensed users |
| dart-sdk 2.19.0.398-c-000-dev - Possibly broken |
| dart-sdk 2.19.0.374-c-002-beta |
| dart-sdk 2.19.0.374-c-001-beta Downloads cached for licensed users |
| dart-sdk 2.19.0.374-c-000-dev Downloads cached for licensed users |
| dart-sdk 2.0.0.51-dev-0 [Approved] |
| dart-sdk 2.0.0.51-dev [Approved] Downloads cached for licensed users |
| dart-sdk 2.0.0.50-dev [Approved] Downloads cached for licensed users |
| dart-sdk 2.0.0 [Approved] Downloads cached for licensed users |
| dart-sdk 1.7.2.1 [Approved] |
| 401 packages found. |
| ''' |
| |
| UNSYNCHRONIZED_VERSIONS = { |
| '4.1.0.18-c-000-dev': '4.1.0-18.0.dev', |
| '3.1.0.15-c-000-dev': '3.1.0-15.0.dev', |
| '3.1.0.417-c-004-beta': '3.1.0-417.4.beta', |
| '3.1.1': '3.1.1', |
| } |
| |
| CHOCOLATEY_UNSYNCHRONIZED = '\n'.join([ |
| line for line in CHOCOLATEY_SYNCHRONIZED.split('\n') |
| if line.startswith('Chocolatey') or |
| line and _parse_chocolatey_release(line) not in UNSYNCHRONIZED_VERSIONS and |
| _parse_chocolatey_release(line) not in UNSYNCHRONIZED_VERSIONS.values() |
| ]) |
| |
| |
| def GenTests(api): |
| yield api.test( |
| 'synchronized', |
| api.platform('win', 64), |
| api.step_data( |
| 'gsutil list dev releases', |
| stdout=api.raw_io.output_text(DEV_RELEASES)), |
| api.step_data( |
| 'gsutil list beta releases', |
| stdout=api.raw_io.output_text(BETA_RELEASES)), |
| api.step_data( |
| 'gsutil list stable releases', |
| stdout=api.raw_io.output_text(STABLE_RELEASES)), |
| api.step_data( |
| 'list published versions', |
| stdout=api.raw_io.output_text(CHOCOLATEY_SYNCHRONIZED)), |
| api.post_process(DoesNotRunRE, 'choco pack'), |
| ) |
| |
| expected_steps = [] |
| for chocolatey_version, semantic_version in UNSYNCHRONIZED_VERSIONS.items(): |
| channel = 'dev' if 'dev' in semantic_version else 'beta' if 'beta' in semantic_version else 'stable' |
| expected_steps.append( |
| api.post_process(StepCommandContains, |
| f'choco pack {channel} {semantic_version}', |
| [f'version={chocolatey_version}'])) |
| package_re = chocolatey_version.replace(".", r"\.") |
| expected_steps.append( |
| api.post_process(StepCommandRE, |
| f'choco push {channel} {semantic_version}', |
| ['.*', '.*', f'.*{package_re}\\.nupkg'])) |
| yield api.test( |
| 'unsynchronized', |
| api.platform('win', 64), |
| api.step_data( |
| 'gsutil list dev releases', |
| stdout=api.raw_io.output_text(DEV_RELEASES)), |
| api.step_data( |
| 'gsutil list beta releases', |
| stdout=api.raw_io.output_text(BETA_RELEASES)), |
| api.step_data( |
| 'gsutil list stable releases', |
| stdout=api.raw_io.output_text(STABLE_RELEASES)), |
| api.step_data( |
| 'list published versions', |
| stdout=api.raw_io.output_text(CHOCOLATEY_UNSYNCHRONIZED)), |
| *expected_steps, |
| ) |