| #!/usr/bin/env python2 |
| # -*- coding: utf-8 -*-" |
| # Copyright 2017 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Script to build a component artifact.""" |
| |
| from __future__ import print_function |
| |
| import json |
| import os |
| import re |
| import zipfile |
| |
| from distutils.version import LooseVersion |
| |
| from chromite.lib import commandline |
| from chromite.lib import cros_build_lib |
| from chromite.lib import cros_logging as logging |
| from chromite.lib import gs |
| from chromite.lib import osutils |
| |
| logger = logging.getLogger(__name__) |
| |
| COMPONENT_ZIP = 'files.zip' |
| MANIFEST_FILE_NAME = 'manifest.json' |
| MANIFEST_VERSION_FIELD = u'version' |
| MANIFEST_PACKAGE_VERSION_FIELD = u'package_version' |
| |
| def GetParser(): |
| parser = commandline.ArgumentParser(description=__doc__) |
| # Optional arguments: |
| parser.add_argument('--gsbucket', default=None, metavar='GS_BUCKET_URI', |
| help='Override the gsbucket field (Google Cloud Storage ' |
| 'bucket where component is uploaded to) in config ' |
| 'file.') |
| parser.add_argument('--upload', dest='upload', action='store_true', |
| default=False, |
| help='Upload to Omaha gsbucket.') |
| # Required arguments: |
| required = parser.add_argument_group('required arguments') |
| required.add_argument('--board', metavar='BOARD', |
| help='Board to build the component for.', |
| required=True) |
| required.add_argument('--config_path', metavar='CONFIG', |
| help='Path to the config file.', required=True) |
| required.add_argument('--platform', metavar='PLATFORM', |
| help='Name for the platform folder in Omaha.', |
| choices=['chromeos_arm32-archive', |
| 'chromeos_intel64-archive', |
| 'chromeos_arm32', |
| 'chromeos_intel64'], required=True) |
| # Positional arguments: |
| parser.add_argument('component', metavar='COMPONENT', |
| help='The component to build (key inside the config ' |
| 'file).') |
| return parser |
| |
| |
| def ParseVersion(version_str): |
| """Parse version string into a list with components. |
| |
| Args: |
| version_str: (str) version string. |
| |
| Returns: |
| [int]: a list with version components. |
| """ |
| pattern = re.compile("[0-9]+(\\.[0-9]+){2,3}") |
| m = pattern.match(version_str) |
| if m: |
| return [int(x) for x in m.group().split('.')] |
| return [] |
| |
| def CheckGsBucket(gsbucket): |
| """Return list of folders in a gs bucket. |
| |
| Args: |
| gsbucket: (str) gs bucket url. |
| |
| Returns: |
| [str]: a list of folder paths. |
| """ |
| ctx = gs.GSContext() |
| dirs = ctx.LS(gsbucket) |
| |
| return [x for x in dirs if x != gsbucket] |
| |
| |
| def GetCurrentVersion(paths, platform): |
| """Find the current component version by iterating gsbucket root folder. |
| |
| Args: |
| paths: ([str]) a list of folder paths strings. |
| platform: (str) the platform for which the component is being built |
| |
| Returns: |
| str: current component version. |
| str: gs path for current component version. |
| """ |
| current_version = LooseVersion('0.0.0.0') |
| current_version_path = None |
| |
| for version_path in paths: |
| if version_path[-1] != '/': |
| logger.fatal("version_path (%s) needs to end with '/'.", version_path) |
| continue |
| version = os.path.basename(version_path[:-1]) |
| if len(ParseVersion(version)) < 3: |
| # Path does not contain a component version. |
| continue |
| |
| v = LooseVersion(version) |
| if v > current_version: |
| # Skip the version if the path for the target platform does not exist. |
| ctx = gs.GSContext() |
| src = os.path.join(version_path, platform, COMPONENT_ZIP) |
| if not ctx.Exists(src): |
| continue |
| |
| current_version = v |
| current_version_path = version_path |
| return str(current_version), current_version_path |
| |
| |
| def DecideVersion(version, current_version): |
| """Decide the component version |
| |
| Each version has release.major.minor[.bugfix] format. |
| If release.major.minor are equal, then use current_version as version and |
| increase bugfix by 1 (set to be 1 if bugfix is missing). Otherwise, use |
| version (package) as final version and set bugfix 1. |
| |
| Args: |
| version: (str) package version |
| current_version: (str) current component version |
| |
| Returns: |
| str: next component version. |
| """ |
| version = ParseVersion(version) |
| current_version = ParseVersion(current_version) |
| if (len(version) != 3 and len(version) != 4) or \ |
| (len(current_version) != 3 and len(current_version) != 4): |
| logger.fatal('version is in wrong format.') |
| return None |
| |
| if LooseVersion('.'.join([str(x) for x in version[0:3]])) < \ |
| LooseVersion('.'.join([str(x) for x in current_version[0:3]])): |
| logger.fatal('component being built is outdated.') |
| return None |
| if version[0] == current_version[0] and version[1] == current_version[1] and \ |
| version[2] == current_version[2]: |
| # Rev bug fix on top of current_version. |
| version = current_version |
| if len(version) < 4: |
| version.append(1) |
| else: |
| version[3] = version[3] + 1 |
| else: |
| # Use package version.1 as next component version. |
| if len(version) < 4: |
| version.append(1) |
| else: |
| version[3] = 1 |
| return '.'.join([str(x) for x in version]) |
| |
| |
| def CheckValidMetadata(metadata): |
| """Check if metadata in configuration is valid. |
| |
| Args: |
| metadata: (str) metadata in component configs. |
| |
| Returns: |
| bool: if metadata is valid. |
| """ |
| if not "package_name" in metadata or \ |
| not "files" in metadata or \ |
| not "gsbucket" in metadata or \ |
| not "pkgpath" in metadata or \ |
| not "manifest" in metadata: |
| cros_build_lib.Die('attribute is missing.') |
| return False |
| else: |
| return True |
| |
| |
| def CheckComponentFilesExistence(paths): |
| """Check if paths exist. |
| |
| Args: |
| paths: ([str]) a list of path. |
| |
| Returns: |
| bool: true if all paths exists. |
| """ |
| for path in paths: |
| if not os.path.exists(path): |
| cros_build_lib.Die('component file is missing: %s', path) |
| return False |
| logger.info('File to be included to final component: %s', path) |
| return True |
| |
| |
| def AddDirectoryToZip(zip_file, dir_path): |
| """Adds a directory to a zip file. |
| |
| This will add the whole directory to the zip, rather than contents of the |
| directory. Empty (sub)directories will be ignored. |
| |
| Args: |
| zip_file: (zipfile.ZipFile) the zip file to which to add the dir. |
| dir_path: (string) the directory to add to the zip file. |
| """ |
| # The directories parent path, used to calculate the target paths in the zip |
| # file, which should include the |dir_path| itself. |
| dir_parent = os.path.normpath(os.path.join(dir_path, os.pardir)) |
| |
| for current_dir, _subdirs, file_names in os.walk(dir_path): |
| for file_name in file_names: |
| file_path = os.path.normpath(os.path.join(current_dir, file_name)) |
| zip_path = os.path.relpath(file_path, dir_parent) |
| zip_file.write(file_path, zip_path) |
| |
| |
| def UploadComponent(component_dir, gsbucket): |
| """Upload a component. |
| |
| Args: |
| component_dir: (str) location for generated component. |
| gsbucket: (str) gs bucket to upload. |
| """ |
| logger.info('upload %s to %s', component_dir, gsbucket) |
| ctx = gs.GSContext() |
| if cros_build_lib.BooleanPrompt( |
| prompt='Are you sure you want to upload component to official gs bucket', |
| default=False, |
| prolog='Once component is uploaded, it can not be modified again.'): |
| # Upload component to gs. |
| ctx.Copy(component_dir, gsbucket, recursive=True) |
| |
| |
| def CreateComponent(manifest_path, version, package_name, package_version, |
| platform, files, upload, gsbucket): |
| """Create component zip file. |
| |
| Args: |
| manifest_path: (str) path to raw manifest file. |
| version: (str) component version. |
| package_name: (str) the package name |
| package_version: (str) package version. |
| platform: (str) platform folder name on Omaha. |
| files: ([str]) paths for component files. |
| upload: (bool) whether to upload the generate component to Omaha. |
| gsbucket: (str) Omaha gsbucket path. |
| """ |
| if not os.path.exists(manifest_path): |
| cros_build_lib.Die('manifest file is missing: %s', manifest_path) |
| with open(manifest_path) as f: |
| # Construct final manifest file. |
| data = json.load(f) |
| data[MANIFEST_VERSION_FIELD] = version |
| data[MANIFEST_PACKAGE_VERSION_FIELD] = package_version |
| # Create final zip file of the component and store it to a temp folder. |
| with osutils.TempDir(prefix='component_') as tempdir: |
| component_folder = os.path.join(tempdir, data[MANIFEST_VERSION_FIELD], |
| platform) |
| os.makedirs(component_folder) |
| component_zipfile = os.path.join(component_folder, COMPONENT_ZIP) |
| zf = zipfile.ZipFile(component_zipfile, 'w', zipfile.ZIP_DEFLATED) |
| # Move component files into zip file. |
| for f in files: |
| if os.path.isdir(f): |
| AddDirectoryToZip(zf, f) |
| else: |
| zf.write(f, os.path.basename(f)) |
| # Write manifest file into zip file. |
| zf.writestr(MANIFEST_FILE_NAME, json.dumps(data)) |
| logger.info('component is generated at %s', zf.filename) |
| zf.close() |
| |
| # Upload component to gs bucket. |
| if upload: |
| if '9999' in package_version: |
| cros_build_lib.Die('Cannot upload component while the %s package ' |
| 'is being worked on.', package_name) |
| UploadComponent(os.path.join(tempdir, data[MANIFEST_VERSION_FIELD]), |
| gsbucket) |
| |
| def GetCurrentPackageVersion(current_version_path, platform): |
| """Get package version of current component. |
| |
| Args: |
| current_version_path: (str) path to current version component. |
| platform: (str) platform name in omaha. |
| |
| Returns: |
| str: package version of current component. |
| """ |
| if current_version_path: |
| ctx = gs.GSContext() |
| src = os.path.join(current_version_path, platform, COMPONENT_ZIP) |
| if ctx.Exists(src): |
| with osutils.TempDir(prefix='component_') as tempdir: |
| ctx.Copy(src, tempdir) |
| cros_build_lib.RunCommand( |
| ['unzip', '-o', '-d', |
| tempdir, os.path.join(tempdir, COMPONENT_ZIP)], |
| redirect_stdout=True, redirect_stderr=True) |
| with open(os.path.join(tempdir, MANIFEST_FILE_NAME)) as f: |
| manifest = json.load(f) |
| if MANIFEST_PACKAGE_VERSION_FIELD in manifest: |
| return manifest[MANIFEST_PACKAGE_VERSION_FIELD] |
| return '0.0.0.0' |
| |
| def FixPackageVersion(version): |
| """Fix version to the format of X.Y.Z-rN |
| |
| Package name in ebuild is in the format of (X){1,3}-rN, we convert it |
| to X.Y.Z-rN by padding 0 to Z (and Y). |
| This function is added because a package like arc++ has version numbers |
| (X)-rN which is not consistent with the rest of the packages. |
| |
| Args: |
| version: (str) version to format. |
| |
| Returns: |
| str: fixed version. |
| Or None: if version is not fixable. |
| """ |
| pattern = re.compile('([0-9]+)(\\.[0-9]+)?(\\.[0-9]+)?(-r[0-9]+)?$') |
| m = pattern.match(version) |
| if m is None or m.group(1) is None: |
| logger.info('version %s is in wrong format.', version) |
| return None |
| version = m.group(1) |
| for i in range(2, 4): |
| version = (version + '.0') if m.group(i) is None else (version + m.group(i)) |
| if m.group(4) is not None: |
| version += m.group(4) |
| return version |
| |
| def GetPackageVersion(folder_name, package_name): |
| """Get the version of the package. |
| |
| It checks if the folder is for the package. If yes, return the version of the |
| package. |
| |
| Args: |
| folder_name: (str) name of the folder. |
| package_name: (str) name of the package. |
| |
| Returns: |
| str: fixed version. |
| """ |
| pattern = re.compile('(^[\\w-]*)-[0-9]+(\\.[0-9]+){0,2}(-r[0-9]+)?$') |
| m = pattern.match(folder_name) |
| if m is not None and m.group(1) == package_name: |
| return FixPackageVersion(folder_name[len(package_name)+1:]) |
| return None |
| |
| def BuildComponent(component_to_build, components, board, platform, |
| gsbucket_override=None, upload=False): |
| """Build a component. |
| |
| Args: |
| component_to_build: (str) component to build. |
| components: ([object]) a list of components. |
| board: (str) board to build the component on. |
| platform: (str) platform name in omaha. |
| gsbucket_override: (str) gsbucket value to override in components if not |
| None. |
| upload: (bool) True if uploading to Omaha; False if not uploading to Omaha. |
| """ |
| for component in components: |
| for name, metadata in component.iteritems(): |
| if name == component_to_build: |
| if not CheckValidMetadata(metadata): |
| continue |
| if (metadata.get('valid_platforms') and |
| not platform in metadata['valid_platforms']): |
| cros_build_lib.Die('Invalid platform') |
| logger.info('build component:%s', name) |
| # Check if component files are built successfully. |
| files = [os.path.join(cros_build_lib.GetSysroot(), 'build', board, x) \ |
| for x in metadata["files"]] |
| if not CheckComponentFilesExistence(files): |
| cros_build_lib.Die('component files are missing.') |
| |
| # Check release versions on gs. |
| if gsbucket_override is not None: |
| gsbucket = gsbucket_override |
| else: |
| gsbucket = metadata['gsbucket'] |
| logger.info('Use %s gsbucket for component.', gsbucket) |
| dirs = CheckGsBucket(gsbucket) |
| logger.info('Dirs in gsbucket:%s', dirs) |
| current_version, current_version_path = GetCurrentVersion(dirs, |
| platform) |
| logger.info('latest component version on Omaha gs: %s', current_version) |
| # Get package version of current component. |
| current_package_version = GetCurrentPackageVersion(current_version_path, |
| platform) |
| |
| # Check component (gentoo package) version. |
| package_name = metadata["package_name"] |
| for f in os.listdir(os.path.join(cros_build_lib.GetSysroot(), 'build', |
| board, metadata["pkgpath"])): |
| package_version = GetPackageVersion(f, package_name) |
| if package_version is not None: |
| logger.info('current package version: %s', package_version) |
| logger.info('package version of current component: %s', |
| current_package_version) |
| version = DecideVersion(package_version, current_version) |
| logger.info('next component version on Omaha gs: %s', version) |
| |
| manifest_path = os.path.join(cros_build_lib.GetSysroot(), 'build', |
| board, metadata["manifest"]) |
| |
| CreateComponent(manifest_path, version, package_name, |
| package_version, platform, files, upload, gsbucket) |
| return |
| cros_build_lib.Die('Package could not be found, component could not be' |
| 'built.') |
| |
| |
| def GetComponentsToBuild(path): |
| """Parse components from config file. |
| |
| Args: |
| path: (str) file path to config file. |
| |
| Returns: |
| Object: a json object of config file content. |
| """ |
| with open(path) as f: |
| return json.load(f) |
| |
| |
| def main(argv): |
| opts = GetParser().parse_args(argv) |
| BuildComponent(component_to_build=opts.component, |
| components=GetComponentsToBuild(opts.config_path), |
| board=opts.board, |
| platform=opts.platform, |
| gsbucket_override=opts.gsbucket, |
| upload=opts.upload) |
| |
| if __name__ == '__main__': |
| commandline.ScriptWrapperMain(lambda _: main) |