blob: 938419136fd38c3bb6b74ddae05871e2bbf63c95 [file] [log] [blame]
# 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,
)