Merge remote-tracking branch 'origin/master' into cherry_pick_cache_warning
diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3ce8efc..9940883 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml
@@ -10,3 +10,5 @@ directory: / schedule: interval: monthly + labels: + - autosubmit
diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml index ac3e456..dd7bbbc 100644 --- a/.github/workflows/no-response.yml +++ b/.github/workflows/no-response.yml
@@ -1,12 +1,10 @@ # A workflow to close issues where the author hasn't responded to a request for -# more information; see https://github.com/godofredoc/no-response for docs. +# more information; see https://github.com/actions/stale. name: No Response -# Both `issue_comment` and `scheduled` event types are required. +# Run as a daily cron. on: - issue_comment: - types: [created] schedule: # Every day at 8am - cron: '0 8 * * *' @@ -14,21 +12,24 @@ # All permissions not specified are set to 'none'. permissions: issues: write + pull-requests: write jobs: - noResponse: + no-response: runs-on: ubuntu-latest if: ${{ github.repository_owner == 'dart-lang' }} steps: - - uses: godofredoc/no-response@0ce2dc0e63e1c7d2b87752ceed091f6d32c9df09 + - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 with: - responseRequiredLabel: "needs-info" - responseRequiredColor: 4774bc - daysUntilClose: 14 - # Comment to post when closing an Issue for lack of response. - closeComment: > - Without additional information we're not able to resolve this issue, - so it will be closed at this time. You're still free to add more - info and respond to any questions above, though. We'll reopen the - issue if you do. Thanks for your contribution! - token: ${{ github.token }} + days-before-stale: -1 + days-before-close: 14 + stale-issue-label: "needs-info" + close-issue-message: > + Without additional information we're not able to resolve this issue. + Feel free to add more info or respond to any questions above and we + can reopen the case. Thanks for your contribution! + stale-pr-label: "needs-info" + close-pr-message: > + Without additional information we're not able to resolve this PR. + Feel free to add more info or respond to any questions above. + Thanks for your contribution!
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2717cf0..91b3aac 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml
@@ -24,8 +24,8 @@ matrix: sdk: [dev] steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c - - uses: dart-lang/setup-dart@a57a6c04cf7d4840e88432aad6281d1e125f0d46 + - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab + - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f with: sdk: ${{ matrix.sdk }} - id: install @@ -52,8 +52,8 @@ sdk: [dev] shard: [0, 1, 2, 3, 4, 5, 6] steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c - - uses: dart-lang/setup-dart@a57a6c04cf7d4840e88432aad6281d1e125f0d46 + - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab + - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f with: sdk: ${{ matrix.sdk }} - name: Install dependencies
diff --git a/doc/cache_layout.md b/doc/cache_layout.md new file mode 100644 index 0000000..8cd76a7 --- /dev/null +++ b/doc/cache_layout.md
@@ -0,0 +1,245 @@ +# The Pub cache + +The Pub cache is where pub stores downloaded packages and globally activated +packages. + +The information in this document is informational, and can be used for +understanding the cache, but we strongly encourage all manipulation of the cache +happens though the `dart pub`/`flutter pub` commands to avoid relying on +accidental properties of the cache that might be broken in the future. + +See [system_cache](../lib/src/system_cache.dart) for implementation of top-level +cache conventions. + +## Location + +The global default pub-cache is located at: + * `$HOME/.pub_cache` on Linux and Mac OS, + * `%LOCALAPPDATA%/Pub/Cache` on Windows. + +Prior to Flutter 3.8.0, the Flutter SDK declared `PUB_CACHE=$FLUTTER_ROOT/.pub_cache` overriding the default global pub-cache. + +The default location of the pub-cache can be overridden using the environment variable `PUB_CACHE`. + +For the remainder of this document we refer to the location of the pub-cache as `$PUB_CACHE`.``` + +## Layout + +The layout of the pub cache has evolved over time, and where possible we strive +for backwards and forwards compatibility where possible, such that a new and an +old sdk can share the same cache. + +Here are the top-level folders you can find in a Pub cache. + +```plaintext +$PUB_CACHE/ +├── global_packages/ # Globally activated packages +├── bin/ # Executables compiled from globally activated packages. +├── git/ # Cloned git packages +├── hosted/ # Hosted package downloads +├── hosted-hashes/ # Hashes of hosted packages +├── log/ # Logs after crashes and --verbose +├── README.md # Short description of the folder +└── _temp/ # Package downloads are extracted here, and moved atomically. +``` + +Prior to Dart 2.15.0 pub would also store credentials in the pub-cache. They are now +stored in a platform specific config dir: + +* On Linux `$XDG_CONFIG_HOME/dart/pub-credentials.json` if `$XDG_CONFIG_HOME` is + defined, otherwise `$HOME/.config/dart/pub-credentials.json` +* On Mac OS: `$HOME/Library/Application Support/dart/pub-credentials.json` +* On Windows: `%APPDATA%/dart/pub-credentials.json` + +### Hosted + +The `hosted/` folder contains one folder per repository that Pub has retrieved packages from. + +See [hosted](../lib/src/source/hosted.dart) for details. + +```plaintext +$PUB_CACHE/hosted +├── pub.dartlang.org +├── pub.dev +└── pub.flutter-io.cn +``` + +Before Dart 2.19 pub would by default download from `pub.dartlang.org`. This was +changed to `pub.dev`. The two sites are mirrors and should always be identical. +We decided to make the switch when we introduced content-hashes, because they +anyway required redownloading of all packages to calculate the hashes. + +The url of the repository is encoded to a directory name with a weird URI-like +encoding. This is a mistake that seems costly to fix, but is worth being aware +of. + +Each repository folder has a sub-folder per `$package-$version/` that is +downloaded from that repository: + +```plaintext +$PUB_CACHE/hosted/pub.dev/ +├── .cache/ +├── args-2.3.2/ +├── retry-1.0.0/ +├── yaml-3.1.1/ +├── yaml_edit-2.0.2/ +└── yaml_edit-2.1.0/ +``` + +A package name can always be used as a file-name (TODO: should we have a length-restriction on package-names? https://github.com/dart-lang/pub/issues/3895). + +A serialized version string can always be encoded as a file-name. + +These subfolders contain the content of the packages as they are extracted from +the package archives. These are extracted in `.pub_cache/_temp` and moved here +atomically, hence, packages here are always fully extracted. + +The `.cache/` folder is storing the last version listing response for each +package: + +```plaintext +$PUB_CACHE/hosted/pub.dev/.cache +├── args-versions.json +├── retry-versions.json +├── yaml_edit-versions.json +└── yaml-versions.json +``` + +These are used as a heuristic to speed up version resolution. They are +timestamped with the time of retrieval. + +(This should arguably have been called something like `$PUB_CACHE/hosted-version-listings` to separate cleanly from the package downloads). + +Adding further files or folders inside `hosted/` unless the start with a '.' will break +the `dart pub cache clean` command from older SDKs and should be avoided. (It assumes all folders/files are packages that need to be restored). + +The `$PUB_CACHE/hosted-hashes/` folder has a file per package-version with the sha256 hash of the downloaded archive: + +```plaintext +$PUB_CACHE/hosted-hashes/ +└── pub.dev/ + ├── args-2.3.2.sha256 + ├── retry-1.0.0.sha256 + ├── yaml-3.1.1.sha256 + ├── yaml_edit-2.0.2.sha256 + └── yaml_edit-2.1.0.sha256 +``` + +These are used to ensure the integrity of the relation between a `pubspec.lock` file and +the cache. + +* If a version-listing shows another hash, the package is redownloaded. +* If a `pubspec.lock` file shows another hash the package is redownloaded. + +`$PUB_CACHE/hosted-hashes/` was introduced in Dart 2.19.0. + +## Git + +The `$PUB_CACHE/git/` folder has checkouts of the git repositories containing git dependencies. + +See [git](../lib/src/source/git.dart) for details. + +A git dependency is declared using: + * `url` (required) + * `ref` (optional, defaults to the default branch) + * `path` (optional, defaults to `.`) + +Note that we have the entire checkout, even though a package can be nested +deeper inside using `path`. Two packages can share the same checkout. + +It is laid out as this example: + +```plaintext +$PUB_CACHE/git/ +├── cache/ +│ ├── pana-72b499ded128c6590fbda1b7e87de1c8bbb38a04/ +│ └── pub-d666e8aee885cce49978e27a66c99ee08ce3995f/ +├── pana-bab826581f3f7a0604022f2043490a3b501e785e/ +├── pub-75c671c7d65db43f197b55419a8519906a611730/ +└── pub-c4e9ddc888c3aa89ef4462f0c4298929191e32b9/ +``` + +The `$PUB_CACHE/git/cache/` folder contains a "bare" checkout of each git-url (just the ). The +folders are `$PUB_CACHE/git/cache/$name-$hash/` where `$name` is derived from base-name of the +git url (without `.git`). and `$hash` is the sha1 of the git-url. This makes +them recognizable and unique. + +The other sub-folders are the actual checkouts. They are clones of respective the `$PUB_CACHE/git/cache/$name-$hash/` +folders checked out at a specific `ref`. The name is `$PUB_CACHE/git/$name-$resolvedRef/` where +`resolvedRef` is the commit-id that `ref` resolves to. + +## Global packages + +The `$PUB_CACHE/global_packages/` folder contains the globally activated +packages. + +See [global_packages](../lib/src/global_packages.dart) for the implementation +the global package conventions. + +The folder is laid out like in this example: + +```plaintext +$PUB_CACHE/global_packages/ +├── stagehand/ +│ ├── bin/ +│ │ └── stagehand.dart-2.19.0.snapshot +│ ├── .dart_tool/ +│ │ └── package_config.json +│ ├── incremental +│ └── pubspec.lock +└── mono_repo/ + ├── bin/ + │ ├── mono_repo.dart-2.18.4.snapshot + │ ├── mono_repo.dart-3.0.0-0.0.dev.snapshot + │ └── mono_repo.dart-3.0.0-55.0.dev.snapshot + ├── .dart_tool/ + │ └── package_config.json + ├── incremental + └── pubspec.lock +``` + +There can only be one globally activated package with a given name at the same +time. + +Each globally installed package has its own folder with a `pubspec.lock` and a +`.dart_tool/package_config.json`. + +The `pubspec.lock` holds the current resolution for the activated package. + +The `bin/` folder contains precompiled snapshots - these are compilations of +`bin/*.dart` files from the activated packages, suffixed by +`-$sdkVersion.snapshot`. Several snapshots can exist if the same globally +activated package is used by several sdk-versions (TODO: This does have some +limitations, and we should probably rethink this). A re-activation of the +package will delete all the existing snapshots. + +The `incremental` is used while compiling them. (TODO: We should probably remove +this after succesful compilation https://github.com/dart-lang/pub/issues/3896). + +For packages activated with `--source=path` the lockfile is special-cased to just point +to the activated path, and `.dart_tool/package_config.json`, snapshots are +stored in that folder. + +The `$PUB_CACHE/bin/` folder contains "binstubs" that are small executable +scripts that will run the precompiled snapshots. + +By default one binstub is generated per `executable` in the `pubspec.yaml` of an +activated package. The binstub contains decodable information about which +package it belongs to, so it can be deleted when a package is `deactivated` and +a helpful message can be shown in case of conflicts. + +If the snapshot doesn't exist, the binstub will attempt to create it by invoking +`dart pub global run`. + +```plaintext +$PUB_CACHE/bin/ +├── mono_repo +└── stagehand +``` + +## Logs + +When pub crashes or is run with `--verbose` it will create a +`$PUB_CACHE/log/pub_log.txt` with the dart sdk version, platform, `$PUB_CACHE`, +`$PUB_HOSTED_URL`, `pubspec.yaml`, `pubspec.lock`, current command, verbose log and +stack-trace.
diff --git a/doc/repository-spec-v2.md b/doc/repository-spec-v2.md index 9104428..5c19cb1 100644 --- a/doc/repository-spec-v2.md +++ b/doc/repository-spec-v2.md
@@ -11,11 +11,11 @@ A custom package repository is identified by a _hosted-url_, like `https://pub.dev` or `https://some-server.com/prefix/pub/`. The _hosted-url_ always includes protocol `http://` or `https://`. -For the purpose of this specification the _hosted-url_ should always be +For the purpose of this specification, the _hosted-url_ should always be normalized such that it doesn't end with a slash (`/`). As all URL end-points described in this specification includes slash prefix. -For the remainder of this specification the placeholder `<hosted-url>` will be +For the remainder of this specification, the placeholder `<hosted-url>` will be used in place of a _hosted-url_ such as: * `https://pub.dev` * `https://some-server.com/prefix/pub` @@ -64,7 +64,7 @@ versions of the API to change responses. Clients are strongly encouraged to specify an `Accept` header. But for -compatiblity will probably want to assume API version `2`, +compatibility will probably want to assume API version `2`, if no `Accept` header is specified. @@ -110,9 +110,9 @@ } ``` -The `<message>` is intended to be a brief human readable explanation of what +The `<message>` is intended to be a brief human-readable explanation of what when wrong and why the request failed. The `<code>` is a text string intended to -allow clients to handle special cases without using regular expression to +allow clients to handle special cases without using regular expressions to parse the `<message>`. @@ -129,7 +129,7 @@ * `dart pub token add <hosted-url>` This command will prompt the user for the `<token>` on stdin, reducing the risk -that the `<token>` is accidentally stored in shell history. For security reasons +that the `<token>` is accidentally stored in shell history. For security reasons, authentication can only be used when `<hosted-url>` uses HTTPS. For further details on token management see: `dart pub token --help`. @@ -152,7 +152,7 @@ a token for the given `<hosted-url>`, then the `dart pub` client knows for sure that the token it has stored for the given `<hosted-url>` is invalid. Hence, the `dart pub` client shall remove the token from local configuration. -Hence, a server shall not send `401` in case where a token is valid, but does +Hence, a server shall not send `401` in cases where a token is valid but does not have permissions to access the package in question. When receiving a `401` response the `dart pub` client shall: @@ -180,7 +180,7 @@ The `dart pub` will display the `message` in the terminal, so the user can discover that they need to navigate to `https://pub.example.com/manage-tokens`. Once the user opens this URL in the browser, the server is then free to ask the -user to sign-in using any browser-based authentication mechanism. Once signed-in +user to sign-in using any browser-based authentication mechanism. Once signed in the server can allow the user to create a token and tell the user to copy/paste this into stdin for `dart pub token add pub.example.com`. @@ -258,8 +258,8 @@ The response (after following redirects) must be a gzipped TAR archive. The `archive_url` may be temporary and is allowed to include query-string -parameters. This allows for the server to return signed-URLs for S3, GCS or -other blob storage service. If temporary URLs are returned it is wise to not set +parameters. This allows for the server to return signed URLs for S3, GCS, or +other blob storage services. If temporary URLs are returned it is wise to not set expiration to less than 25 minutes (to allow for retries and clock drift). The `archive_sha256` should be the hex-encoded sha256 checksum of the file at @@ -267,7 +267,7 @@ integrity of the downloaded archive. The `archive_sha256` also provides an easy way for clients to detect if -something has changed on the server. In the absense of this field the client can +something has changed on the server. In the absence of this field, the client can still download the archive to obtain a checksum and detect changes to the archive. @@ -276,7 +276,7 @@ `archive_url` is requested. Example: if `https://pub.example.com/path` returns an `archive_url = 'https://pub.example.com/path/...'` then the request for `https://pub.example.com/path/...` will include `Authorization` header. -This would however, not be case if the same server returned +This would however, not be the case if the same server returned `archive_url = 'https://pub.example.com/blob/...'`. @@ -303,7 +303,7 @@ } ``` -To publish a package a HTTP `GET` request for +To publish a package an HTTP `GET` request for `<hosted-url>/api/packages/versions/new` is made. This request returns an `<multipart-upload-url>` and a dictionary of fields. To upload the package archive a multi-part `POST` request is made to `<multipart-upload-url>` with @@ -351,7 +351,7 @@ The client shall then issue a `GET` request to `<finalize-upload-url>`. As with `archive_url` the client will only attach an `Authorization` if the `<hosted-url>` is a prefix of `<finalize-upload-url>`. If the server wants to -accepts the uploaded package the server should respond: +accept the uploaded package the server should respond: ```http HTTP/1.1 200 Ok @@ -366,7 +366,7 @@ The server is allowed to consider the publishing incomplete until the `GET` request for `<finalize-upload-url>` has been issued. Once this request has succeeded the package is considered successfully published. If the server has -caches that need to expire before newly published packages becomes available, +caches that need to expire before newly published packages become available, or it has other out-of-band approvals that need to be given it's reasonable to inform the user about this in the `<message>`. @@ -382,14 +382,14 @@ } ``` -This can be used to forbid git-dependencies in published packages, limit the -archive size, or enforce any other repository specific constraints. +This can be used to forbid git dependencies in published packages, limit the +archive size, or enforce any other repository-specific constraints. This upload flow allows for archives to be uploaded directly to a signed POST URL for [S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/HTTPPOSTExamples.html), [GCS](https://cloud.google.com/storage/docs/xml-api/post-object-forms) or similar blob storage service. Both the -`<multipart-upload-url>` and `<finalize-upload-url>` is allowed to contain +`<multipart-upload-url>` and `<finalize-upload-url>` are allowed to contain query-string parameters, and both of these URLs need only be temporary.
diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart index 7bb02a1..06a0260 100644 --- a/lib/src/command/add.dart +++ b/lib/src/command/add.dart
@@ -2,6 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:io'; + import 'package:args/args.dart'; import 'package:collection/collection.dart'; import 'package:path/path.dart' as p; @@ -18,6 +20,7 @@ import '../package.dart'; import '../package_name.dart'; import '../pubspec.dart'; +import '../sdk.dart'; import '../solver.dart'; import '../source/git.dart'; import '../source/hosted.dart'; @@ -244,6 +247,15 @@ writeTextFile(entrypoint.pubspecPath, newPubspecText); } + String? overridesFileContents; + final overridesPath = + p.join(entrypoint.rootDir, Pubspec.pubspecOverridesFilename); + try { + overridesFileContents = readTextFile(overridesPath); + } on IOException { + overridesFileContents = null; + } + /// Even if it is a dry run, run `acquireDependencies` so that the user /// gets a report on the other packages that might change version due /// to this new dependency. @@ -253,6 +265,8 @@ newPubspecText, cache.sources, location: Uri.parse(entrypoint.pubspecPath), + overridesFileContents: overridesFileContents, + overridesLocation: Uri.file(overridesPath), ), ) .acquireDependencies( @@ -612,6 +626,9 @@ { 'dependencies': { packageName: parsedDescriptor, + }, + 'environment': { + 'sdk': sdk.version.toString(), } }, cache.sources,
diff --git a/lib/src/command/cache_add.dart b/lib/src/command/cache_add.dart index 763856e..85c2c5d 100644 --- a/lib/src/command/cache_add.dart +++ b/lib/src/command/cache_add.dart
@@ -72,15 +72,10 @@ } Future<void> downloadVersion(id) async { - if (cache.contains(id)) { - // TODO(rnystrom): Include source and description if not hosted. - // See solve_report.dart for code to harvest. + final result = await cache.downloadPackage(id); + if (!result.didUpdate) { log.message('Already cached ${id.name} ${id.version}.'); - return; } - - // Download it. - await cache.downloadPackage(id); } if (argResults['all']) {
diff --git a/lib/src/command/dependency_services.dart b/lib/src/command/dependency_services.dart index c56f413..ced8c00 100644 --- a/lib/src/command/dependency_services.dart +++ b/lib/src/command/dependency_services.dart
@@ -53,7 +53,7 @@ Future<void> runProtected() async { final compatiblePubspec = stripDependencyOverrides(entrypoint.root.pubspec); - final breakingPubspec = stripVersionUpperBounds(compatiblePubspec); + final breakingPubspec = stripVersionBounds(compatiblePubspec); final compatiblePackagesResult = await _tryResolve(compatiblePubspec, cache); @@ -85,7 +85,7 @@ if (package == null) return []; final lockFile = entrypoint.lockFile; final pubspec = upgradeType == _UpgradeType.multiBreaking - ? stripVersionUpperBounds(rootPubspec) + ? stripVersionBounds(rootPubspec) : Pubspec( rootPubspec.name, dependencies: rootPubspec.dependencies.values, @@ -535,7 +535,7 @@ // This happens when we resolved a package from a legacy // server not providing archive_sha256. As a side-effect of // downloading the package we compute and store the sha256. - package = await cache.downloadPackage(package); + package = (await cache.downloadPackage(package)).packageId; } } } else {
diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart index 89db747..294df13 100644 --- a/lib/src/command/lish.dart +++ b/lib/src/command/lish.dart
@@ -66,6 +66,8 @@ /// Whether the publish requires confirmation. bool get force => argResults['force']; + bool get skipValidation => argResults['skip-validation']; + LishCommand() { argParser.addFlag( 'dry-run', @@ -79,6 +81,12 @@ negatable: false, help: 'Publish without confirmation if there are no errors.', ); + argParser.addFlag( + 'skip-validation', + negatable: false, + help: + 'Publish without validation and resolution (this will ignore errors).', + ); argParser.addOption( 'server', help: 'The package server to which to upload this package.', @@ -251,7 +259,13 @@ 'pubspec.'); } - await entrypoint.acquireDependencies(SolveType.get, analytics: analytics); + if (!skipValidation) { + await entrypoint.acquireDependencies(SolveType.get, analytics: analytics); + } else { + log.warning( + 'Running with `skip-validation`. No client-side validation is done.', + ); + } var files = entrypoint.root.listFiles(); log.fine('Archiving and publishing ${entrypoint.root.name}.'); @@ -267,10 +281,12 @@ createTarGz(files, baseDir: entrypoint.rootDir).toBytes(); // Validate the package. - var isValid = await _validate( - packageBytesFuture.then((bytes) => bytes.length), - files, - ); + var isValid = skipValidation + ? true + : await _validate( + packageBytesFuture.then((bytes) => bytes.length), + files, + ); if (!isValid) { overrideExitCode(exit_codes.DATA); return;
diff --git a/lib/src/command/outdated.dart b/lib/src/command/outdated.dart index 18ebf30..dee80a1 100644 --- a/lib/src/command/outdated.dart +++ b/lib/src/command/outdated.dart
@@ -13,6 +13,7 @@ import '../command_runner.dart'; import '../entrypoint.dart'; import '../io.dart'; +import '../lock_file.dart'; import '../log.dart' as log; import '../package.dart'; import '../package_name.dart'; @@ -142,13 +143,19 @@ await log.spinner( 'Resolving', () async { - final upgradablePackagesResult = - await _tryResolve(upgradablePubspec, cache); + final upgradablePackagesResult = await _tryResolve( + upgradablePubspec, + cache, + lockFile: entrypoint.lockFile, + ); hasUpgradableResolution = upgradablePackagesResult != null; upgradablePackages = upgradablePackagesResult ?? []; - final resolvablePackagesResult = - await _tryResolve(resolvablePubspec, cache); + final resolvablePackagesResult = await _tryResolve( + resolvablePubspec, + cache, + lockFile: entrypoint.lockFile, + ); hasResolvableResolution = resolvablePackagesResult != null; resolvablePackages = resolvablePackagesResult ?? []; }, @@ -386,11 +393,16 @@ /// Try to solve [pubspec] return [PackageId]s in the resolution or `null` if no /// resolution was found. -Future<List<PackageId>?> _tryResolve(Pubspec pubspec, SystemCache cache) async { +Future<List<PackageId>?> _tryResolve( + Pubspec pubspec, + SystemCache cache, { + LockFile? lockFile, +}) async { final solveResult = await tryResolveVersions( SolveType.upgrade, cache, Package.inMemory(pubspec), + lockFile: lockFile, ); return solveResult?.packages; @@ -546,31 +558,35 @@ log.message(b.toString()); } - var upgradable = rows - .where( - (row) => - row.current != null && - row.upgradable != null && - row.current != row.upgradable && - // Include transitive only, if we show them - (showTransitiveDependencies || - hasKind(_DependencyKind.direct)(row) || - hasKind(_DependencyKind.dev)(row)), - ) - .length; + var upgradable = rows.where( + (row) { + final current = row.current; + final upgradable = row.upgradable; + return current != null && + upgradable != null && + current < upgradable && + // Include transitive only, if we show them + (showTransitiveDependencies || + hasKind(_DependencyKind.direct)(row) || + hasKind(_DependencyKind.dev)(row)); + }, + ).length; - var notAtResolvable = rows - .where( - (row) => - (row.current != null || !lockFileExists) && - row.resolvable != null && - row.upgradable != row.resolvable && - // Include transitive only, if we show them - (showTransitiveDependencies || - hasKind(_DependencyKind.direct)(row) || - hasKind(_DependencyKind.dev)(row)), - ) - .length; + var notAtResolvable = rows.where( + (row) { + final current = row.current; + final upgradable = row.upgradable; + final resolvable = row.resolvable; + return (current != null || !lockFileExists) && + resolvable != null && + upgradable != null && + upgradable < resolvable && + // Include transitive only, if we show them + (showTransitiveDependencies || + hasKind(_DependencyKind.direct)(row) || + hasKind(_DependencyKind.dev)(row)); + }, + ).length; if (!hasUpgradableResolution || !hasResolvableResolution) { log.message(mode.noResolutionText); @@ -720,7 +736,7 @@ @override Future<Pubspec> resolvablePubspec(Pubspec? pubspec) async { - return stripVersionUpperBounds(pubspec!); + return stripVersionBounds(pubspec!); } } @@ -763,6 +779,11 @@ _id.source == other._id.source && _pubspec.version == other._pubspec.version; + bool operator <(_VersionDetails other) => + _overridden == other._overridden && + _id.source == other._id.source && + _pubspec.version < other._pubspec.version; + @override int get hashCode => Object.hash(_pubspec.version, _id.source, _overridden); }
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart index 84fe11e..6c1dcda 100644 --- a/lib/src/command/upgrade.dart +++ b/lib/src/command/upgrade.dart
@@ -3,7 +3,9 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:io'; +import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; import 'package:yaml_edit/yaml_edit.dart'; @@ -176,7 +178,7 @@ Future<void> _runUpgradeMajorVersions() async { final toUpgrade = _directDependenciesToUpgrade(); - final resolvablePubspec = stripVersionUpperBounds( + final resolvablePubspec = stripVersionBounds( entrypoint.root.pubspec, stripOnly: toUpgrade, ); @@ -247,12 +249,23 @@ } } + String? overridesFileContents; + final overridesPath = + p.join(entrypoint.rootDir, Pubspec.pubspecOverridesFilename); + try { + overridesFileContents = readTextFile(overridesPath); + } on IOException { + overridesFileContents = null; + } + await entrypoint .withPubspec( Pubspec.parse( newPubspecText, cache.sources, location: Uri.parse(entrypoint.pubspecPath), + overridesFileContents: overridesFileContents, + overridesLocation: Uri.file(overridesPath), ), ) .acquireDependencies(
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index db3df6f..ab8fed6 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart
@@ -23,8 +23,8 @@ import 'lock_file.dart'; import 'log.dart' as log; import 'package.dart'; -import 'package_config.dart'; import 'package_config.dart' show PackageConfig; +import 'package_config.dart'; import 'package_graph.dart'; import 'package_name.dart'; import 'pub_embeddable_command.dart'; @@ -32,6 +32,7 @@ import 'sdk.dart'; import 'solver.dart'; import 'solver/report.dart'; +import 'solver/solve_suggestions.dart'; import 'source/cached.dart'; import 'source/unknown.dart'; import 'system_cache.dart'; @@ -355,16 +356,30 @@ } SolveResult result; - result = await log.progress('Resolving dependencies$suffix', () async { - _checkSdkConstraint(root.pubspec); - return resolveVersions( - type, - cache, - root, - lockFile: lockFile, - unlock: unlock ?? [], + + try { + result = await log.progress('Resolving dependencies$suffix', () async { + _checkSdkConstraint(root.pubspec); + return resolveVersions( + type, + cache, + root, + lockFile: lockFile, + unlock: unlock ?? [], + ); + }); + } on SolveFailure catch (e) { + throw SolveFailure( + e.incompatibility, + suggestions: await suggestResolutionAlternatives( + this, + type, + e.incompatibility, + unlock ?? [], + cache, + ), ); - }); + } // We have to download files also with --dry-run to ensure we know the // archive hashes for downloaded files. @@ -899,8 +914,8 @@ // Check if language version specified in the `package_config.json` is // correct. This is important for path dependencies as these can mutate. - for (final pkg in packageConfig.packages) { - if (pkg.name == root.name || pkg.name == 'flutter_gen') continue; + for (final pkg in packageConfig.nonInjectedPackages) { + if (pkg.name == root.name) continue; final id = lockFile.packages[pkg.name]; if (id == null) { assert(
diff --git a/lib/src/flutter_releases.dart b/lib/src/flutter_releases.dart new file mode 100644 index 0000000..38e8ca8 --- /dev/null +++ b/lib/src/flutter_releases.dart
@@ -0,0 +1,105 @@ +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'dart:convert'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:http/http.dart'; +import 'package:pub_semver/pub_semver.dart'; + +import 'http.dart'; +import 'log.dart'; + +String get flutterReleasesUrl => + Platform.environment['_PUB_TEST_FLUTTER_RELEASES_URL'] ?? + 'https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json'; + +// Retrieves all released versions of Flutter. +Future<List<FlutterRelease>> _flutterReleases = () async { + final response = await retryForHttp( + 'fetching available Flutter releases', + () => globalHttpClient.fetch(Request('GET', Uri.parse(flutterReleasesUrl))), + ); + final decoded = jsonDecode(response.body); + if (decoded is! Map) throw FormatException('Bad response - should be a Map'); + final releases = decoded['releases']; + if (releases is! List) { + throw FormatException('Bad response - releases should be a list.'); + } + final result = <FlutterRelease>[]; + for (final release in releases) { + final channel = { + 'beta': Channel.beta, + 'stable': Channel.stable, + 'dev': Channel.dev + }[release['channel']]; + if (channel == null) throw FormatException('Release with bad channel'); + final dartVersion = release['dart_sdk_version']; + // Some releases don't have an associated dart version, ignore. + if (dartVersion is! String) continue; + final flutterVersion = release['version']; + if (flutterVersion is! String) throw FormatException('Not a string'); + result.add( + FlutterRelease( + flutterVersion: Version.parse(flutterVersion), + dartVersion: Version.parse(dartVersion.split(' ').first), + channel: channel, + ), + ); + } + return result + // Sort releases by channel and version. + .sorted((a, b) { + final compareChannels = b.channel.index - a.channel.index; + if (compareChannels != 0) return compareChannels; + return a.flutterVersion.compareTo(b.flutterVersion); + }) + // Newest first. + .reversed + .toList(); +}(); + +/// The "best" Flutter release for a given set of constraints is the first one +/// in [_flutterReleases] that matches both the flutter and dart constraint. +/// +/// Returns if no such release could be found. +Future<FlutterRelease?> inferBestFlutterRelease( + Map<String, VersionConstraint> sdkConstraints, +) async { + final List<FlutterRelease> flutterReleases; + try { + flutterReleases = await _flutterReleases; + } on Exception catch (e) { + fine('Failed retrieving the list of flutter-releases: $e'); + return null; + } + return flutterReleases.firstWhereOrNull( + (release) => + (sdkConstraints['flutter'] ?? VersionConstraint.any) + .allows(release.flutterVersion) && + (sdkConstraints['dart'] ?? VersionConstraint.any) + .allows(release.dartVersion), + ); +} + +enum Channel { + stable, + beta, + dev, +} + +/// A version of the Flutter SDK and its related Dart SDK. +class FlutterRelease { + final Version flutterVersion; + final Version dartVersion; + final Channel channel; + FlutterRelease({ + required this.flutterVersion, + required this.dartVersion, + required this.channel, + }); + @override + toString() => + 'FlutterRelease(flutter=$flutterVersion, dart=$dartVersion, channel=$channel)'; +}
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart index 93d6678..5ed3438 100644 --- a/lib/src/global_packages.dart +++ b/lib/src/global_packages.dart
@@ -235,7 +235,7 @@ } on SolveFailure catch (error) { for (var incompatibility in error.incompatibility.externalIncompatibilities) { - if (incompatibility.cause != IncompatibilityCause.noVersions) continue; + if (incompatibility.cause is! NoVersionsIncompatibilityCause) continue; if (incompatibility.terms.single.package.name != name) continue; // If the SolveFailure is caused by [dep] not // being available, report that as a [dataError].
diff --git a/lib/src/io.dart b/lib/src/io.dart index 8f510f6..086f93f 100644 --- a/lib/src/io.dart +++ b/lib/src/io.dart
@@ -1190,6 +1190,6 @@ /// /// Otherwise, wrap with single quotation, and use '\'' to insert single quote. String escapeShellArgument(String x) => - RegExp(r'^[a-zA-Z0-9-_=@.]+$').stringMatch(x) == null + RegExp(r'^[a-zA-Z0-9-_=@.^]+$').stringMatch(x) == null ? "'${x.replaceAll(r'\', r'\\').replaceAll("'", r"'\''")}'" : x;
diff --git a/lib/src/lock_file.dart b/lib/src/lock_file.dart index f725260..48de2fd 100644 --- a/lib/src/lock_file.dart +++ b/lib/src/lock_file.dart
@@ -168,8 +168,9 @@ packageEntries, (name, spec) { // Parse the version. - final versionEntry = _getStringEntry(spec, 'version'); - final version = Version.parse(versionEntry); + final versionEntry = + _getEntry<YamlScalar>(spec, 'version', 'version string'); + final version = _parseVersion(versionEntry); // Parse the source. final sourceName = _getStringEntry(spec, 'source'); @@ -243,7 +244,7 @@ return fn(); } on FormatException catch (e) { throw SourceSpanFormatException( - 'Invalid $description: ${e.message}', + '$description: ${e.message}', span, ); } @@ -257,6 +258,14 @@ ); } + static Version _parseVersion(YamlNode node) { + return _parseNode( + node, + 'version', + parse: Version.parse, + ); + } + static String _getStringEntry(YamlMap map, String key) { return _parseNode<String>( _getEntry<YamlScalar>(map, key, 'string'), @@ -275,19 +284,19 @@ final value = node.value; if (parse != null) { if (value is! String) { - _failAt('Expected a $typeDescription.', node); + _failAt('Expected a $typeDescription', node); } return _wrapFormatException( - 'Expected a $typeDescription.', + 'Expected a $typeDescription', node.span, () => parse(node.value), ); } else if (value is T) { return value; } - _failAt('Expected a $typeDescription.', node); + _failAt('Expected a $typeDescription', node); } - _failAt('Expected a $typeDescription.', node); + _failAt('Expected a $typeDescription', node); } static void _parseEachEntry<K, V>(
diff --git a/lib/src/package_config.dart b/lib/src/package_config.dart index f5ef0b0..b215555 100644 --- a/lib/src/package_config.dart +++ b/lib/src/package_config.dart
@@ -160,9 +160,13 @@ // // See https://github.com/flutter/flutter/issues/73870 . Iterable<PackageConfigEntry> get nonInjectedPackages => - packages.where((package) => package.name != 'flutter_gen'); + packages.where((package) => !_isInjectedFlutterGenPackage(package)); } +bool _isInjectedFlutterGenPackage(PackageConfigEntry package) => + package.name == 'flutter_gen' && + package.rootUri.toString() == 'flutter_gen'; + class PackageConfigEntry { /// Package name. String name;
diff --git a/lib/src/pubspec_utils.dart b/lib/src/pubspec_utils.dart index c46573a..3f57976 100644 --- a/lib/src/pubspec_utils.dart +++ b/lib/src/pubspec_utils.dart
@@ -36,20 +36,22 @@ } /// Returns new pubspec with the same dependencies as [original] but with the -/// upper bounds of the constraints removed. +/// the bounds of the constraints removed. /// -/// If [stripOnly] is provided, only the packages whose names are in -/// [stripOnly] will have their upper bounds removed. If [stripOnly] is -/// not specified or empty, then all packages will have their upper bounds -/// removed. -Pubspec stripVersionUpperBounds( +/// If [stripLower] is `false` (the default) only the upper bound is removed. +/// +/// If [stripOnly] is provided, only the packages whose names are in [stripOnly] +/// will have their bounds removed. If [stripOnly] is not specified or empty, +/// then all packages will have their bounds removed. +Pubspec stripVersionBounds( Pubspec original, { Iterable<String>? stripOnly, + bool stripLowerBound = false, }) { ArgumentError.checkNotNull(original, 'original'); stripOnly ??= []; - List<PackageRange> stripUpperBounds( + List<PackageRange> stripBounds( Map<String, PackageRange> constrained, ) { final result = <PackageRange>[]; @@ -61,7 +63,9 @@ if (stripOnly!.isEmpty || stripOnly.contains(packageRange.name)) { unconstrainedRange = PackageRange( packageRange.toRef(), - stripUpperBound(packageRange.constraint), + stripLowerBound + ? VersionConstraint.any + : stripUpperBound(packageRange.constraint), ); } result.add(unconstrainedRange); @@ -74,8 +78,8 @@ original.name, version: original.version, sdkConstraints: original.sdkConstraints, - dependencies: stripUpperBounds(original.dependencies), - devDependencies: stripUpperBounds(original.devDependencies), + dependencies: stripBounds(original.dependencies), + devDependencies: stripBounds(original.devDependencies), dependencyOverrides: original.dependencyOverrides.values, ); }
diff --git a/lib/src/solver.dart b/lib/src/solver.dart index 74dbfa8..b8f68e7 100644 --- a/lib/src/solver.dart +++ b/lib/src/solver.dart
@@ -4,6 +4,8 @@ import 'dart:async'; +import 'package:pub_semver/pub_semver.dart'; + import 'lock_file.dart'; import 'package.dart'; import 'solver/failure.dart'; @@ -33,6 +35,7 @@ Package root, { LockFile? lockFile, Iterable<String> unlock = const [], + Map<String, Version> sdkOverrides = const {}, }) { lockFile ??= LockFile.empty(); return VersionSolver( @@ -41,6 +44,7 @@ root, lockFile, unlock, + sdkOverrides: sdkOverrides, ).solve(); }
diff --git a/lib/src/solver/failure.dart b/lib/src/solver/failure.dart index ba4e69f..6c6b3f4 100644 --- a/lib/src/solver/failure.dart +++ b/lib/src/solver/failure.dart
@@ -19,6 +19,8 @@ /// it will have one term, which will be the root package. final Incompatibility incompatibility; + final String? suggestions; + @override String get message => toString(); @@ -30,12 +32,12 @@ PackageNotFoundException? get packageNotFound { for (var incompatibility in incompatibility.externalIncompatibilities) { var cause = incompatibility.cause; - if (cause is PackageNotFoundCause) return cause.exception; + if (cause is PackageNotFoundIncompatibilityCause) return cause.exception; } return null; } - SolveFailure(this.incompatibility) + SolveFailure(this.incompatibility, {this.suggestions}) : assert( incompatibility.terms.isEmpty || incompatibility.terms.single.package.isRoot, @@ -44,7 +46,10 @@ /// Describes how [incompatibility] was derived, and thus why version solving /// failed. @override - String toString() => _Writer(incompatibility).write(); + String toString() => [ + _Writer(incompatibility).write(), + if (suggestions != null) suggestions + ].join('\n'); } /// A class that writes a human-readable description of the cause of a
diff --git a/lib/src/solver/incompatibility.dart b/lib/src/solver/incompatibility.dart index 8b81501..15178b3 100644 --- a/lib/src/solver/incompatibility.dart +++ b/lib/src/solver/incompatibility.dart
@@ -2,8 +2,6 @@ // for details. 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:pub_semver/pub_semver.dart'; - import '../package_name.dart'; import 'incompatibility_cause.dart'; import 'term.dart'; @@ -98,7 +96,7 @@ /// for packages with the given names. @override String toString([Map<String, PackageDetail>? details]) { - if (cause == IncompatibilityCause.dependency) { + if (cause is DependencyIncompatibilityCause) { assert(terms.length == 2); var depender = terms.first; @@ -108,20 +106,11 @@ return '${_terse(depender, details, allowEvery: true)} depends on ' '${_terse(dependee, details)}'; - } else if (cause == IncompatibilityCause.useLatest) { - assert(terms.length == 1); - - var forbidden = terms.last; - assert(forbidden.isPositive); - - return 'the latest version of ${_terseRef(forbidden, details)} ' - '(${VersionConstraint.any.difference(forbidden.constraint)}) ' - 'is required'; - } else if (cause is SdkCause) { + } else if (cause is SdkIncompatibilityCause) { assert(terms.length == 1); assert(terms.first.isPositive); - var cause = this.cause as SdkCause; + var cause = this.cause as SdkIncompatibilityCause; var buffer = StringBuffer(_terse(terms.first, details, allowEvery: true)); if (cause.noNullSafetyCause) { buffer.write(' doesn\'t support null safety'); @@ -135,25 +124,25 @@ } } return buffer.toString(); - } else if (cause == IncompatibilityCause.noVersions) { + } else if (cause is NoVersionsIncompatibilityCause) { assert(terms.length == 1); assert(terms.first.isPositive); return 'no versions of ${_terseRef(terms.first, details)} ' 'match ${terms.first.constraint}'; - } else if (cause is PackageNotFoundCause) { + } else if (cause is PackageNotFoundIncompatibilityCause) { assert(terms.length == 1); assert(terms.first.isPositive); - var cause = this.cause as PackageNotFoundCause; + var cause = this.cause as PackageNotFoundIncompatibilityCause; return "${_terseRef(terms.first, details)} doesn't exist " '(${cause.exception.message})'; - } else if (cause == IncompatibilityCause.unknownSource) { + } else if (cause is UnknownSourceIncompatibilityCause) { assert(terms.length == 1); assert(terms.first.isPositive); return '${terms.first.package.name} comes from unknown source ' '"${terms.first.package.source}"'; - } else if (cause == IncompatibilityCause.root) { - // [IncompatibilityCause.root] is only used when a package depends on the + } else if (cause is RootIncompatibilityCause) { + // [RootIncompatibilityCause] is only used when a package depends on the // entrypoint with an incompatible version, so we want to print the // entrypoint's actual version to make it clear why this failed. assert(terms.length == 1); @@ -276,8 +265,8 @@ var buffer = StringBuffer('${_terse(thisPositive, details, allowEvery: true)} '); - var isDependency = cause == IncompatibilityCause.dependency && - other.cause == IncompatibilityCause.dependency; + var isDependency = cause is DependencyIncompatibilityCause && + other.cause is DependencyIncompatibilityCause; buffer.write(isDependency ? 'depends on' : 'requires'); buffer.write(' both $thisNegatives'); if (thisLine != null) buffer.write(' ($thisLine)'); @@ -340,7 +329,7 @@ priorPositives.map((term) => _terse(term, details)).join(' or '); buffer.write('if $priorString then '); } else { - var verb = prior.cause == IncompatibilityCause.dependency + var verb = prior.cause is DependencyIncompatibilityCause ? 'depends on' : 'requires'; buffer.write('${_terse(priorPositives.first, details, allowEvery: true)} ' @@ -351,7 +340,7 @@ if (priorLine != null) buffer.write(' ($priorLine)'); buffer.write(' which '); - if (latter.cause == IncompatibilityCause.dependency) { + if (latter.cause is DependencyIncompatibilityCause) { buffer.write('depends on '); } else { buffer.write('requires '); @@ -411,13 +400,13 @@ } else { buffer.write(_terse(positives.first, details, allowEvery: true)); buffer.write( - prior.cause == IncompatibilityCause.dependency + prior.cause is DependencyIncompatibilityCause ? ' depends on ' : ' requires ', ); } - if (latter.cause == IncompatibilityCause.unknownSource) { + if (latter.cause is UnknownSourceIncompatibilityCause) { var package = latter.terms.first.package; buffer.write('${package.name} '); if (priorLine != null) buffer.write('($priorLine) '); @@ -429,12 +418,8 @@ buffer.write('${_terse(latter.terms.first, details)} '); if (priorLine != null) buffer.write('($priorLine) '); - if (latter.cause == IncompatibilityCause.useLatest) { - var latest = - VersionConstraint.any.difference(latter.terms.single.constraint); - buffer.write('but the latest version ($latest) is required'); - } else if (latter.cause is SdkCause) { - var cause = latter.cause as SdkCause; + if (latter.cause is SdkIncompatibilityCause) { + var cause = latter.cause as SdkIncompatibilityCause; if (cause.noNullSafetyCause) { buffer.write('which doesn\'t support null safety'); } else { @@ -446,11 +431,11 @@ buffer.write('SDK version ${cause.constraint}'); } } - } else if (latter.cause == IncompatibilityCause.noVersions) { + } else if (latter.cause is NoVersionsIncompatibilityCause) { buffer.write("which doesn't match any versions"); - } else if (latter.cause is PackageNotFoundCause) { + } else if (latter.cause is PackageNotFoundIncompatibilityCause) { buffer.write("which doesn't exist " - '(${(latter.cause as PackageNotFoundCause).exception.message})'); + '(${(latter.cause as PackageNotFoundIncompatibilityCause).exception.message})'); } else { buffer.write('which is forbidden'); }
diff --git a/lib/src/solver/incompatibility_cause.dart b/lib/src/solver/incompatibility_cause.dart index c8f2db8..13047df 100644 --- a/lib/src/solver/incompatibility_cause.dart +++ b/lib/src/solver/incompatibility_cause.dart
@@ -6,30 +6,14 @@ import '../exceptions.dart'; import '../language_version.dart'; +import '../package_name.dart'; import '../sdk.dart'; +import '../source/sdk.dart'; import 'incompatibility.dart'; /// The reason an [Incompatibility]'s terms are incompatible. -abstract class IncompatibilityCause { - const IncompatibilityCause._(); - - /// The incompatibility represents the requirement that the root package - /// exists. - static const IncompatibilityCause root = _Cause('root'); - - /// The incompatibility represents a package's dependency. - static const IncompatibilityCause dependency = _Cause('dependency'); - - /// The incompatibility represents the user's request that we use the latest - /// version of a given package. - static const IncompatibilityCause useLatest = _Cause('use latest'); - - /// The incompatibility indicates that the package has no versions that match - /// the given constraint. - static const IncompatibilityCause noVersions = _Cause('no versions'); - - /// The incompatibility indicates that the package has an unknown source. - static const IncompatibilityCause unknownSource = _Cause('unknown source'); +sealed class IncompatibilityCause { + const IncompatibilityCause(); /// Human readable notice / information providing context for this /// incompatibility. @@ -47,6 +31,50 @@ String? get hint => null; } +/// The incompatibility represents the requirement that the root package +/// exists. +class RootIncompatibilityCause extends IncompatibilityCause { + factory RootIncompatibilityCause() => const RootIncompatibilityCause._(); + const RootIncompatibilityCause._(); +} + +/// The incompatibility represents a package's dependency. +class DependencyIncompatibilityCause extends IncompatibilityCause { + final PackageRange depender; + final PackageRange target; + DependencyIncompatibilityCause(this.depender, this.target); + + @override + String? get notice { + final dependerDescription = depender.description; + if (dependerDescription is SdkDescription) { + final targetConstraint = target.constraint; + if (targetConstraint is Version) { + return ''' +Note: ${target.name} is pinned to version $targetConstraint by ${depender.name} from the ${dependerDescription.sdk} SDK. +See https://dart.dev/go/sdk-version-pinning for details. +'''; + } + } + return null; + } +} + +/// The incompatibility indicates that the package has no versions that match +/// the given constraint. +class NoVersionsIncompatibilityCause extends IncompatibilityCause { + factory NoVersionsIncompatibilityCause() => + const NoVersionsIncompatibilityCause._(); + const NoVersionsIncompatibilityCause._(); +} + +/// The incompatibility indicates that the package has an unknown source. +class UnknownSourceIncompatibilityCause extends IncompatibilityCause { + factory UnknownSourceIncompatibilityCause() => + const UnknownSourceIncompatibilityCause._(); + const UnknownSourceIncompatibilityCause._(); +} + /// The incompatibility was derived from two existing incompatibilities during /// conflict resolution. class ConflictCause extends IncompatibilityCause { @@ -58,22 +86,12 @@ /// from which the target incompatibility was derived. final Incompatibility other; - ConflictCause(this.conflict, this.other) : super._(); -} - -/// A class for stateless [IncompatibilityCause]s. -class _Cause extends IncompatibilityCause { - final String _name; - - const _Cause(this._name) : super._(); - - @override - String toString() => _name; + ConflictCause(this.conflict, this.other); } /// The incompatibility represents a package's SDK constraint being /// incompatible with the current SDK. -class SdkCause extends IncompatibilityCause { +class SdkIncompatibilityCause extends IncompatibilityCause { /// The union of all the incompatible versions' constraints on the SDK. // TODO(zarah): Investigate if this can be non-nullable final VersionConstraint? constraint; @@ -115,16 +133,16 @@ return sdk.installMessage; } - SdkCause(this.constraint, this.sdk) : super._(); + SdkIncompatibilityCause(this.constraint, this.sdk); } /// The incompatibility represents a package that couldn't be found by its /// source. -class PackageNotFoundCause extends IncompatibilityCause { +class PackageNotFoundIncompatibilityCause extends IncompatibilityCause { /// The exception indicating why the package couldn't be found. final PackageNotFoundException exception; - PackageNotFoundCause(this.exception) : super._(); + PackageNotFoundIncompatibilityCause(this.exception); @override String? get hint => exception.hint;
diff --git a/lib/src/solver/package_lister.dart b/lib/src/solver/package_lister.dart index 3dccd37..ce794b7 100644 --- a/lib/src/solver/package_lister.dart +++ b/lib/src/solver/package_lister.dart
@@ -54,6 +54,8 @@ /// reversed. final bool _isDowngrade; + final Map<String, Version> sdkOverrides; + /// A map from dependency names to constraints indicating which versions of /// [_ref] have already had their dependencies on the given versions returned /// by [incompatibilitiesFor]. @@ -107,11 +109,15 @@ this._overriddenPackages, this._allowedRetractedVersion, { bool downgrade = false, + this.sdkOverrides = const {}, }) : _isDowngrade = downgrade; /// Creates a package lister for the root [package]. - PackageLister.root(Package package, this._systemCache) - : _ref = PackageRef.root(package), + PackageLister.root( + Package package, + this._systemCache, { + required Map<String, Version>? sdkOverrides, + }) : _ref = PackageRef.root(package), // Treat the package as locked so we avoid the logic for finding the // boundaries of various constraints, which is useless for the root // package. @@ -120,7 +126,8 @@ _overriddenPackages = Set.unmodifiable(package.dependencyOverrides.keys), _isDowngrade = false, - _allowedRetractedVersion = null; + _allowedRetractedVersion = null, + sdkOverrides = sdkOverrides ?? {}; /// Returns the number of versions of this package that match [constraint]. Future<int> countVersions(VersionConstraint constraint) async { @@ -202,7 +209,7 @@ return [ Incompatibility( [Term(id.toRange(), true)], - IncompatibilityCause.noVersions, + NoVersionsIncompatibilityCause(), ) ]; } on PackageNotFoundException { @@ -212,7 +219,7 @@ return [ Incompatibility( [Term(id.toRange(), true)], - IncompatibilityCause.noVersions, + NoVersionsIncompatibilityCause(), ) ]; } @@ -229,7 +236,7 @@ return [ Incompatibility( [Term(depender, true)], - SdkCause( + SdkIncompatibilityCause( pubspec.sdkConstraints[sdk.identifier]?.effectiveConstraint, sdk, ), @@ -316,11 +323,12 @@ /// Returns an [Incompatibility] that represents a dependency from [depender] /// onto [target]. - Incompatibility _dependency(PackageRange depender, PackageRange target) => - Incompatibility( - [Term(depender, true), Term(target, false)], - IncompatibilityCause.dependency, - ); + Incompatibility _dependency(PackageRange depender, PackageRange target) { + return Incompatibility( + [Term(depender, true), Term(target, false)], + DependencyIncompatibilityCause(depender, target), + ); + } /// If the version at [index] in [_versions] isn't compatible with the current /// version of [sdk], returns an [Incompatibility] indicating that. @@ -356,7 +364,7 @@ return Incompatibility( [Term(_ref.withConstraint(incompatibleVersions), true)], - SdkCause(sdkConstraint, sdk), + SdkIncompatibilityCause(sdkConstraint, sdk), ); } @@ -461,6 +469,7 @@ if (constraint == null) return true; return sdk.isAvailable && - constraint.effectiveConstraint.allows(sdk.version!); + constraint.effectiveConstraint + .allows(sdkOverrides[sdk.identifier] ?? sdk.version!); } }
diff --git a/lib/src/solver/result.dart b/lib/src/solver/result.dart index 8c5a33f..f7a5d41 100644 --- a/lib/src/solver/result.dart +++ b/lib/src/solver/result.dart
@@ -67,9 +67,10 @@ if (id.source is CachedSource) { return await withDependencyType(_root.pubspec.dependencyType(id.name), () async { - return await cache.downloadPackage( + return (await cache.downloadPackage( id, - ); + )) + .packageId; }); } return id;
diff --git a/lib/src/solver/solve_suggestions.dart b/lib/src/solver/solve_suggestions.dart new file mode 100644 index 0000000..ef95486 --- /dev/null +++ b/lib/src/solver/solve_suggestions.dart
@@ -0,0 +1,278 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:pub_semver/pub_semver.dart'; + +import '../command_runner.dart'; +import '../entrypoint.dart'; +import '../flutter_releases.dart'; +import '../io.dart'; +import '../package.dart'; +import '../package_name.dart'; +import '../pubspec.dart'; +import '../pubspec_utils.dart'; +import '../solver.dart'; +import '../source/hosted.dart'; +import '../system_cache.dart'; +import 'incompatibility.dart'; +import 'incompatibility_cause.dart'; + +/// Looks through the root-[incompability] of a solve-failure and tries to see if +/// the conflict could resolved by any of the following suggestions: +/// * An update of the current SDK. +/// * Any single change to a package constraint. +/// * Removing the bounds on all constraints, changing less than 5 dependencies. +/// * Running `pub upgrade --major versions`. +/// +/// Returns a formatted list of suggestions, or the empty String if no +/// suggestions were found. +Future<String?> suggestResolutionAlternatives( + Entrypoint entrypoint, + SolveType type, + Incompatibility incompatibility, + Iterable<String> unlock, + SystemCache cache, +) async { + final resolutionContext = _ResolutionContext( + entrypoint: entrypoint, + type: type, + cache: cache, + unlock: unlock, + ); + + final visited = <String>{}; + final stopwatch = Stopwatch()..start(); + final suggestions = <_ResolutionSuggestion>[]; + void addSuggestionIfPresent(_ResolutionSuggestion? suggestion) { + if (suggestion != null) suggestions.add(suggestion); + } + + for (final externalIncompatibility + in incompatibility.externalIncompatibilities) { + if (stopwatch.elapsed > Duration(seconds: 3)) { + // Never spend more than 3 seconds computing suggestions. + break; + } + final cause = externalIncompatibility.cause; + if (cause is SdkIncompatibilityCause) { + addSuggestionIfPresent(await resolutionContext.suggestSdkUpdate(cause)); + } else { + for (final term in externalIncompatibility.terms) { + final name = term.package.name; + + if (!visited.add(name)) { + continue; + } + addSuggestionIfPresent( + await resolutionContext.suggestSinglePackageUpdate(name), + ); + } + } + } + if (suggestions.isEmpty) { + addSuggestionIfPresent( + await resolutionContext.suggestUnlockingAll(stripLowerBound: true) ?? + await resolutionContext.suggestUnlockingAll(stripLowerBound: false), + ); + } + + if (suggestions.isEmpty) return null; + final tryOne = suggestions.length == 1 + ? 'You can try the following suggestion to make the pubspec resolve:' + : 'You can try one of the following suggestions to make the pubspec resolve:'; + + suggestions.sort((a, b) => a.priority.compareTo(b.priority)); + + return '\n$tryOne\n${suggestions.take(5).map((e) => e.suggestion).join('\n')}'; +} + +class _ResolutionSuggestion { + final String suggestion; + final int priority; + _ResolutionSuggestion(this.suggestion, {this.priority = 0}); +} + +String packageAddDescription(Entrypoint entrypoint, PackageId id) { + final name = id.name; + final isDev = entrypoint.root.pubspec.devDependencies.containsKey(name); + final resolvedDescription = id.description; + final String descriptor; + final d = resolvedDescription.description.serializeForPubspec( + containingDir: Directory.current + .path // The add command will resolve file names relative to CWD. + // This currently should have no implications as we don't create suggestions + // for path-packages. + , + languageVersion: entrypoint.root.pubspec.languageVersion, + ); + if (d == null) { + descriptor = VersionConstraint.compatibleWith(id.version).toString(); + } else { + descriptor = json.encode({ + 'version': VersionConstraint.compatibleWith(id.version).toString(), + id.source.name: d + }); + } + + final devPart = isDev ? 'dev:' : ''; + return '$devPart$name:${escapeShellArgument(descriptor)}'; +} + +class _ResolutionContext { + final Entrypoint entrypoint; + final SolveType type; + final Iterable<String> unlock; + final SystemCache cache; + _ResolutionContext({ + required this.entrypoint, + required this.type, + required this.cache, + required this.unlock, + }); + + /// If [cause] mentions an sdk, attempt resolving using another released + /// version of Flutter/Dart. Return that as a suggestion if found. + Future<_ResolutionSuggestion?> suggestSdkUpdate( + SdkIncompatibilityCause cause, + ) async { + final sdkName = cause.sdk.identifier; + if (!(sdkName == 'dart' || (sdkName == 'flutter' && runningFromFlutter))) { + // Only make sdk upgrade suggestions for Flutter and Dart. + return null; + } + + final constraint = cause.constraint; + if (constraint == null) return null; + + /// Find the most relevant Flutter release fullfilling the constraint. + final bestRelease = + await inferBestFlutterRelease({cause.sdk.identifier: constraint}); + if (bestRelease == null) return null; + final result = await _tryResolve( + entrypoint.root.pubspec, + sdkOverrides: { + 'dart': bestRelease.dartVersion, + 'flutter': bestRelease.flutterVersion + }, + ); + if (result == null) { + return null; + } + return _ResolutionSuggestion( + runningFromFlutter + ? '* Try using the Flutter SDK version: ${bestRelease.flutterVersion}. ' + : + // Here we assume that any Dart version included in a Flutter + // release can also be found as a released Dart SDK. + '* Try using the Dart SDK version: ${bestRelease.dartVersion}. See https://dart.dev/get-dart.', + ); + } + + /// Attempt another resolution with a relaxed constraint on [name]. If that + /// resolves, suggest upgrading to that version. + Future<_ResolutionSuggestion?> suggestSinglePackageUpdate(String name) async { + final originalRange = entrypoint.root.dependencies[name] ?? + entrypoint.root.devDependencies[name]; + if (originalRange == null || + originalRange.description is! HostedDescription) { + // We can only relax constraints on hosted dependencies. + return null; + } + final originalConstraint = originalRange.constraint; + final relaxedPubspec = stripVersionBounds( + entrypoint.root.pubspec, + stripOnly: [name], + stripLowerBound: true, + ); + + final result = await _tryResolve(relaxedPubspec); + if (result == null) { + return null; + } + final resolvingPackage = result.packages.firstWhere((p) => p.name == name); + + final addDescription = packageAddDescription(entrypoint, resolvingPackage); + + var priority = 1; + var suggestion = + '* Try updating your constraint on $name: $topLevelProgram pub add $addDescription'; + if (originalConstraint is VersionRange) { + final min = originalConstraint.min; + if (min != null) { + if (resolvingPackage.version < min) { + priority = 3; + suggestion = + '* Consider downgrading your constraint on $name: $topLevelProgram pub add $addDescription'; + } else { + priority = 2; + suggestion = + '* Try upgrading your constraint on $name: $topLevelProgram pub add $addDescription'; + } + } + } + + return _ResolutionSuggestion(suggestion, priority: priority); + } + + /// Attempt resolving with all version constraints relaxed. If that resolves, + /// return a corresponding suggestion to update. + Future<_ResolutionSuggestion?> suggestUnlockingAll({ + required bool stripLowerBound, + }) async { + final originalPubspec = entrypoint.root.pubspec; + final relaxedPubspec = + stripVersionBounds(originalPubspec, stripLowerBound: stripLowerBound); + + final result = await _tryResolve(relaxedPubspec); + if (result == null) { + return null; + } + final updatedPackageVersions = <PackageId>[]; + for (final id in result.packages) { + final originalConstraint = (originalPubspec.dependencies[id.name] ?? + originalPubspec.devDependencies[id.name]) + ?.constraint; + if (originalConstraint != null) { + updatedPackageVersions.add(id); + } + } + if (stripLowerBound && updatedPackageVersions.length > 5) { + // Too complex, don't suggest. + return null; + } + if (stripLowerBound) { + updatedPackageVersions.sort((a, b) => a.name.compareTo(b.name)); + final formattedConstraints = updatedPackageVersions + .map((e) => packageAddDescription(entrypoint, e)) + .join(' '); + return _ResolutionSuggestion( + '* Try updating the following constraints: $topLevelProgram pub add $formattedConstraints', + priority: 4, + ); + } else { + return _ResolutionSuggestion( + '* Try an upgrade of your constraints: $topLevelProgram pub upgrade --major-versions', + priority: 4, + ); + } + } + + /// Attempt resolving + Future<SolveResult?> _tryResolve( + Pubspec pubspec, { + Map<String, Version> sdkOverrides = const {}, + }) async { + try { + return await resolveVersions( + type, + cache, + Package.inMemory(pubspec), + sdkOverrides: sdkOverrides, + lockFile: entrypoint.lockFile, + unlock: unlock, + ); + } on SolveFailure { + return null; + } + } +}
diff --git a/lib/src/solver/version_solver.dart b/lib/src/solver/version_solver.dart index e8a2346..afef413 100644 --- a/lib/src/solver/version_solver.dart +++ b/lib/src/solver/version_solver.dart
@@ -75,6 +75,10 @@ /// The set of packages for which the lockfile should be ignored. final Set<String> _unlock; + /// If present these represents the version of an SDK to assume during + /// resolution. + final Map<String, Version> _sdkOverrides; + final _stopwatch = Stopwatch(); VersionSolver( @@ -82,8 +86,10 @@ this._systemCache, this._root, this._lockFile, - Iterable<String> unlock, - ) : _dependencyOverrides = _root.dependencyOverrides, + Iterable<String> unlock, { + Map<String, Version> sdkOverrides = const {}, + }) : _sdkOverrides = sdkOverrides, + _dependencyOverrides = _root.dependencyOverrides, _unlock = {...unlock}; /// Finds a set of dependencies that match the root package's constraints, or @@ -93,7 +99,7 @@ _addIncompatibility( Incompatibility( [Term(PackageRange.root(_root), false)], - IncompatibilityCause.root, + RootIncompatibilityCause(), ), ); @@ -345,7 +351,7 @@ _addIncompatibility( Incompatibility( [Term(candidate.toRef().withConstraint(VersionConstraint.any), true)], - IncompatibilityCause.unknownSource, + UnknownSourceIncompatibilityCause(), ), ); return candidate.name; @@ -367,7 +373,7 @@ _addIncompatibility( Incompatibility( [Term(package.toRef().withConstraint(VersionConstraint.any), true)], - PackageNotFoundCause(error), + PackageNotFoundIncompatibilityCause(error), ), ); return package.name; @@ -387,7 +393,7 @@ _addIncompatibility( Incompatibility( [Term(package, true)], - IncompatibilityCause.noVersions, + NoVersionsIncompatibilityCause(), ), ); return package.name; @@ -496,7 +502,13 @@ PackageLister _packageLister(PackageRange package) { var ref = package.toRef(); return _packageListers.putIfAbsent(ref, () { - if (ref.isRoot) return PackageLister.root(_root, _systemCache); + if (ref.isRoot) { + return PackageLister.root( + _root, + _systemCache, + sdkOverrides: _sdkOverrides, + ); + } var locked = _getLocked(ref.name); if (locked != null && locked.toRef() != ref) locked = null; @@ -516,6 +528,7 @@ overridden, _getAllowedRetracted(ref.name), downgrade: _type == SolveType.downgrade, + sdkOverrides: _sdkOverrides, ); }); }
diff --git a/lib/src/source/cached.dart b/lib/src/source/cached.dart index 9818146..933b08c 100644 --- a/lib/src/source/cached.dart +++ b/lib/src/source/cached.dart
@@ -52,11 +52,6 @@ /// the system cache. Future<Pubspec> describeUncached(PackageId id, SystemCache cache); - /// Determines if the package identified by [id] is already downloaded to the - /// system cache. - bool isInSystemCache(PackageId id, SystemCache cache) => - dirExists(getDirectoryInCache(id, cache)); - /// Downloads the package identified by [id] to the system cache. Future<DownloadPackageResult> downloadToSystemCache( PackageId id,
diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart index 6f6632c..1b28401 100644 --- a/lib/src/source/hosted.dart +++ b/lib/src/source/hosted.dart
@@ -9,7 +9,7 @@ import 'dart:typed_data'; import 'package:collection/collection.dart' - show IterableExtension, IterableNullableExtension, ListEquality, maxBy; + show IterableExtension, IterableNullableExtension, maxBy; import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; @@ -554,23 +554,27 @@ if (maxAge == null || now.difference(stat.modified) < maxAge) { try { final cachedDoc = jsonDecode(readTextFile(cachePath)); + if (cachedDoc is! Map) { + throw FormatException('Broken cached version listing response'); + } final timestamp = cachedDoc['_fetchedAt']; - if (timestamp is String) { - final parsedTimestamp = DateTime.parse(timestamp); - final cacheAge = DateTime.now().difference(parsedTimestamp); - if (maxAge != null && cacheAge > maxAge) { - // Too old according to internal timestamp - delete. - tryDeleteEntry(cachePath); - } else { - var res = _versionInfoFromPackageListing( - cachedDoc, - ref, - Uri.file(cachePath), - cache, - ); - _responseCache[ref] = Pair(parsedTimestamp, res); - return res; - } + if (timestamp is! String) { + throw FormatException('Broken cached version listing response'); + } + final parsedTimestamp = DateTime.parse(timestamp); + final cacheAge = DateTime.now().difference(parsedTimestamp); + if (maxAge != null && cacheAge > maxAge) { + // Too old according to internal timestamp - delete. + tryDeleteEntry(cachePath); + } else { + var res = _versionInfoFromPackageListing( + cachedDoc, + ref, + Uri.file(cachePath), + cache, + ); + _responseCache[ref] = Pair(parsedTimestamp, res); + return res; } } on io.IOException { // Could not read the file. Delete if it exists. @@ -857,28 +861,6 @@ ); } - /// Determines if the package identified by [id] is already downloaded to the - /// system cache and has the expected content-hash. - @override - bool isInSystemCache(PackageId id, SystemCache cache) { - if ((id.description as ResolvedHostedDescription).sha256 != null) { - try { - final cachedSha256 = readTextFile(hashPath(id, cache)); - if (!const ListEquality().equals( - hexDecode(cachedSha256), - (id.description as ResolvedHostedDescription).sha256, - )) { - return false; - } - } on io.IOException { - // Most likely the hash file was not written, because we had a legacy - // entry. - return false; - } - } - return dirExists(getDirectoryInCache(id, cache)); - } - /// The system cache directory for the hosted source contains subdirectories /// for each separate repository URL that's used on the system. /// @@ -1421,6 +1403,10 @@ if (url == source.defaultUrl) { return null; } + if (languageVersion >= + LanguageVersion.firstVersionWithShorterHostedSyntax) { + return url; + } return {'url': url, 'name': packageName}; }
diff --git a/lib/src/system_cache.dart b/lib/src/system_cache.dart index 181e847..971adba 100644 --- a/lib/src/system_cache.dart +++ b/lib/src/system_cache.dart
@@ -126,16 +126,6 @@ } } - /// Determines if the system cache contains the package identified by [id]. - bool contains(PackageId id) { - final source = id.source; - - if (source is CachedSource) { - return source.isInSystemCache(id, this); - } - throw ArgumentError('Package $id is not cacheable.'); - } - /// Create a new temporary directory within the system cache. /// /// The system cache maintains its own temporary directory that it uses to @@ -241,7 +231,7 @@ /// /// Returns [id] with an updated [ResolvedDescription], this can be different /// if the content-hash changed while downloading. - Future<PackageId> downloadPackage(PackageId id) async { + Future<DownloadPackageResult> downloadPackage(PackageId id) async { final source = id.source; assert(source is CachedSource); final result = await (source as CachedSource).downloadToSystemCache( @@ -258,7 +248,7 @@ if (result.didUpdate) { maintainCache(); } - return result.packageId; + return result; } /// Get the latest version of [package].
diff --git a/lib/src/validator/analyze.dart b/lib/src/validator/analyze.dart index d22da8d..c29fa9e 100644 --- a/lib/src/validator/analyze.dart +++ b/lib/src/validator/analyze.dart
@@ -22,12 +22,7 @@ .where(dirExists); final result = await runProcess( Platform.resolvedExecutable, - [ - 'analyze', - '--fatal-infos', - ...dirsToAnalyze, - p.join(entrypoint.rootDir, 'pubspec.yaml') - ], + ['analyze', ...dirsToAnalyze, p.join(entrypoint.rootDir, 'pubspec.yaml')], ); if (result.exitCode != 0) { final limitedOutput = limitLength(result.stdout.join('\n'), 1000);
diff --git a/pubspec.yaml b/pubspec.yaml index d93fc33..7bc76e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml
@@ -9,7 +9,7 @@ analyzer: ^5.1.0 args: ^2.4.0 async: ^2.6.1 - cli_util: ^0.3.5 + cli_util: ^0.4.0 collection: ^1.15.0 convert: ^3.0.2 crypto: ^3.0.1
diff --git a/test/add/common/add_test.dart b/test/add/common/add_test.dart index f4a611b..8e958af 100644 --- a/test/add/common/add_test.dart +++ b/test/add/common/add_test.dart
@@ -1081,4 +1081,26 @@ ) ]).validate(); }); + + test('should take pubspec_overrides.yaml into account', () async { + final server = await servePackages(); + server.serve('foo', '1.0.0'); + await d.dir('bar', [d.libPubspec('bar', '1.0.0')]).create(); + await d.appDir( + dependencies: { + 'bar': '^1.0.0', + }, + ).create(); + await d.dir(appPath, [ + d.pubspecOverrides({ + 'dependency_overrides': { + 'bar': {'path': '../bar'} + } + }) + ]).create(); + + await pubGet(); + + await pubAdd(args: ['foo'], output: contains('+ foo 1.0.0')); + }); }
diff --git a/test/add/hosted/non_default_pub_server_test.dart b/test/add/hosted/non_default_pub_server_test.dart index f6767a0..7be0095 100644 --- a/test/add/hosted/non_default_pub_server_test.dart +++ b/test/add/hosted/non_default_pub_server_test.dart
@@ -33,11 +33,43 @@ await d.appDir( dependencies: { + 'foo': {'version': '1.2.3', 'hosted': url} + }, + ).validate(); + }); + + test('Uses old syntax when needed', () async { + // Make the default server serve errors. Only the custom server should + // be accessed. + (await servePackages()).serveErrors(); + + final server = await startPackageServer(); + server.serve('foo', '0.2.5'); + server.serve('foo', '1.1.0'); + server.serve('foo', '1.2.3'); + final oldSyntaxSdkConstraint = { + 'environment': { + 'sdk': '>=2.14.0 <3.0.0' // Language version for old syntax. + }, + }; + + await d.appDir( + dependencies: {}, + pubspec: oldSyntaxSdkConstraint, + ).create(); + + final url = server.url; + + await pubAdd(args: ['foo:1.2.3', '--hosted-url', url]); + + await d.appDir( + dependencies: { 'foo': { 'version': '1.2.3', 'hosted': {'name': 'foo', 'url': url} } }, + pubspec: oldSyntaxSdkConstraint, ).validate(); }); @@ -75,18 +107,9 @@ await d.appDir( dependencies: { - 'foo': { - 'version': '1.2.3', - 'hosted': {'name': 'foo', 'url': url} - }, - 'bar': { - 'version': '3.2.3', - 'hosted': {'name': 'bar', 'url': url} - }, - 'baz': { - 'version': '1.3.5', - 'hosted': {'name': 'baz', 'url': url} - } + 'foo': {'version': '1.2.3', 'hosted': url}, + 'bar': {'version': '3.2.3', 'hosted': url}, + 'baz': {'version': '1.3.5', 'hosted': url} }, ).validate(); }); @@ -139,10 +162,7 @@ ]).validate(); await d.appDir( dependencies: { - 'foo': { - 'version': '^1.2.3', - 'hosted': {'name': 'foo', 'url': url} - } + 'foo': {'version': '^1.2.3', 'hosted': url} }, ).validate(); }); @@ -170,10 +190,7 @@ ]).validate(); await d.appDir( dependencies: { - 'foo': { - 'version': '^1.2.3', - 'hosted': {'name': 'foo', 'url': url} - } + 'foo': {'version': '^1.2.3', 'hosted': url} }, ).validate(); }); @@ -202,10 +219,7 @@ ]).validate(); await d.appDir( dependencies: { - 'foo': { - 'version': 'any', - 'hosted': {'name': 'foo', 'url': url} - } + 'foo': {'version': 'any', 'hosted': url} }, ).validate(); });
diff --git a/test/embedding/embedding_test.dart b/test/embedding/embedding_test.dart index 480acae..cf504d6 100644 --- a/test/embedding/embedding_test.dart +++ b/test/embedding/embedding_test.dart
@@ -99,6 +99,32 @@ File(snapshot).parent.deleteSync(recursive: true); }); + test('Can depend on package:flutter_gen', () async { + // Regression test for https://github.com/dart-lang/pub/issues/3314. + final server = await servePackages(); + server.serve( + 'flutter_gen', + '1.0.0', + contents: [ + d.dir('bin', [d.file('flutter_gen.dart', 'main() {print("hi");}')]) + ], + ); + + await d.appDir( + dependencies: {'flutter_gen': '^1.0.0'}, + ).create(); + await pubGet(); + final buffer = StringBuffer(); + + await runEmbeddingToBuffer( + ['run', 'flutter_gen'], + buffer, + workingDirectory: d.path(appPath), + environment: getPubTestEnvironment(), + ); + expect(buffer.toString(), contains('hi')); + }); + testWithGolden('run works, though hidden', (ctx) async { await servePackages(); await d.dir(appPath, [
diff --git a/test/embedding/ensure_pubspec_resolved.dart b/test/embedding/ensure_pubspec_resolved.dart index ea4daba..fd30bb6 100644 --- a/test/embedding/ensure_pubspec_resolved.dart +++ b/test/embedding/ensure_pubspec_resolved.dart
@@ -58,7 +58,7 @@ final contents = json.decode(File(packageConfig).readAsStringSync()); contents['packages'].add({ 'name': 'flutter_gen', - 'rootUri': '.dart_tool/flutter_gen', + 'rootUri': 'flutter_gen', 'languageVersion': '2.8', }); writeTextFile(packageConfig, json.encode(contents));
diff --git a/test/hosted/offline_test.dart b/test/hosted/offline_test.dart index 5e54c76..da98d0c 100644 --- a/test/hosted/offline_test.dart +++ b/test/hosted/offline_test.dart
@@ -118,10 +118,8 @@ await pubCommand( command, args: ['--offline'], - error: equalsIgnoringWhitespace(""" - Because myapp depends on foo >2.0.0 which doesn't match any - versions, version solving failed. - """), + error: contains(''' +Because myapp depends on foo >2.0.0 which doesn't match any versions, version solving failed.'''), ); });
diff --git a/test/lock_file_test.dart b/test/lock_file_test.dart index 3dbcf92..9fa7081 100644 --- a/test/lock_file_test.dart +++ b/test/lock_file_test.dart
@@ -7,6 +7,7 @@ import 'package:pub/src/source/hosted.dart'; import 'package:pub/src/system_cache.dart'; import 'package:pub_semver/pub_semver.dart'; +import 'package:source_span/source_span.dart'; import 'package:test/test.dart' hide Description; import 'package:yaml/yaml.dart'; @@ -135,7 +136,7 @@ sources, ); }, - throwsFormatException, + throwsSourceSpanException, ); }); @@ -149,7 +150,7 @@ sources, ); }, - throwsFormatException, + throwsSourceSpanException, ); }); @@ -166,7 +167,7 @@ sources, ); }, - throwsFormatException, + throwsSourceSpanException, ); }); @@ -184,7 +185,7 @@ sources, ); }, - throwsFormatException, + throwsSourceSpanException, ); }); @@ -201,7 +202,7 @@ sources, ); }, - throwsFormatException, + throwsSourceSpanException, ); }); @@ -218,7 +219,7 @@ sources, ); }, - throwsFormatException, + throwsSourceSpanException, ); }); @@ -236,54 +237,54 @@ sources, ); }, - throwsFormatException, + throwsSourceSpanException, ); }); test("throws if the old-style SDK constraint isn't a string", () { expect( () => LockFile.parse('sdk: 1.0', sources), - throwsFormatException, + throwsSourceSpanException, ); }); test('throws if the old-style SDK constraint is invalid', () { expect( () => LockFile.parse('sdk: oops', sources), - throwsFormatException, + throwsSourceSpanException, ); }); test("throws if the sdks field isn't a map", () { expect( () => LockFile.parse('sdks: oops', sources), - throwsFormatException, + throwsSourceSpanException, ); }); test("throws if an sdk constraint isn't a string", () { expect( () => LockFile.parse('sdks: {dart: 1.0}', sources), - throwsFormatException, + throwsSourceSpanException, ); expect( () { LockFile.parse('sdks: {dart: 1.0.0, flutter: 1.0}', sources); }, - throwsFormatException, + throwsSourceSpanException, ); }); test('throws if an sdk constraint is invalid', () { expect( () => LockFile.parse('sdks: {dart: oops}', sources), - throwsFormatException, + throwsSourceSpanException, ); expect( () { LockFile.parse('sdks: {dart: 1.0.0, flutter: oops}', sources); }, - throwsFormatException, + throwsSourceSpanException, ); }); @@ -411,3 +412,5 @@ }); }); } + +final throwsSourceSpanException = throwsA(isA<SourceSpanException>());
diff --git a/test/outdated/outdated_test.dart b/test/outdated/outdated_test.dart index 0ef5756..0757d0a 100644 --- a/test/outdated/outdated_test.dart +++ b/test/outdated/outdated_test.dart
@@ -70,7 +70,9 @@ }, ) ..serve('transitive', '1.2.3') - ..serve('dev_trans', '1.0.0'); + ..serve('dev_trans', '1.0.0') + ..serve('retracted', '1.0.0') + ..serve('retracted', '1.0.1'); await d.dir('local_package', [ d.libDir('local_package'), @@ -83,7 +85,8 @@ 'dependencies': { 'foo': '^1.0.0', 'bar': '^1.0.0', - 'local_package': {'path': '../local_package'} + 'local_package': {'path': '../local_package'}, + 'retracted': '^1.0.0', }, 'dev_dependencies': {'builder': '^1.0.0'}, }) @@ -112,7 +115,10 @@ ..serve('transitive', '2.0.0') ..serve('transitive2', '1.0.0') ..serve('transitive3', '1.0.0') - ..serve('dev_trans', '2.0.0'); + ..serve('dev_trans', '2.0.0') + // Even though the current (and latest) version is retracted, it should be + // the one shown in the upgradable and resolvable columns. + ..retractPackageVersion('retracted', '1.0.1'); await ctx.runOutdatedTests(); });
diff --git a/test/pinned_dependency_hint_test.dart b/test/pinned_dependency_hint_test.dart new file mode 100644 index 0000000..f1f4247 --- /dev/null +++ b/test/pinned_dependency_hint_test.dart
@@ -0,0 +1,74 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. 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:path/path.dart' as p; +import 'package:test/test.dart'; + +import 'descriptor.dart' as d; +import 'test_pub.dart'; + +main() { + test('Gives hint when solve failure concerns a pinned flutter package', + () async { + await d.dir('flutter', [ + d.dir('packages', [ + d.dir( + 'flutter_foo', + [ + d.libPubspec('flutter_foo', '0.0.1', deps: {'tool': '1.0.0'}) + ], + ) + ]), + d.file('version', '1.2.3') + ]).create(); + await servePackages() + ..serve('bar', '1.0.0', deps: {'tool': '^2.0.0'}) + ..serve('tool', '1.0.0') + ..serve('tool', '2.0.0'); + + await d.appDir( + dependencies: { + 'bar': 'any', + 'flutter_foo': {'sdk': 'flutter'} + }, + ).create(); + await pubGet( + environment: {'FLUTTER_ROOT': p.join(d.sandbox, 'flutter')}, + error: contains( + 'Note: tool is pinned to version 1.0.0 by flutter_foo from the flutter SDK.', + ), + ); + }); + + test('Gives hint when solve failure concerns a pinned flutter package', + () async { + await d.dir('flutter', [ + d.dir('packages', [ + d.dir( + 'flutter_foo', + [ + d.libPubspec('flutter_foo', '0.0.1', deps: {'tool': '1.0.0'}) + ], + ) + ]), + d.file('version', '1.2.3') + ]).create(); + await servePackages() + ..serve('tool', '1.0.0', deps: {'bar': '^2.0.0'}) + ..serve('bar', '1.0.0'); + + await d.appDir( + dependencies: { + 'bar': 'any', + 'flutter_foo': {'sdk': 'flutter'} + }, + ).create(); + await pubGet( + environment: {'FLUTTER_ROOT': p.join(d.sandbox, 'flutter')}, + error: contains( + 'Note: tool is pinned to version 1.0.0 by flutter_foo from the flutter SDK.', + ), + ); + }); +}
diff --git a/test/sdk_test.dart b/test/sdk_test.dart index b9a89d5..f409bc7 100644 --- a/test/sdk_test.dart +++ b/test/sdk_test.dart
@@ -127,10 +127,8 @@ await pubCommand( command, environment: {'FLUTTER_ROOT': p.join(d.sandbox, 'flutter')}, - error: equalsIgnoringWhitespace(""" - Because myapp depends on foo ^1.0.0 from sdk which doesn't match - any versions, version solving failed. - """), + error: contains(''' +Because myapp depends on foo ^1.0.0 from sdk which doesn't match any versions, version solving failed.'''), ); }); @@ -142,10 +140,8 @@ ).create(); await pubCommand( command, - error: equalsIgnoringWhitespace(""" - Because myapp depends on foo from sdk which doesn't exist - (unknown SDK "unknown"), version solving failed. - """), + error: equalsIgnoringWhitespace(''' +Because myapp depends on foo from sdk which doesn't exist (unknown SDK "unknown"), version solving failed.'''), exitCode: exit_codes.UNAVAILABLE, ); });
diff --git a/test/solve_suggestions_test.dart b/test/solve_suggestions_test.dart new file mode 100644 index 0000000..a419243 --- /dev/null +++ b/test/solve_suggestions_test.dart
@@ -0,0 +1,275 @@ +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file +// for details. 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:shelf/shelf.dart'; + +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart'; + +import 'descriptor.dart' as d; +import 'test_pub.dart'; + +void main() { + test('suggests an upgrade to the flutter sdk', () async { + await d.dir('flutter', [d.file('version', '1.2.3')]).create(); + final server = await servePackages(); + server.serve( + 'foo', + '1.0.0', + pubspec: { + 'environment': {'flutter': '>=3.3.0', 'sdk': '^2.17.0'} + }, + ); + server.handle( + '/flutterReleases', + (request) => Response.ok(releasesMockResponse), + ); + await d.dir(appPath, [ + d.libPubspec('myApp', '1.0.0', deps: {'foo': 'any'}, sdk: '^2.17.0') + ]).create(); + await pubGet( + error: contains('* Try using the Flutter SDK version: 3.3.2.'), + environment: { + '_PUB_TEST_SDK_VERSION': '2.17.0', + 'FLUTTER_ROOT': path('flutter'), + '_PUB_TEST_FLUTTER_RELEASES_URL': '${server.url}/flutterReleases', + 'PUB_ENVIRONMENT': 'flutter_cli', + }, + ); + }); + + test('suggests an upgrade to the dart sdk', () async { + final server = await servePackages(); + server.serve( + 'foo', + '1.0.0', + pubspec: { + 'environment': {'sdk': '>=2.18.0 <2.18.1'} + }, + ); + server.handle( + '/flutterReleases', + (request) => Response.ok(releasesMockResponse), + ); + await d.dir(appPath, [ + d.libPubspec('myApp', '1.0.0', deps: {'foo': 'any'}, sdk: '^2.17.0') + ]).create(); + await pubGet( + error: contains('* Try using the Dart SDK version: 2.18.0'), + environment: { + '_PUB_TEST_SDK_VERSION': '2.17.0', + '_PUB_TEST_FLUTTER_RELEASES_URL': '${server.url}/flutterReleases', + }, + ); + }); + + test('suggests an upgrade or downgrade to a package constraint', () async { + final server = await servePackages(); + server.serve('foo', '1.0.0', deps: {'bar': '^2.0.0'}); + server.serve('foo', '0.9.0', deps: {'bar': '^1.0.0'}); + + server.serve('bar', '1.0.0'); + server.serve('bar', '2.0.0'); + + await d.dir(appPath, [ + d.libPubspec( + 'myApp', + '1.0.0', + deps: {'foo': '^1.0.0'}, + devDeps: {'bar': '^1.0.0'}, + ) + ]).create(); + await pubGet( + error: allOf( + [ + contains( + '* Consider downgrading your constraint on foo: dart pub add foo:^0.9.0', + ), + contains( + '* Try upgrading your constraint on bar: dart pub add dev:bar:^2.0.0', + ), + ], + ), + ); + }); + + test('suggests an update to an empty package constraint', () async { + final server = await servePackages(); + server.serve('foo', '1.0.0'); + + await d.dir(appPath, [ + d.libPubspec( + 'myApp', + '1.0.0', + deps: {'foo': '>1.0.0 <=0.0.0'}, + ) + ]).create(); + await pubGet( + error: allOf( + [ + contains( + '* Try updating your constraint on foo: dart pub add foo:^1.0.0', + ), + ], + ), + ); + }); + + test('suggests updates to multiple packages', () async { + final server = await servePackages(); + server.serve('foo', '1.0.0', deps: {'bar': '2.0.0'}); + server.serve('bar', '1.0.0', deps: {'foo': '2.0.0'}); + server.serve('foo', '2.0.0', deps: {'bar': '2.0.0'}); + server.serve('bar', '2.0.0', deps: {'foo': '2.0.0'}); + + await d.dir(appPath, [ + d.libPubspec( + 'myApp', + '1.0.0', + deps: {'foo': '1.0.0'}, + devDeps: {'bar': '1.0.0'}, + ) + ]).create(); + await pubGet( + error: contains( + '* Try updating the following constraints: dart pub add dev:bar:^2.0.0 foo:^2.0.0', + ), + ); + }); + + test('suggests a major upgrade if more than 5 needs to be upgraded', + () async { + final server = await servePackages(); + server.serve('foo', '1.0.0', deps: {'bar': '2.0.0'}); + server.serve('bar', '1.0.0', deps: {'foo': '2.0.0'}); + server.serve('foo', '2.0.0', deps: {'bar': '2.0.0'}); + server.serve('bar', '2.0.0', deps: {'foo': '2.0.0'}); + server.serve('foo1', '1.0.0', deps: {'bar1': '2.0.0'}); + server.serve('bar1', '1.0.0', deps: {'foo1': '2.0.0'}); + server.serve('foo1', '2.0.0', deps: {'bar1': '2.0.0'}); + server.serve('bar1', '2.0.0', deps: {'foo1': '2.0.0'}); + server.serve('foo2', '1.0.0', deps: {'bar2': '2.0.0'}); + server.serve('bar2', '1.0.0', deps: {'foo2': '2.0.0'}); + server.serve('foo2', '2.0.0', deps: {'bar2': '2.0.0'}); + server.serve('bar2', '2.0.0', deps: {'foo2': '2.0.0'}); + + await d.dir(appPath, [ + d.libPubspec( + 'myApp', + '1.0.0', + deps: { + 'foo': '1.0.0', + 'bar': '1.0.0', + 'foo1': '1.0.0', + 'bar1': '1.0.0', + 'foo2': '1.0.0', + 'bar2': '1.0.0', + }, + ) + ]).create(); + await pubGet( + error: contains( + '* Try an upgrade of your constraints: dart pub upgrade --major-versions', + ), + ); + }); + + test('suggests upgrades to non-default servers', () async { + final server = await servePackages(); + final server2 = await startPackageServer(); + server.serve( + 'foo', + '1.0.0', + deps: { + 'bar': {'version': '2.0.0', 'hosted': server2.url} + }, + ); + + server2.serve('bar', '1.0.0'); + server2.serve('bar', '2.0.0'); + + await d.dir(appPath, [ + d.libPubspec( + 'myApp', + '1.0.0', + deps: { + 'foo': '^1.0.0', + 'bar': {'version': '^1.0.0', 'hosted': server2.url}, + }, + ) + ]).create(); + await pubGet( + error: contains( + '* Try upgrading your constraint on bar: dart pub add ' + 'bar:\'{"version":"^2.0.0","hosted":"${server2.url}"}\'', + ), + ); + await pubAdd( + args: ['bar:{"version":"^2.0.0","hosted":"${server2.url}"}'], + ); + await d.dir(appPath, [ + d.libPubspec( + 'myApp', + '1.0.0', + deps: { + 'foo': '^1.0.0', + 'bar': {'version': '^2.0.0', 'hosted': server2.url}, + }, + ) + ]).validate(); + }); +} + +const releasesMockResponse = ''' +{ + "base_url": "https://storage.googleapis.com/flutter_infra_release/releases", + "current_release": { + "beta": "096162697a9cdc79f4e47f7230d70935fa81fd24", + "dev": "13a2fb10b838971ce211230f8ffdd094c14af02c", + "stable": "e3c29ec00c9c825c891d75054c63fcc46454dca1" + }, + "releases": [ + { + "hash": "e3c29ec00c9c825c891d75054c63fcc46454dca1", + "channel": "stable", + "version": "3.3.2", + "dart_sdk_version": "2.18.1", + "dart_sdk_arch": "x64", + "release_date": "2022-09-14T15:06:55.724077Z", + "archive": "stable/linux/flutter_linux_3.3.2-stable.tar.xz", + "sha256": "a733a75ae07c42b2059a31fc9d64fabfae5dccd15770fa6b7f290e3f5f9c98e8" + }, + { + "hash": "4f9d92fbbdf072a70a70d2179a9f87392b94104c", + "channel": "stable", + "version": "3.3.1", + "dart_sdk_version": "2.18.0", + "dart_sdk_arch": "x64", + "release_date": "2022-09-07T15:30:42.283999Z", + "archive": "stable/linux/flutter_linux_3.3.1-stable.tar.xz", + "sha256": "7cbcff0230affbe07a5ce82298044ac437e96aeba69f83656f9ed9a910a392e7" + }, + { + "hash": "ffccd96b62ee8cec7740dab303538c5fc26ac543", + "channel": "stable", + "version": "3.3.0", + "dart_sdk_version": "2.18.0", + "dart_sdk_arch": "x64", + "release_date": "2022-08-30T17:22:12.916008Z", + "archive": "stable/linux/flutter_linux_3.3.0-stable.tar.xz", + "sha256": "a92a27aa6d4454d7a1cf9f8a0a56e0e5d6865f2cfcd21cf52e57f7922ad5d504" + }, + { + "hash": "096162697a9cdc79f4e47f7230d70935fa81fd24", + "channel": "beta", + "version": "3.3.0-0.5.pre", + "dart_sdk_version": "2.18.0 (build 2.18.0-271.7.beta)", + "dart_sdk_arch": "x64", + "release_date": "2022-08-23T17:03:21.525151Z", + "archive": "beta/linux/flutter_linux_3.3.0-0.5.pre-beta.tar.xz", + "sha256": "8e07158a64a8ce79f9169cffe4ff23a486bdabb29401f13177672fae18de52d2" + } + ] +} +''';
diff --git a/test/testdata/goldens/directory_option_test/commands taking a --directory~-C parameter work.txt b/test/testdata/goldens/directory_option_test/commands taking a --directory~-C parameter work.txt index 2729283..a0df7ef 100644 --- a/test/testdata/goldens/directory_option_test/commands taking a --directory~-C parameter work.txt +++ b/test/testdata/goldens/directory_option_test/commands taking a --directory~-C parameter work.txt
@@ -98,11 +98,8 @@ ## Section 10 $ pub run -C myapp 'bin/app.dart' Building package executable... -[STDERR] Failed to build test_pkg:app: -[STDERR] myapp/bin/app.dart:1:1: Error: The specified language version is too high. The highest supported language version is 3.0. -[STDERR] main() => print('Hi'); -[STDERR] ^ -[EXIT CODE] 1 +Built test_pkg:app. +Hi -------------------------------- END OF OUTPUT ---------------------------------
diff --git a/test/testdata/goldens/help_test/pub publish --help.txt b/test/testdata/goldens/help_test/pub publish --help.txt index 621c940..30ebb01 100644 --- a/test/testdata/goldens/help_test/pub publish --help.txt +++ b/test/testdata/goldens/help_test/pub publish --help.txt
@@ -8,6 +8,7 @@ -h, --help Print this usage information. -n, --dry-run Validate but do not publish the package. -f, --force Publish without confirmation if there are no errors. + --skip-validation Publish without validation and resolution (this will ignore errors). -C, --directory=<dir> Run this in the directory <dir>. Run "pub help" to see global options.
diff --git a/test/testdata/goldens/outdated/outdated_test/newer versions available.txt b/test/testdata/goldens/outdated/outdated_test/newer versions available.txt index 7d91029..4a31e13 100644 --- a/test/testdata/goldens/outdated/outdated_test/newer versions available.txt +++ b/test/testdata/goldens/outdated/outdated_test/newer versions available.txt
@@ -54,6 +54,23 @@ } }, { + "package": "retracted", + "kind": "direct", + "isDiscontinued": false, + "current": { + "version": "1.0.1" + }, + "upgradable": { + "version": "1.0.1" + }, + "resolvable": { + "version": "1.0.1" + }, + "latest": { + "version": "1.0.0" + } + }, + { "package": "transitive", "kind": "transitive", "isDiscontinued": false, @@ -110,6 +127,7 @@ direct dependencies: foo *1.2.3 *1.3.0 *2.0.0 3.0.0 +retracted *1.0.1 *1.0.1 *1.0.1 1.0.0 dev_dependencies: builder *1.2.3 *1.3.0 2.0.0 2.0.0 @@ -131,6 +149,7 @@ direct dependencies: foo *1.2.3 *1.3.0 *2.0.0 3.0.0 +retracted *1.0.1 *1.0.1 *1.0.1 1.0.0 dev_dependencies: builder *1.2.3 *1.3.0 2.0.0 2.0.0 @@ -154,6 +173,7 @@ bar 1.0.0 1.0.0 1.0.0 1.0.0 foo *1.2.3 *1.3.0 *2.0.0 3.0.0 local_package 0.0.1 (path) 0.0.1 (path) 0.0.1 (path) 0.0.1 (path) +retracted *1.0.1 *1.0.1 *1.0.1 1.0.0 dev_dependencies: builder *1.2.3 *1.3.0 2.0.0 2.0.0 @@ -175,6 +195,7 @@ direct dependencies: foo *1.2.3 *1.3.0 *2.0.0 3.0.0 +retracted *1.0.1 *1.0.1 *1.0.1 1.0.0 dev_dependencies: builder *1.2.3 *1.3.0 *2.0.0 3.0.0-alpha @@ -196,6 +217,7 @@ direct dependencies: foo *1.2.3 *1.3.0 3.0.0 3.0.0 +retracted *1.0.1 *1.0.1 *1.0.1 1.0.0 1 upgradable dependency is locked (in pubspec.lock) to an older version. To update it, use `dart pub upgrade`. @@ -214,6 +236,7 @@ direct dependencies: foo *1.2.3 *1.3.0 *2.0.0 3.0.0 +retracted *1.0.1 *1.0.1 *1.0.1 1.0.0 dev_dependencies: builder *1.2.3 *1.3.0 2.0.0 2.0.0 @@ -248,6 +271,23 @@ } }, { + "package": "retracted", + "kind": "direct", + "isDiscontinued": false, + "current": { + "version": "1.0.1" + }, + "upgradable": { + "version": "1.0.1" + }, + "resolvable": { + "version": "1.0.1" + }, + "latest": { + "version": "1.0.0" + } + }, + { "package": "transitive", "kind": "transitive", "isDiscontinued": false,
diff --git a/test/testdata/goldens/outdated/outdated_test/overridden dependencies.txt b/test/testdata/goldens/outdated/outdated_test/overridden dependencies.txt index 54bf561..3dd9640 100644 --- a/test/testdata/goldens/outdated/outdated_test/overridden dependencies.txt +++ b/test/testdata/goldens/outdated/outdated_test/overridden dependencies.txt
@@ -161,9 +161,6 @@ baz *2.0.0 (overridden) *1.0.0 2.0.0 2.0.0 foo *1.0.1 (overridden) *1.0.0 *1.0.0 2.0.0 -3 upgradable dependencies are locked (in pubspec.lock) to older versions. -To update these dependencies, use `dart pub upgrade`. - 1 dependency is constrained to a version that is older than a resolvable version. To update it, edit pubspec.yaml, or run `dart pub upgrade --major-versions`.
diff --git a/test/upgrade/upgrade_major_versions_test.dart b/test/upgrade/upgrade_major_versions_test.dart index 40c23ea..691b221 100644 --- a/test/upgrade/upgrade_major_versions_test.dart +++ b/test/upgrade/upgrade_major_versions_test.dart
@@ -277,5 +277,35 @@ d.packageConfigEntry(name: 'bar', version: '4.0.0'), ]).validate(); }); + + test('should take pubspec_overrides.yaml into account', () async { + await servePackages() + ..serve('foo', '1.0.0') + ..serve('foo', '2.0.0'); + await d.dir('bar', [d.libPubspec('bar', '1.0.0')]).create(); + await d.appDir( + dependencies: { + 'foo': '^1.0.0', + 'bar': '^1.0.0', + }, + ).create(); + await d.dir(appPath, [ + d.pubspecOverrides({ + 'dependency_overrides': { + 'bar': {'path': '../bar'} + } + }) + ]).create(); + + await pubGet(); + + await pubUpgrade( + args: ['--major-versions'], + output: allOf([ + contains('Changed 1 constraint in pubspec.yaml:'), + contains('foo: ^1.0.0 -> ^2.0.0'), + ]), + ); + }); }); }
diff --git a/test/validator/analyze_test.dart b/test/validator/analyze_test.dart index 5a1e8b4..55c9779 100644 --- a/test/validator/analyze_test.dart +++ b/test/validator/analyze_test.dart
@@ -34,7 +34,7 @@ }); test( - 'follows analysis_options.yaml and should warn if package contains errors in pubspec.yaml', + 'follows analysis_options.yaml and should not warn if package contains only infos', () async { await d.dir(appPath, [ d.libPubspec( @@ -53,6 +53,32 @@ ''') ]).create(); + await expectValidation(); + }); + + test( + 'follows analysis_options.yaml and should warn if package contains warnings in pubspec.yaml', + () async { + await d.dir(appPath, [ + d.libPubspec( + 'test_pkg', '1.0.0', + sdk: '^3.0.0', + // Using http where https is recommended. + extras: {'repository': 'http://repo.org/'}, + ), + d.file('LICENSE', 'Eh, do what you want.'), + d.file('README.md', "This package isn't real."), + d.file('CHANGELOG.md', '# 1.0.0\nFirst version\n'), + d.file('analysis_options.yaml', ''' +linter: + rules: + - secure_pubspec_urls +analyzer: + errors: + secure_pubspec_urls: warning +''') + ]).create(); + await expectValidation( error: allOf([ contains(
diff --git a/test/validator/language_version_test.dart b/test/validator/language_version_test.dart index 5a198ae..c1e5f75 100644 --- a/test/validator/language_version_test.dart +++ b/test/validator/language_version_test.dart
@@ -2,6 +2,10 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:io'; + +import 'package:pub/src/language_version.dart'; +import 'package:pub_semver/pub_semver.dart'; import 'package:test/test.dart'; import '../descriptor.dart' as d; @@ -57,9 +61,12 @@ group('should warn if it', () { test('opts in to a newer version.', () async { + final nextVersion = + Version.parse(Platform.version.split(' ').first).nextMajor; await setup( sdkConstraint: '^3.0.0', - libraryLanguageVersion: '3.1', + libraryLanguageVersion: + LanguageVersion.fromVersion(nextVersion).toString(), ); await expectValidationWarning( 'The language version override can\'t specify a version greater than the latest known language version',
diff --git a/test/version_solver_test.dart b/test/version_solver_test.dart index 17c5be2..df3ae2b 100644 --- a/test/version_solver_test.dart +++ b/test/version_solver_test.dart
@@ -200,8 +200,8 @@ // Issue 1853 test( - "produces a nice message for a locked dependency that's the only " - 'version of its package', () async { + "produces a nice message for a locked dependency that's the only version of its package", + () async { await servePackages() ..serve('foo', '1.0.0', deps: {'bar': '>=2.0.0'}) ..serve('bar', '1.0.0') @@ -212,11 +212,9 @@ await d.appDir(dependencies: {'foo': 'any', 'bar': '<2.0.0'}).create(); await expectResolves( - error: equalsIgnoringWhitespace(''' - Because myapp depends on foo any which depends on bar >=2.0.0, - bar >=2.0.0 is required. - So, because myapp depends on bar <2.0.0, version solving failed. - '''), + error: contains(''' +Because myapp depends on foo any which depends on bar >=2.0.0, bar >=2.0.0 is required. +So, because myapp depends on bar <2.0.0, version solving failed.'''), ); }); } @@ -336,11 +334,9 @@ ]).create(); await expectResolves( - error: equalsIgnoringWhitespace(''' - Because no versions of foo match ^2.0.0 and myapp depends on foo - >=1.0.0 <3.0.0, foo ^1.0.0 is required. - So, because myapp depends on foo >=2.0.0 <4.0.0, version solving failed. - '''), + error: contains(''' +Because no versions of foo match ^2.0.0 and myapp depends on foo >=1.0.0 <3.0.0, foo ^1.0.0 is required. +So, because myapp depends on foo >=2.0.0 <4.0.0, version solving failed.'''), ); }); @@ -357,11 +353,9 @@ ]).create(); await expectResolves( - error: equalsIgnoringWhitespace(''' - Because no versions of foo match ^2.0.0 and myapp depends on foo - >=1.0.0 <3.0.0, foo ^1.0.0 is required. - So, because myapp depends on foo >=2.0.0 <4.0.0, version solving failed. - '''), + error: contains(''' +Because no versions of foo match ^2.0.0 and myapp depends on foo >=1.0.0 <3.0.0, foo ^1.0.0 is required. +So, because myapp depends on foo >=2.0.0 <4.0.0, version solving failed.'''), ); }); @@ -378,10 +372,9 @@ ]).create(); await expectResolves( - error: equalsIgnoringWhitespace(''' - Because myapp depends on both foo ^1.0.0 and foo ^2.0.0, version - solving failed. - '''), + error: contains( + 'Because myapp depends on both foo ^1.0.0 and foo ^2.0.0, version solving failed.', + ), ); }); @@ -441,10 +434,8 @@ await d.appDir(dependencies: {'foo': '>=1.0.0 <2.0.0'}).create(); await expectResolves( - error: equalsIgnoringWhitespace(""" - Because myapp depends on foo ^1.0.0 which doesn't match any versions, - version solving failed. - """), + error: contains(''' +Because myapp depends on foo ^1.0.0 which doesn't match any versions, version solving failed.'''), ); }); @@ -575,11 +566,10 @@ ..serve('b', '1.0.0'); await d.appDir(dependencies: {'a': 'any', 'b': '>1.0.0'}).create(); + await expectResolves( - error: equalsIgnoringWhitespace(""" - Because myapp depends on b >1.0.0 which doesn't match any versions, - version solving failed. - """), + error: contains(''' +Because myapp depends on b >1.0.0 which doesn't match any versions, version solving failed.'''), ); }); @@ -1115,11 +1105,10 @@ ]).create(); await expectResolves( - error: equalsIgnoringWhitespace(''' - The current Dart SDK version is 3.1.2+3. + error: contains(''' +The current Dart SDK version is 3.1.2+3. - Because myapp requires SDK version 2.12.0, version solving failed. - '''), +Because myapp requires SDK version 2.12.0, version solving failed.'''), ); });