Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # pylint: disable=line-too-long, subprocess-run-check, unused-argument |
| 3 | # Copyright 2021 The Chromium OS Authors. All rights reserved. |
| 4 | # Use of this source code is governed by a BSD-style license that can be |
| 5 | # found in the LICENSE file. |
| 6 | """Convert Cargo metadata to ebuilds using cros-rust. |
| 7 | |
| 8 | Used to add new Rust projects and building up ebuilds for the dependency tree. |
| 9 | """ |
| 10 | |
| 11 | import argparse |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 12 | from collections import defaultdict |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 13 | from datetime import datetime |
| 14 | import json |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 15 | import logging |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 16 | import os |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 17 | from pprint import pprint |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 18 | import re |
| 19 | import shutil |
| 20 | import subprocess |
| 21 | import sys |
| 22 | |
| 23 | SCRIPT_NAME = 'cargo2ebuild.py' |
| 24 | AUTOGEN_NOTICE = '\n# This file was automatically generated by {}'.format( |
| 25 | SCRIPT_NAME) |
| 26 | |
| 27 | CRATE_DL_URI = 'https://crates.io/api/v1/crates/{PN}/{PV}/download' |
| 28 | |
| 29 | # Required parameters |
| 30 | # copyright_year: Current year for copyright assignment. |
| 31 | # description: Description of the crates. |
| 32 | # homepage: Homepage of the crates. |
| 33 | # license: Ebuild compatible license string. |
| 34 | # dependencies: Ebuild compatible dependency string. |
| 35 | # autogen_notice: Autogenerated notification string. |
Allen Webb | 81a58ba | 2021-05-03 10:53:17 -0500 | [diff] [blame] | 36 | EBUILD_TEMPLATE = ( |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 37 | """# Copyright {copyright_year} The Chromium OS Authors. All rights reserved. |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 38 | # Distributed under the terms of the GNU General Public License v2 |
| 39 | |
| 40 | EAPI="7" |
| 41 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 42 | CROS_RUST_REMOVE_DEV_DEPS=1{remove_target_var} |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 43 | |
| 44 | inherit cros-rust |
| 45 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 46 | DESCRIPTION='{description}' |
| 47 | HOMEPAGE='{homepage}' |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 48 | SRC_URI="https://crates.io/api/v1/crates/${{PN}}/${{PV}}/download -> ${{P}}.crate" |
| 49 | |
| 50 | LICENSE="{license}" |
| 51 | SLOT="${{PV}}/${{PR}}" |
| 52 | KEYWORDS="*" |
| 53 | |
| 54 | {dependencies}{autogen_notice} |
Allen Webb | 81a58ba | 2021-05-03 10:53:17 -0500 | [diff] [blame] | 55 | """) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 56 | |
| 57 | # Required parameters: |
| 58 | # copyright_year: Current year for copyright assignment. |
| 59 | # crate_features: Features to add to this empty crate. |
| 60 | # autogen_notice: Autogenerated notification string. |
Allen Webb | 81a58ba | 2021-05-03 10:53:17 -0500 | [diff] [blame] | 61 | EMPTY_CRATE = ( |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 62 | """# Copyright {copyright_year} The Chromium OS Authors. All rights reserved. |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 63 | # Distributed under the terms of the GNU General Public License v2 |
| 64 | |
| 65 | EAPI="7" |
| 66 | |
| 67 | CROS_RUST_EMPTY_CRATE=1 |
| 68 | {crate_features} |
| 69 | inherit cros-rust |
| 70 | |
| 71 | DESCRIPTION="Empty crate" |
| 72 | HOMEPAGE="" |
| 73 | |
| 74 | LICENSE="BSD-Google" |
| 75 | SLOT="${{PV}}/${{PR}}" |
| 76 | KEYWORDS="*" |
| 77 | {autogen_notice} |
Allen Webb | 81a58ba | 2021-05-03 10:53:17 -0500 | [diff] [blame] | 78 | """) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 79 | |
| 80 | LICENSES = { |
| 81 | 'Apache-2.0': 'Apache-2.0', |
| 82 | 'MIT': 'MIT', |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 83 | 'BSD-2-Clause': 'BSD-2', |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 84 | 'BSD-3-Clause': 'BSD', |
| 85 | '0BSD': '0BSD', |
| 86 | 'ISC': 'ISC', |
| 87 | } |
| 88 | |
| 89 | VERSION_RE = ( |
| 90 | '^(?P<dep>[\\^~=])?' # Dependency type: ^, ~, = |
| 91 | '(?P<major>[0-9]+|[*])' # Major version (can be *) |
| 92 | '(.(?P<minor>[0-9]+|[*]))?' # Minor version |
| 93 | '(.(?P<patch>[0-9]+|[*]))?' # Patch version |
| 94 | '([+\\-].*)?$' # Any semver values beyond patch version |
| 95 | ) |
| 96 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 97 | EBUILD_RE = ( |
| 98 | '^(?P<name>[-a-zA-Z0-9_]+)?' # Ebuild name |
| 99 | '-(?P<version>[0-9]+(.[0-9]+)?(.[0-9]+)?)' |
| 100 | '([-_].*)?\\.ebuild$' # Any semver values beyond patch version |
| 101 | ) |
| 102 | |
| 103 | FEATURES_RE = ( |
| 104 | '^CROS_RUST_EMPTY_CRATE_FEATURES=\\(' |
| 105 | '(?P<features>([^)]|$)*)' # Features array |
| 106 | '\\)' |
| 107 | ) |
| 108 | |
| 109 | DEP_GRAPH_ROOT = '*root*' |
| 110 | |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 111 | |
Allen Webb | 81a58ba | 2021-05-03 10:53:17 -0500 | [diff] [blame] | 112 | class VersionParseError(Exception): |
| 113 | """Error that is returned when parsing a version fails.""" |
| 114 | |
| 115 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 116 | class VersionRange: |
| 117 | """Represents a range of Rust crate versions.""" |
| 118 | @staticmethod |
| 119 | def from_str(value): |
| 120 | """Generate a VersionRange from a crate requirement string.""" |
| 121 | if value == '*': |
| 122 | return VersionRange(v_min=(0, 0, 0)) |
| 123 | |
| 124 | v_min = None |
| 125 | v_min_inclusive = True |
| 126 | v_max = None |
| 127 | v_max_inclusive = False |
| 128 | |
| 129 | # Handle a pair of constraints. |
| 130 | parts = value.split(', ') |
| 131 | if len(parts) == 2: |
| 132 | for part in parts: |
| 133 | if part.startswith('>='): |
| 134 | dep, major, minor, patch = version_to_tuple(part[2:], missing=0) |
| 135 | v_min = (major, minor, patch) |
| 136 | elif part.startswith('<='): |
| 137 | v_max_inclusive = True |
| 138 | dep, major, minor, patch = version_to_tuple(part[2:], missing=0) |
| 139 | v_max = (major, minor, patch) |
| 140 | elif part.startswith('>'): |
| 141 | v_min_inclusive = False |
| 142 | dep, major, minor, patch = version_to_tuple(part[1:], missing=0) |
| 143 | v_min = (major, minor, patch) |
| 144 | elif part.startswith('<'): |
| 145 | dep, major, minor, patch = version_to_tuple(part[1:], missing=0) |
| 146 | v_max = (major, minor, patch) |
| 147 | if v_max and v_min and v_max < v_min: |
| 148 | raise VersionParseError |
| 149 | # We are not going to worry about more than two constraints until we see it. |
| 150 | elif len(parts) != 1: |
| 151 | raise VersionParseError( |
| 152 | 'Version constraint has more than two parts: {}'.format(parts)) |
| 153 | # Handle the typical case of a single constraint operator. |
| 154 | else: |
| 155 | dep, major, minor, patch = version_to_tuple(value, missing=-1) |
| 156 | v_min = (major, minor, patch) |
| 157 | |
| 158 | if dep == '=': |
| 159 | v_max = v_min |
| 160 | v_max_inclusive = True |
| 161 | elif dep == '~': |
| 162 | major = max(v_min[0], 0) |
| 163 | minor = max(v_min[1], 0) |
| 164 | patch = max(v_min[2], 0) |
| 165 | if v_min[0] < 0: |
| 166 | raise VersionParseError( |
| 167 | 'The ~ constraint operator requires a major version: {}'.format(value)) |
| 168 | if v_min[1] < 0: |
| 169 | v_max = (major + 1, 0, 0) |
| 170 | elif v_min[2] < 0: |
| 171 | v_max = (0, minor + 1, 0) |
| 172 | else: |
| 173 | v_max_inclusive = True |
| 174 | v_max = (major, minor, patch) |
| 175 | v_min = (major, minor, patch) |
| 176 | elif dep and dep != '^': |
| 177 | raise VersionParseError('Unrecognized operator: "{}"'.format(dep)) |
| 178 | else: |
| 179 | major = max(v_min[0], 0) |
| 180 | minor = max(v_min[1], 0) |
| 181 | patch = max(v_min[2], 0) |
| 182 | |
| 183 | v_min = (major, minor, patch) |
| 184 | if major > 0: |
| 185 | v_max = (major + 1, 0, 0) |
| 186 | elif minor > 0: |
| 187 | v_max = (0, minor + 1, 0) |
| 188 | else: |
| 189 | v_max_inclusive = True |
| 190 | v_max = v_min |
| 191 | |
| 192 | return VersionRange(v_min=v_min, v_min_inclusive=v_min_inclusive, v_max=v_max, |
| 193 | v_max_inclusive=v_max_inclusive) |
| 194 | |
| 195 | def __init__(self, v_min=None, v_min_inclusive=True, v_max=None, v_max_inclusive=False): |
| 196 | self.min = v_min |
| 197 | self.min_inclusive = v_min_inclusive |
| 198 | self.max = v_max |
| 199 | self.max_inclusive = v_max_inclusive |
| 200 | |
| 201 | def contains(self, version): |
| 202 | """Returns `True` if version is inside the range; `False` otherwise. |
| 203 | |
| 204 | Args: |
| 205 | version: Can be either a tuple of integers (major, minor, patch) or a string that will be converted to a |
| 206 | tuple of integers. Trailing characters will be removed from the version (e.g. "+openssl-1.0.1"). |
| 207 | """ |
| 208 | if isinstance(version, str): |
| 209 | try: |
| 210 | filtered_version = re.sub(r'([0-9]+\.[0-9]+\.[0-9]+)[^0-9].*$', r'\1', version) |
| 211 | if filtered_version != version: |
| 212 | logging.warning('Filtered version "%s" to "%s"', version, filtered_version) |
| 213 | version = tuple([int(a) for a in filtered_version.split('.')]) |
| 214 | except Exception as e: |
| 215 | print(version) |
| 216 | raise e |
| 217 | |
| 218 | if self.min: |
| 219 | if self.min_inclusive: |
| 220 | if self.min > version: |
| 221 | return False |
| 222 | elif self.min >= version: |
| 223 | return False |
| 224 | if self.max: |
| 225 | if self.max_inclusive: |
| 226 | if self.max < version: |
| 227 | return False |
| 228 | elif self.max <= version: |
| 229 | return False |
| 230 | return True |
| 231 | |
| 232 | def to_ebuild_str(self, category_and_name): |
| 233 | """Return an ebuild DEPEND entry equivalent to the range. |
| 234 | |
| 235 | Args: |
| 236 | category_and_name: A string of the form 'dev-rust/crate-name' where 'dev-rust' is the category and |
| 237 | 'crate-name' is the name of the ebuild. |
| 238 | |
| 239 | Returns: |
| 240 | A string that can be used in an ebuild's DEPEND field. |
| 241 | """ |
| 242 | if not self.min or self.min == (0, 0, 0) and not self.max: |
| 243 | return '{}:='.format(category_and_name) |
| 244 | |
| 245 | min_bound = '>=' if self.min_inclusive else '>' |
| 246 | max_bound = '<=' if self.max_inclusive else '<' |
| 247 | if not self.max: |
| 248 | return '{}{}-{}.{}.{}:='.format(min_bound, category_and_name, self.min[0], self.min[1], |
| 249 | self.min[2]) |
| 250 | |
| 251 | for x in range(len(self.min)): |
| 252 | if self.min[x] != self.max[x]: |
| 253 | break |
| 254 | else: |
| 255 | # We want to allow revisions other than -r0, so use `~` instead of `=`. |
| 256 | return '~{}-{}.{}.{}:='.format(category_and_name, self.min[0], self.min[1], |
| 257 | self.min[2]) |
| 258 | |
| 259 | if self.min_inclusive and not self.max_inclusive and self.min[x] + 1 == self.max[x]: |
| 260 | if x == 0 and self.min[1] == 0 and self.min[2] == 0: |
| 261 | return '={}-{}*:='.format(category_and_name, self.min[0]) |
| 262 | if x == 1 and self.min[2] == 0: |
| 263 | return '={}-{}.{}*:='.format(category_and_name, self.min[0], self.min[1]) |
| 264 | |
| 265 | return '{0}{1}-{2}.{3}.{4}:= {5}{1}-{6}.{7}.{8}'.format( |
| 266 | min_bound, category_and_name, self.min[0], self.min[1], self.min[2], |
| 267 | max_bound, self.max[0], self.max[1], self.max[2]) |
| 268 | |
| 269 | |
| 270 | class DepGraphNode: |
| 271 | """A node in the dep-graph for a specific version of a package used by DepGraph.""" |
| 272 | def __init__(self, package): |
| 273 | self.package = package |
| 274 | self.dependencies = [] |
| 275 | |
| 276 | def __repr__(self): |
| 277 | return '{}'.format([(a['name'], a['req']) for a in self.dependencies]) |
| 278 | |
| 279 | def add_dependency(self, dep): |
| 280 | """Adds a dependency to the list.""" |
| 281 | self.dependencies.append(dep) |
| 282 | |
| 283 | |
| 284 | class DepGraph: |
| 285 | """A model of the dependency relationships between crates. |
| 286 | |
| 287 | The functionality this provides over using the Cargo metadata directly is: |
| 288 | * Filtering based on cros-rust.eclass features. |
| 289 | * Tracking of system ebuilds for detecting edge cases such as: |
| 290 | * Already fulfilled dependencies. |
| 291 | * Optional crates that would replace or supersede non-empty ebuilds. |
| 292 | * Empty crates that are missing definitions for required features. |
| 293 | """ |
| 294 | def __init__(self): |
| 295 | # name -> version -> DepGraphNode |
| 296 | self.graph = defaultdict(dict) |
| 297 | |
| 298 | # name -> version -> None or [features] |
| 299 | self.system_crates = defaultdict(dict) |
| 300 | |
| 301 | # The crate that will be initially checked when flattening the depgraph into a list of required and optional |
| 302 | # crates. |
| 303 | self.root = None |
| 304 | |
| 305 | def add_package(self, package): |
| 306 | """Add a crate to the dependency graph from a parsed metadata.json generated by cargo. |
| 307 | |
| 308 | Args: |
| 309 | package: A parsed package from the Cargo metadata. |
| 310 | |
| 311 | Returns: |
| 312 | filter_target: True if CROS_RUST_REMOVE_TARGET_CFG=1 should be set in the ebuild. |
| 313 | deps: A list of dependencies from the Cargo metadata JSON that still apply after filtering. |
| 314 | """ |
| 315 | dependencies = package['dependencies'] |
| 316 | |
| 317 | # Check if there are target specific dependencies and if so if they can be handled by |
| 318 | # CROS_RUST_REMOVE_TARGET_CFG. This requires that there not be complicated cfg blocks since the cros-rust eclass |
| 319 | # only applies simplistic filtering. |
| 320 | has_filterable_target = False |
| 321 | has_unfilterable_target = False |
| 322 | for dep in dependencies: |
| 323 | # Skip all dev dependencies. We will still handle normal and build deps. |
| 324 | if dep.get('kind', None) == 'dev': |
| 325 | continue |
| 326 | |
| 327 | target = dep.get('target', None) |
| 328 | if target is None: |
| 329 | continue |
| 330 | |
| 331 | if target_applies_to_chromeos(target): |
| 332 | continue |
| 333 | |
| 334 | if not target_is_filterable(target): |
| 335 | logging.debug('target "%s" is unfilterable.', target) |
| 336 | has_unfilterable_target = True |
| 337 | else: |
| 338 | has_filterable_target = True |
| 339 | if has_unfilterable_target: |
| 340 | logging.warning('"%s-%s" might benefit from better target specific dependency filtering', |
| 341 | package['name'], package['version']) |
| 342 | filter_target = has_filterable_target and not has_unfilterable_target |
| 343 | |
| 344 | graph_node = DepGraphNode(package) |
| 345 | self.graph[package['name']][package['version']] = graph_node |
| 346 | |
| 347 | # Check each dependency and add it if it is relevant to Chrome OS. |
| 348 | deps = [] |
| 349 | for dep in dependencies: |
| 350 | # Skip all dev dependencies. We will still handle normal and build deps. |
| 351 | if dep.get('kind', None) == 'dev': |
| 352 | continue |
| 353 | |
| 354 | # Skip filterable configuration dependant dependencies. |
| 355 | target = dep.get('target', None) |
| 356 | if filter_target and target is not None and not target_applies_to_chromeos(target): |
| 357 | continue |
| 358 | |
| 359 | graph_node.add_dependency(dep) |
| 360 | deps.append(dep) |
| 361 | return filter_target, deps |
| 362 | |
| 363 | def find_system_crates(self, target_dir, names=None): |
| 364 | """Take note of crates already present in the target directory. |
| 365 | |
| 366 | Args: |
| 367 | target_dir: The overlay directory where the ebuilds exist. |
| 368 | names: A list of package names to check. If unspecified all the crates present in the DepGraph are checked. |
| 369 | """ |
| 370 | if names is None: |
| 371 | names = self.graph |
| 372 | |
| 373 | for name in names: |
| 374 | # Skip if we have already done this crate. |
| 375 | if name in self.system_crates: |
| 376 | continue |
| 377 | available = self.system_crates[name] |
| 378 | |
| 379 | # Check if an empty package already has a non-empty ebuild. |
| 380 | ebuild_target_dir = get_ebuild_dir(name, target_dir) |
| 381 | |
| 382 | # Find all ebuilds in ebuild_target dir and if they have |
| 383 | # CROS_RUST_EMPTY_CRATE in them, check if features are set. |
| 384 | for root, _, files in os.walk(ebuild_target_dir): |
| 385 | files.sort() |
| 386 | for efile in files: |
| 387 | if not efile.endswith('.ebuild'): |
| 388 | continue |
| 389 | efile_path = os.path.join(root, efile) |
| 390 | |
| 391 | m = re.match(EBUILD_RE, efile) |
| 392 | if not m or m.group('name') != name: |
| 393 | logging.warning("Found misplaced ebuild: '%s'.", efile_path) |
| 394 | continue |
| 395 | |
| 396 | version = m.group('version') |
| 397 | |
| 398 | with open(efile_path, 'r') as ebuild: |
| 399 | contents = ebuild.read() |
| 400 | if 'CROS_RUST_EMPTY_CRATE' not in contents: |
| 401 | features = None |
| 402 | else: |
| 403 | m = re.search(FEATURES_RE, contents, re.MULTILINE) |
| 404 | if m: |
| 405 | sanitized = re.sub('[^a-zA-Z0-9_ \t\n-]+', '', m.group('features')) |
| 406 | features = set(a.strip() for a in sanitized.split() if a) |
| 407 | else: |
| 408 | features = set() |
| 409 | logging.debug("Found ebuild: '%s'.", efile_path[len(target_dir):]) |
| 410 | available[version] = features |
| 411 | |
| 412 | def set_root(self, package): |
| 413 | """Set the root of the dependency graph used when flattening the graph into a list of dependencies.""" |
| 414 | self.root = package |
| 415 | |
| 416 | def resolve_version_range(self, name, version_range): |
| 417 | """Find a dependency graph entry that fits the constraints. |
| 418 | |
| 419 | Optional dependencies often are not found, so the minimum version in version_range is returned in those cases. |
| 420 | |
| 421 | Args: |
| 422 | name: crate name |
| 423 | version_range: A VersionRange used to match against crate versions. |
| 424 | |
| 425 | Returns: |
| 426 | version: Returns the version of the match or the minimum matching version if no match is found. |
| 427 | node: None if no match is found otherwise returns the DepGraphNode of the match. |
| 428 | """ |
| 429 | for version, node in self.graph[name].items(): |
| 430 | if version_range.contains(version): |
| 431 | return version, node |
| 432 | if not version_range.min: |
| 433 | return '0.0.0', None |
| 434 | return '.'.join(map(str, version_range.min)), None |
| 435 | |
| 436 | def check_dependencies(self, args, package, features, optional, features_only=False): |
| 437 | """Check the dependencies of a specified package with the specified features enabled. |
| 438 | |
| 439 | Args: |
| 440 | args: The command line flags set for cargo2ebuild. |
| 441 | package: A parsed package from the Cargo metadata. |
| 442 | features: A set containing the enabled features for this package. This will be expanded by the function to |
| 443 | include any additional features implied by the original set of features (e.g. if default is set, all the |
| 444 | default features will be added). |
| 445 | optional: A dictionary of sets to which any new optional dependencies are added. It has the format: |
| 446 | (package_name, package_version): set() |
| 447 | where the set contains the required features. |
| 448 | features_only: If True, this function only checks the optional dependencies for ones that are enabled if |
| 449 | the features specified by `features` are enabled. |
| 450 | |
| 451 | Returns: |
| 452 | A list of (package_name, package_version) that are required by the specified package with the specified |
| 453 | features enabled. |
| 454 | """ |
| 455 | name = package['name'] |
| 456 | version = package['version'] |
| 457 | node = self.graph[name][version] |
| 458 | |
| 459 | enabled_optional_deps = get_feature_dependencies(package, features) |
| 460 | |
| 461 | new_required = [] |
| 462 | for dep in node.dependencies: |
| 463 | dep_name = dep['name'] |
| 464 | if features_only and dep_name not in enabled_optional_deps: |
| 465 | continue |
| 466 | req = VersionRange.from_str(dep['req']) |
| 467 | dep_version, dep_node = self.resolve_version_range(dep_name, req) |
| 468 | identifier = (dep_name, dep_version) |
| 469 | |
| 470 | dep_features = set(dep['features']) |
| 471 | if dep_node and dep.get('uses_default_features', False): |
| 472 | dep_features.add('default') |
| 473 | |
| 474 | is_required = not dep.get('optional', False) or dep_name in enabled_optional_deps |
| 475 | if dep_name in enabled_optional_deps: |
| 476 | if not features_only: |
| 477 | logging.info('Optional dependency required by feature: "%s-%s"', dep_name, dep_version) |
| 478 | dep_features.update(enabled_optional_deps[dep_name]) |
| 479 | |
| 480 | found = False |
| 481 | sys_version = None |
| 482 | repl_versions = [] |
| 483 | if dep_name not in self.system_crates: |
| 484 | self.find_system_crates(args.target_dir, names=[dep_name]) |
| 485 | for sys_version, sys_features in self.system_crates[dep_name].items(): |
| 486 | sys_crate_is_empty = sys_features is not None |
| 487 | missing_features = dep_features.difference(sys_features) if sys_crate_is_empty else set() |
| 488 | if not args.overwrite_existing_ebuilds and sys_version == dep_version: |
| 489 | if sys_crate_is_empty: |
| 490 | if is_required: |
| 491 | logging.error('Empty crate "%s-%s" should be replaced with full version.', |
| 492 | dep_name, dep_version) |
| 493 | elif missing_features: |
| 494 | logging.error('Empty crate "%s-%s" has missing features: %s.', |
| 495 | dep_name, dep_version, missing_features) |
| 496 | found = True |
| 497 | break |
| 498 | if not sys_crate_is_empty and req.contains(sys_version): |
| 499 | if not args.no_substitute or (not is_required and dep_node is None): |
| 500 | found = True |
| 501 | break |
| 502 | if VersionRange.from_str('^{}'.format(sys_version)).contains(dep_version): |
| 503 | if sys_crate_is_empty: |
| 504 | dep_features.update(sys_features) |
| 505 | else: |
| 506 | repl_versions.append(sys_version) |
| 507 | if found and not features_only: |
| 508 | logging.debug('Using system crate: "%s-%s".', dep_name, sys_version) |
| 509 | continue |
| 510 | |
| 511 | if is_required: |
| 512 | if dep_node is None: |
| 513 | logging.error('Required crate "%s" "%s" not in dep-graph. It will be omitted.', |
| 514 | dep_name, dep['req']) |
| 515 | else: |
| 516 | logging.debug('New crate required: "%s-%s".', dep_name, dep_version) |
| 517 | new_required.append((dep_node.package, dep_features)) |
| 518 | else: |
| 519 | # If the empty crate would replace a non-empty version of the same crate, treat it as required. |
| 520 | if repl_versions: |
| 521 | if dep_node is not None: |
| 522 | logging.info('Empty crate for "%s-%s" would replace non-empty versions %s. ' |
| 523 | 'Upgrading to non-empty.', dep_name, dep_version, repl_versions) |
| 524 | new_required.append((dep_node.package, dep_features)) |
| 525 | else: |
| 526 | logging.error('Required crate "%s-%s" not in metadata. Fix: add it to Cargo.toml', |
| 527 | dep_name, dep_version) |
| 528 | else: |
| 529 | logging.debug('New optional crate required: "%s-%s".', dep_name, dep_version) |
| 530 | # Include all features supported by the crate. |
| 531 | if dep_node is not None: |
| 532 | dep_features = dep_node.package['features'].keys() |
| 533 | optional[identifier].update(dep_features if features else ()) |
| 534 | return new_required |
| 535 | |
| 536 | def flatten(self, args): |
| 537 | """Flatten the dependency graph into a list of required and optional crates. |
| 538 | |
| 539 | Returns: |
| 540 | Two dictionaries that map tuples of package names and versions to sets of features: |
| 541 | (name, version) -> set(features) |
| 542 | The first dictionary contains the required crates with their required features, while the second contains |
| 543 | the optional crates, versions, and their features. |
| 544 | """ |
| 545 | # (name, version) -> set(features) |
| 546 | required = defaultdict(set) |
| 547 | optional = defaultdict(set) |
| 548 | remaining = [(self.root, {'default'})] |
| 549 | while remaining: |
| 550 | to_check, features = remaining.pop(0) |
| 551 | name = to_check['name'] |
| 552 | version = to_check['version'] |
| 553 | |
| 554 | identifier = (name, version) |
| 555 | if identifier in required: |
| 556 | features_to_enable = features.difference(required[identifier]) |
| 557 | if not features_to_enable: |
| 558 | continue |
| 559 | logging.debug('Extending dependencies for "%s-%s" to include features: "%s"', |
| 560 | name, version, features_to_enable) |
| 561 | remaining.extend(self.check_dependencies(args, to_check, features_to_enable, optional, |
| 562 | features_only=True)) |
| 563 | else: |
| 564 | logging.debug('Resolving dependencies for "%s-%s".', name, version) |
| 565 | remaining.extend(self.check_dependencies(args, to_check, features, optional)) |
| 566 | required[identifier].update(features) |
| 567 | |
| 568 | return dict(required), {a: b for a, b in optional.items() if a not in required} |
| 569 | |
| 570 | |
| 571 | def target_applies_to_chromeos(target): |
| 572 | """Return true if the target would be accepted by the cros-rust eclass.""" |
| 573 | return ( |
| 574 | target.startswith('cfg(unix') or |
| 575 | target.startswith('cfg(linux') or |
| 576 | target.startswith('cfg(not(windows)') or |
| 577 | '-linux-gnu' in target |
| 578 | ) |
| 579 | |
| 580 | |
| 581 | def target_is_filterable(target): |
| 582 | """Checks to see if the cros-rust eclass would probably handle this target properly. |
| 583 | |
| 584 | Returns: |
| 585 | False if the Cargo.toml target configuration block would not be accepted by the cros-rust eclass, but is probably needed. |
| 586 | """ |
| 587 | if target_applies_to_chromeos(target): |
| 588 | return True |
| 589 | if 'unix' in target or 'linux' in target or 'not(' in target: |
| 590 | return False |
| 591 | return True |
| 592 | |
| 593 | |
| 594 | def get_feature_dependencies(package, features): |
| 595 | """Expand the features set to include implied features and return the enabled optional dependencies. |
| 596 | |
| 597 | Args: |
| 598 | package: A parsed package from the Cargo metadata. |
| 599 | features: A set() of features that is expanded to include any additional dependencies implied by the original |
| 600 | set of features. |
| 601 | |
| 602 | Returns: |
| 603 | A dictionary that maps crate names to the required features for the specific crate. |
| 604 | Note: that no version is supplied; it must be obtained by finding the named dependency in the package's |
| 605 | optional dependency requirements. |
| 606 | """ |
| 607 | feature_list = list(features) |
| 608 | enabled_deps = defaultdict(set) |
| 609 | lookup = package.get('features', {}) |
| 610 | for feature in feature_list: |
| 611 | if feature not in lookup: |
| 612 | if feature != 'default': |
| 613 | logging.error('Requested feature "%s" not listed in crate "%s".', |
| 614 | feature, package['name']) |
| 615 | continue |
| 616 | for dep in lookup[feature]: |
| 617 | # Check if it is a feature instead of a dependency. |
| 618 | if dep in lookup: |
| 619 | if dep not in features: |
| 620 | feature_list.append(dep) |
| 621 | features.add(dep) |
| 622 | continue |
| 623 | parts = dep.split('/') |
| 624 | |
| 625 | # This creates an empty set if there is not one already. |
| 626 | entry = enabled_deps[parts[0]] |
| 627 | if len(parts) > 1: |
| 628 | entry.add(parts[1]) |
| 629 | return dict(enabled_deps) |
| 630 | |
| 631 | |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 632 | def prepare_staging(args): |
| 633 | """Prepare staging directory.""" |
| 634 | sdir = args.staging_dir |
| 635 | dirs = [ |
| 636 | os.path.join(sdir, 'ebuild', 'dev-rust'), |
| 637 | os.path.join(sdir, 'crates') |
| 638 | ] |
| 639 | for d in dirs: |
| 640 | os.makedirs(d, exist_ok=True) |
| 641 | |
| 642 | |
| 643 | def load_metadata(manifest_path): |
| 644 | """Run cargo metadata and get metadata for build.""" |
| 645 | cwd = os.path.dirname(manifest_path) |
| 646 | cmd = [ |
| 647 | 'cargo', 'metadata', '--format-version', '1', '--manifest-path', |
| 648 | manifest_path |
| 649 | ] |
| 650 | output = subprocess.check_output(cmd, cwd=cwd) |
| 651 | |
| 652 | return json.loads(output) |
| 653 | |
| 654 | |
| 655 | def get_crate_path(package, staging_dir): |
| 656 | """Get path to crate in staging directory.""" |
| 657 | return os.path.join( |
| 658 | staging_dir, 'crates', '{}-{}.crate'.format(package['name'], |
| 659 | package['version'])) |
| 660 | |
| 661 | |
| 662 | def get_clean_crate_name(package): |
| 663 | """Clean up crate name to {name}-{major}.{minor}.{patch}.""" |
| 664 | return '{}-{}.crate'.format(package['name'], |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 665 | get_clean_package_version(package['version'])) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 666 | |
| 667 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 668 | def version_to_tuple(version, missing=-1): |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 669 | """Extract dependency type and semver from a given version string.""" |
| 670 | def version_to_int(num): |
| 671 | if not num or num == '*': |
| 672 | return missing |
| 673 | return int(num) |
| 674 | |
| 675 | m = re.match(VERSION_RE, version) |
| 676 | if not m: |
Allen Webb | 81a58ba | 2021-05-03 10:53:17 -0500 | [diff] [blame] | 677 | raise VersionParseError |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 678 | |
| 679 | dep = m.group('dep') |
| 680 | major = m.group('major') |
| 681 | minor = m.group('minor') |
| 682 | patch = m.group('patch') |
| 683 | |
| 684 | has_star = any([x == '*' for x in [major, minor, patch]]) |
| 685 | |
| 686 | major = version_to_int(major) |
| 687 | minor = version_to_int(minor) |
| 688 | patch = version_to_int(patch) |
| 689 | |
| 690 | if has_star: |
| 691 | dep = '~' |
| 692 | elif not dep: |
| 693 | dep = '^' |
| 694 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 695 | return dep, major, minor, patch |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 696 | |
| 697 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 698 | def get_clean_package_version(version): |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 699 | """Get package version in the format {major}.{minor}.{patch}.""" |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 700 | (_, major, minor, patch) = version_to_tuple(version, missing=0) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 701 | return '{}.{}.{}'.format(major, minor, patch) |
| 702 | |
| 703 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 704 | def get_ebuild_dir(name, staging_dir): |
| 705 | """Get the directory that contains specific ebuilds.""" |
| 706 | return os.path.join(staging_dir, 'dev-rust', name) |
| 707 | |
| 708 | |
| 709 | def get_ebuild_path(name, version, staging_dir, make_dir=False): |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 710 | """Get path to ebuild in given directory.""" |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 711 | ebuild_dir = get_ebuild_dir(name, staging_dir) |
| 712 | |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 713 | ebuild_path = os.path.join( |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 714 | ebuild_dir, |
| 715 | '{}-{}.ebuild'.format(name, get_clean_package_version(version))) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 716 | |
| 717 | if make_dir: |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 718 | os.makedirs(ebuild_dir, exist_ok=True) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 719 | |
| 720 | return ebuild_path |
| 721 | |
| 722 | |
| 723 | def download_package(package, staging_dir): |
| 724 | """Download the crate from crates.io.""" |
| 725 | dl_uri = CRATE_DL_URI.format(PN=package['name'], PV=package['version']) |
| 726 | crate_path = get_crate_path(package, staging_dir) |
| 727 | |
| 728 | # Already downloaded previously |
| 729 | if os.path.isfile(crate_path): |
| 730 | return |
| 731 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 732 | ret = subprocess.run( |
| 733 | ['curl', '-L', dl_uri, '-o', crate_path], |
| 734 | stdout=subprocess.DEVNULL, |
| 735 | stderr=subprocess.DEVNULL).returncode |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 736 | |
| 737 | if ret: |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 738 | logging.error('Failed to download "%s": %s', dl_uri, ret) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 739 | |
| 740 | |
| 741 | def get_description(package): |
| 742 | """Get a description of the crate from metadata.""" |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 743 | if package.get('description', None): |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 744 | desc = re.sub("[`']", '"', package['description']) |
| 745 | return desc.strip() |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 746 | |
| 747 | return '' |
| 748 | |
| 749 | |
| 750 | def get_homepage(package): |
| 751 | """Get the homepage of the crate from metadata or use crates.io.""" |
| 752 | if package.get('homepage', None): |
| 753 | return package['homepage'] |
| 754 | |
| 755 | return 'https://crates.io/crates/{}'.format(package['name']) |
| 756 | |
| 757 | |
| 758 | def convert_license(cargo_license, package): |
| 759 | """Convert licenses from cargo to a format usable in ebuilds.""" |
| 760 | cargo_license = '' if not cargo_license else cargo_license |
| 761 | has_or = ' OR ' in cargo_license |
| 762 | delim = ' OR ' if has_or else '/' |
| 763 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 764 | found = [a.strip() for a in cargo_license.split(delim) if a] |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 765 | licenses_or = [] |
| 766 | for f in found: |
| 767 | if f in LICENSES: |
| 768 | licenses_or.append(LICENSES[f]) |
| 769 | |
| 770 | if not licenses_or: |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 771 | logging.error('"%s" is missing an appropriate license: "%s"', package['name'], cargo_license) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 772 | return "$(die 'Please replace with appropriate license')" |
| 773 | |
| 774 | if len(licenses_or) > 1: |
| 775 | lstr = '|| ( {} )'.format(' '.join(licenses_or)) |
| 776 | else: |
| 777 | lstr = licenses_or[0] |
| 778 | |
| 779 | return lstr |
| 780 | |
| 781 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 782 | def convert_dependencies(dependencies, filter_target=False): |
| 783 | """Convert crate dependencies to ebuild dependencies.""" |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 784 | deps = [] |
| 785 | for dep in dependencies: |
Allen Webb | 81a58ba | 2021-05-03 10:53:17 -0500 | [diff] [blame] | 786 | # Convert version requirement to ebuild DEPEND. |
| 787 | try: |
| 788 | # Convert requirement to version tuple |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 789 | bounds = ( |
| 790 | VersionRange.from_str(dep['req']) |
| 791 | .to_ebuild_str('dev-rust/{}'.format(dep['name'])) |
| 792 | ) |
| 793 | deps.append('\t{}'.format(bounds)) |
Allen Webb | 81a58ba | 2021-05-03 10:53:17 -0500 | [diff] [blame] | 794 | except VersionParseError: |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 795 | logging.error('Failed to parse dep version for "%s": %s', |
| 796 | dep['name'], dep['req'], exc_info=True) |
Allen Webb | 81a58ba | 2021-05-03 10:53:17 -0500 | [diff] [blame] | 797 | # Rarely dependencies look something like ">=0.6, <0.8" |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 798 | deps.append("\t$(die 'Please replace with proper DEPEND: {} = {}')".format( |
| 799 | dep['name'], dep['req'])) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 800 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 801 | remove_target_var = '\nCROS_RUST_REMOVE_TARGET_CFG=1' if filter_target else '' |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 802 | if not deps: |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 803 | return '', remove_target_var |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 804 | |
| 805 | # Add DEPEND= into template with all dependencies |
| 806 | # RDEPEND="${DEPEND}" required for race in cros-rust |
| 807 | fmtstring = 'DEPEND="\n{}\n"\nRDEPEND="${{DEPEND}}"\n' |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 808 | return fmtstring.format('\n'.join(deps)), remove_target_var |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 809 | |
| 810 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 811 | def package_ebuild(package, ebuild_dir, crate_dependencies, filter_target): |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 812 | """Create ebuild from metadata and write to ebuild directory.""" |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 813 | logging.debug('Processing "%s-%s"', package['name'], package['version']) |
| 814 | ebuild_path = get_ebuild_path(package['name'], package['version'], ebuild_dir, make_dir=True) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 815 | |
| 816 | autogen_notice = AUTOGEN_NOTICE |
| 817 | |
| 818 | # Check if version matches clean version or modify the autogen notice |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 819 | if package['version'] != get_clean_package_version(package['version']): |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 820 | autogen_notice = '\n'.join([ |
| 821 | autogen_notice, |
| 822 | '# ${{PV}} was changed from the original {}'.format( |
| 823 | package['version']) |
| 824 | ]) |
| 825 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 826 | (ebuild_dependencies, remove_target_var) = convert_dependencies(crate_dependencies, filter_target) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 827 | template_info = { |
| 828 | 'copyright_year': datetime.now().year, |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 829 | 'remove_target_var': remove_target_var, |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 830 | 'description': get_description(package), |
| 831 | 'homepage': get_homepage(package), |
| 832 | 'license': convert_license(package['license'], package), |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 833 | 'dependencies': ebuild_dependencies, |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 834 | 'autogen_notice': autogen_notice, |
| 835 | } |
| 836 | |
| 837 | with open(ebuild_path, 'w') as ebuild: |
| 838 | ebuild.write(EBUILD_TEMPLATE.format(**template_info)) |
| 839 | |
| 840 | |
| 841 | def upload_gsutil(package, staging_dir, no_upload=False): |
| 842 | """Upload crate to distfiles.""" |
| 843 | if no_upload: |
| 844 | return |
| 845 | |
| 846 | crate_path = get_crate_path(package, staging_dir) |
| 847 | crate_name = get_clean_crate_name(package) |
| 848 | ret = subprocess.run([ |
| 849 | 'gsutil', 'cp', '-a', 'public-read', crate_path, |
| 850 | 'gs://chromeos-localmirror/distfiles/{}'.format(crate_name) |
| 851 | ]).returncode |
| 852 | |
| 853 | if ret: |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 854 | logging.error('Failed to upload "%s" to chromeos-localmirror: %s', crate_name, ret) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 855 | |
| 856 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 857 | def update_ebuild(package, args): |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 858 | """Update ebuild with generated one and generate MANIFEST.""" |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 859 | staging_dir = args.staging_dir |
| 860 | ebuild_dir = os.path.join(staging_dir, 'ebuild') |
| 861 | target_dir = args.target_dir |
| 862 | ebuild_src = get_ebuild_path(package['name'], package['version'], ebuild_dir) |
| 863 | ebuild_dest = get_ebuild_path(package['name'], package['version'], target_dir, make_dir=True) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 864 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 865 | # TODO make this aware of the revision numbers of existing versions |
| 866 | # when doing a replacement. |
| 867 | |
| 868 | # Do not overwrite existing ebuilds unless explicitly asked to. |
Allen Webb | 81a58ba | 2021-05-03 10:53:17 -0500 | [diff] [blame] | 869 | if args.overwrite_existing_ebuilds or not os.path.exists(ebuild_dest): |
| 870 | shutil.copy(ebuild_src, ebuild_dest) |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 871 | upload_gsutil(package, staging_dir, no_upload=args.no_upload) |
Allen Webb | 81a58ba | 2021-05-03 10:53:17 -0500 | [diff] [blame] | 872 | else: |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 873 | logging.info('ebuild %s already exists, skipping.', ebuild_dest) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 874 | |
| 875 | # Generate manifest w/ ebuild digest |
| 876 | ret = subprocess.run(['ebuild', ebuild_dest, 'digest']).returncode |
| 877 | if ret: |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 878 | logging.error('ebuild %s digest failed: %s', ebuild_dest, ret) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 879 | |
| 880 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 881 | def process_package(package, staging_dir, dep_graph): |
| 882 | """Process each package listed in the metadata. |
| 883 | |
| 884 | This includes following: |
| 885 | * Setting the package as the root of the dep graph if it is included by source (rather than from crates.io). |
| 886 | * Adding the package to the the dep_graph. |
| 887 | * Downloading the crate file, so it can be potentially uploaded to localmirror. |
| 888 | * Generating an ebuild for the crate in the staging directory. |
| 889 | """ |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 890 | ebuild_dir = os.path.join(staging_dir, 'ebuild') |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 891 | |
| 892 | # Add the user submitted package to the set of required packages. |
| 893 | if package.get('source', None) is None: |
| 894 | dep_graph.set_root(package) |
| 895 | filter_target, crate_dependencies = dep_graph.add_package(package) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 896 | |
| 897 | download_package(package, staging_dir) |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 898 | package_ebuild(package, ebuild_dir, crate_dependencies, filter_target) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 899 | |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 900 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 901 | def process_empty_package(name, version, features, args): |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 902 | """Process packages that should generate empty ebuilds.""" |
| 903 | staging_dir = args.staging_dir |
| 904 | ebuild_dir = os.path.join(staging_dir, 'ebuild') |
| 905 | target_dir = args.target_dir |
| 906 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 907 | ebuild_src = get_ebuild_path(name, version, ebuild_dir, make_dir=True) |
| 908 | ebuild_dest = get_ebuild_path(name, version, target_dir, make_dir=True) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 909 | |
| 910 | crate_features = '' |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 911 | if features: |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 912 | crate_features = 'CROS_RUST_EMPTY_CRATE_FEATURES=( {} )'.format( |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 913 | ' '.join(['"{}"'.format(x) for x in features])) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 914 | |
| 915 | template_info = { |
| 916 | 'copyright_year': datetime.now().year, |
| 917 | 'crate_features': crate_features, |
| 918 | 'autogen_notice': AUTOGEN_NOTICE, |
| 919 | } |
| 920 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 921 | logging.debug('Writing empty crate: %s', ebuild_src) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 922 | with open(ebuild_src, 'w') as ebuild: |
| 923 | ebuild.write(EMPTY_CRATE.format(**template_info)) |
| 924 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 925 | # Do not overwrite existing ebuilds unless explicitly asked to. |
| 926 | if not args.overwrite_existing_ebuilds and os.path.exists(ebuild_dest): |
| 927 | logging.info('ebuild %s already exists, skipping.', ebuild_dest) |
| 928 | return |
| 929 | |
| 930 | if not args.dry_run and name not in args.skip: |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 931 | shutil.copy(ebuild_src, ebuild_dest) |
| 932 | |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 933 | |
| 934 | def main(argv): |
| 935 | """Convert dependencies from Cargo.toml into ebuilds.""" |
| 936 | args = parse_args(argv) |
| 937 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 938 | logging_kwargs = {'stream': sys.stderr, 'format': '%(levelname)s: %(message)s'} |
| 939 | if args.verbose > 1: |
| 940 | logging_kwargs['level'] = logging.DEBUG |
| 941 | elif args.verbose > 0: |
| 942 | logging_kwargs['level'] = logging.INFO |
| 943 | else: |
| 944 | logging_kwargs['level'] = logging.WARNING |
| 945 | logging.basicConfig(**logging_kwargs) |
| 946 | |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 947 | prepare_staging(args) |
| 948 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 949 | dep_graph = DepGraph() |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 950 | metadata = load_metadata(args.manifest_path) |
| 951 | for p in metadata['packages']: |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 952 | process_package(p, args.staging_dir, dep_graph) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 953 | |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 954 | dep_graph.find_system_crates(args.target_dir) |
| 955 | |
| 956 | required_packages, optional_packages = dep_graph.flatten(args) |
| 957 | if args.verbose > 2: |
| 958 | print('Dependency graph:') |
| 959 | pprint(dict(dep_graph.graph)) |
| 960 | print('System versions:') |
| 961 | pprint(dict(dep_graph.system_crates)) |
| 962 | print('Required versions:') |
| 963 | pprint(required_packages) |
| 964 | print('Optional versions:') |
| 965 | pprint(optional_packages) |
| 966 | |
| 967 | for (name, version), features in optional_packages.items(): |
| 968 | process_empty_package(name, version, features, args) |
| 969 | |
| 970 | if not args.dry_run: |
| 971 | for p in metadata['packages']: |
| 972 | if p['name'] in args.skip: |
| 973 | continue |
| 974 | identifier = (p['name'], p['version']) |
| 975 | if identifier in required_packages: |
| 976 | update_ebuild(p, args) |
| 977 | display_dir = args.target_dir |
| 978 | else: |
| 979 | display_dir = '/'.join((args.staging_dir, 'ebuild')) |
| 980 | print('Generated ebuilds can be found in: {}'.format(display_dir)) |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 981 | |
| 982 | |
| 983 | def parse_args(argv): |
| 984 | """Parse the command-line arguments.""" |
| 985 | parser = argparse.ArgumentParser(description=__doc__) |
| 986 | parser.add_argument( |
| 987 | '-s', |
| 988 | '--staging-dir', |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 989 | default='/tmp/cargo2ebuild-staging', |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 990 | help='Staging directory for temporary and generated files') |
| 991 | |
| 992 | parser.add_argument( |
| 993 | '-d', |
| 994 | '--dry-run', |
| 995 | action='store_true', |
| 996 | help='Generate dependency tree but do not upload anything') |
| 997 | |
| 998 | parser.add_argument('-t', |
| 999 | '--target-dir', |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 1000 | default='/mnt/host/source/src/third_party/chromiumos-overlay', |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 1001 | help='Path to chromiumos-overlay') |
| 1002 | |
| 1003 | parser.add_argument('-k', |
| 1004 | '--skip', |
| 1005 | action='append', |
| 1006 | help='Skip these packages when updating ebuilds') |
| 1007 | parser.add_argument('-n', |
| 1008 | '--no-upload', |
| 1009 | action='store_true', |
| 1010 | help='Skip uploading crates to distfiles') |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 1011 | parser.add_argument('-X', |
| 1012 | '--no-substitute', |
| 1013 | action='store_true', |
| 1014 | help='Do not substitute system versions for required crates') |
Allen Webb | 81a58ba | 2021-05-03 10:53:17 -0500 | [diff] [blame] | 1015 | parser.add_argument('-x', |
| 1016 | '--overwrite-existing-ebuilds', |
| 1017 | action='store_true', |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 1018 | help='If an ebuild already exists, overwrite it.') |
| 1019 | parser.add_argument('-v', |
| 1020 | '--verbose', |
| 1021 | action='count', |
| 1022 | default=0, |
| 1023 | help='Enable verbose logging') |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 1024 | |
| 1025 | parser.add_argument('manifest_path', |
Allen Webb | 2e0d565 | 2021-05-03 11:57:57 -0500 | [diff] [blame] | 1026 | nargs='?', |
| 1027 | default='./Cargo.toml', |
Abhishek Pandit-Subedi | 702740f | 2021-03-16 21:39:00 -0700 | [diff] [blame] | 1028 | help='Cargo.toml used to generate ebuilds.') |
| 1029 | |
| 1030 | args = parser.parse_args(argv) |
| 1031 | |
| 1032 | # Require target directory if not dry run |
| 1033 | if not args.target_dir and not args.dry_run: |
| 1034 | raise Exception('Target directory must be set unless dry-run is True.') |
| 1035 | |
| 1036 | if not args.skip: |
| 1037 | args.skip = [] |
| 1038 | |
| 1039 | return args |
| 1040 | |
| 1041 | |
| 1042 | if __name__ == '__main__': |
| 1043 | sys.exit(main(sys.argv[1:])) |