| // Copyright 2013 The Flutter 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 'package:meta/meta.dart'; |
| |
| /// An identifier of a target that loosely follows [GN][] and [Bazel][] format. |
| /// |
| /// [gn]: https://gn.googlesource.com/gn/+/master/docs/reference.md#labels |
| /// [bazel]: https://bazel.build/concepts/labels |
| /// |
| /// A label in `engine_tool` are two string components separated by a colon in |
| /// the format `//path/to/package-name:target-name`, with constraints on the |
| /// characters that can be used in each component (see [package] and [target]). |
| /// |
| /// Unlike counterparts in Bazel and GN: |
| /// - The package name is always a source-absolute path (i.e. starts with `//`). |
| /// - Valid identifier characters are `a-zA-Z0-9_-`, not starting with a digit. |
| /// - The target name is never empty, even when it is a default target. |
| @immutable |
| final class Label { |
| /// Creates a new label with the given package and target name. |
| /// |
| /// If [target] is omitted, the final component of [package] is used as the |
| /// target name (e.g. `//name` becomes `//name:name`). |
| /// |
| /// Throws a [FormatException] if the package or target name is invalid. |
| factory Label(String package, [String? target]) { |
| final FormatException? packageError = _checkPackage(package); |
| if (packageError != null) { |
| throw packageError; |
| } |
| |
| target ??= package.substring(package.lastIndexOf('/') + 1); |
| final FormatException? targetError = _checkTarget(target); |
| if (targetError != null) { |
| throw targetError; |
| } |
| |
| return Label._(package, target); |
| } |
| |
| const Label._(this.package, this.target); |
| |
| /// Parses a label from a string in the format `//package-name:target-name`. |
| /// |
| /// Throws a [FormatException] if the label is invalid. |
| static Label parse(String label) { |
| final int colon = label.indexOf(':'); |
| if (colon == -1) { |
| return Label(label); |
| } |
| return Label(label.substring(0, colon), label.substring(colon + 1)); |
| } |
| |
| /// Parses a label from a string in the format `//package-name:target-name`. |
| /// |
| /// If a toolchain is present, it is removed. |
| /// |
| /// Throws a [FormatException] if the label is invalid. |
| static Label parseGn(String label) { |
| // Remove (//build/toolchain/...) at the end of the label. |
| if (label.endsWith(')')) { |
| final int start = label.lastIndexOf('(', label.length - 2); |
| if (start != -1) { |
| label = label.substring(0, start); |
| } |
| } |
| |
| return Label.parse(label); |
| } |
| |
| /// A source-absolute package name, starting with `//`. |
| /// |
| /// The package name must be a valid identifier, i.e. it must be a sequence of |
| /// identifier components separated by slashes and cannot end with a slash. |
| final String package; |
| |
| /// A target name within the package. |
| /// |
| /// The target name must be a valid identifier. |
| final String target; |
| |
| @override |
| bool operator ==(Object other) { |
| return other is Label && package == other.package && target == other.target; |
| } |
| |
| @override |
| int get hashCode => Object.hash(package, target); |
| |
| /// Returns a string representation of the label (i.e. used by [parse]). |
| /// |
| /// Use [toNinjaLabel] to remove the leading `//` from the package name. |
| @override |
| String toString() => '$package:$target'; |
| |
| /// Returns a Ninja-compatible string representation of the label. |
| /// |
| /// `//package-name:target-name` becomes `package-name:target-name`. |
| String toNinjaLabel() => '${package.substring(2)}:$target'; |
| |
| static FormatException? _checkPackage(String package) { |
| // Must start with a double slash. |
| if (!package.startsWith('//')) { |
| return FormatException( |
| 'Package name must start with "//".', |
| package, |
| ); |
| } |
| |
| // Cannot end with a slash. |
| if (package.endsWith('/')) { |
| return FormatException( |
| 'Package name must not end with a slash.', |
| package, |
| ); |
| } |
| |
| // Check each component of the package name for valid identifier characters. |
| // We use a standard loop to give a better error message. |
| int i = 2; |
| while (true) { |
| final int j = package.indexOf('/', i); |
| final String component = |
| j == -1 ? package.substring(i) : package.substring(i, j); |
| if (!_identifier.hasMatch(component)) { |
| return FormatException( |
| 'Package name component must be a valid identifier.', |
| package, |
| i, |
| ); |
| } |
| if (j == -1) { |
| return null; |
| } |
| i = j + 1; |
| } |
| } |
| |
| static FormatException? _checkTarget(String target) { |
| if (!_identifier.hasMatch(target)) { |
| return FormatException( |
| 'Target name must be a valid identifier.', |
| target, |
| ); |
| } |
| return null; |
| } |
| |
| static final RegExp _identifier = RegExp(r'^[a-zA-Z_][a-zA-Z0-9_-]*$'); |
| } |
| |
| /// A generic target pattern that can be used to match multiple targets. |
| /// |
| /// Similar to [Label], but supports wildcards in the package and target name: |
| /// - `//path/to/package/...` all targets in the package and its subpackages. |
| /// - `//path/to/package:all` all targets in the package. |
| /// |
| /// Inspired by <https://bazel.build/run/build#specifying-build-targets>. |
| @immutable |
| final class TargetPattern { |
| /// Creates a new pattern with the given package and target name. |
| /// |
| /// If [target] is omitted, the pattern matches all targets in the package. |
| factory TargetPattern(String package, [String? target]) { |
| // We are so close to a Label that we can reuse the same checks. |
| // The only difference is that the package name can end with `...`. |
| bool packageEndsWithWildcard = false; |
| if (package.endsWith('/...')) { |
| packageEndsWithWildcard = true; |
| package = package.substring(0, package.length - 4); |
| } |
| |
| // Edgecase: //... is a valid pattern that matches all targets. |
| if (packageEndsWithWildcard && package == '/') { |
| return const TargetPattern._('//...', null); |
| } |
| |
| // Throws a FormatException if the package or target name is invalid. |
| final Label label = Label(package, target); |
| return TargetPattern._( |
| packageEndsWithWildcard ? '${label.package}...' : label.package, |
| packageEndsWithWildcard ? null : label.target, |
| ); |
| } |
| |
| const TargetPattern._(this.package, this.target); |
| |
| /// Parses a pattern from a string in the format `//package-name:target-name`. |
| /// |
| /// Throws a [FormatException] if the pattern is invalid. |
| static TargetPattern parse(String pattern) { |
| final int colon = pattern.indexOf(':'); |
| if (colon == -1) { |
| return TargetPattern(pattern); |
| } |
| return TargetPattern( |
| pattern.substring(0, colon), |
| pattern.substring(colon + 1), |
| ); |
| } |
| |
| /// A source-absolute package name optionally ending with `...`. |
| final String package; |
| |
| /// A target name within the package, or `all` to match all targets. |
| /// |
| /// If [package] ends with `...`, [target] must be `null`. |
| final String? target; |
| |
| @override |
| bool operator ==(Object other) { |
| return other is TargetPattern && |
| package == other.package && |
| target == other.target; |
| } |
| |
| @override |
| int get hashCode => Object.hash(package, target); |
| |
| /// Returns a string representation of the target pattern. |
| /// |
| /// The string representation is compatible with [parse]. |
| @override |
| String toString() => target == null ? package : '$package:$target'; |
| |
| /// Returns a string representation of the target pattern in [GN format][gn]. |
| /// |
| /// [gn]: https://gn.googlesource.com/gn/+/master/docs/reference.md#labels |
| /// |
| /// Specifically: |
| /// - `//package/to/package/...` is converted to `path/to/package/*`. |
| /// - `//package/to/package:all` is converted to `path/to/package:*`. |
| String toGnPattern() { |
| // Remove the leading `//` and replace `...` and `all`. |
| final String package = this.package.substring(2); |
| if (target == null) { |
| assert(package.endsWith('...')); |
| |
| // Edgecase: //... is a valid pattern that matches all targets. |
| if (package == '...') { |
| return '/*'; |
| } |
| |
| return package.replaceRange(package.length - 3, package.length, '/*'); |
| } |
| return '$package:${target == 'all' ? '*' : target}'; |
| } |
| } |