diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b5dc528..dacfae0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml
@@ -24,7 +24,7 @@ matrix: sdk: [dev] steps: - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 - uses: dart-lang/setup-dart@f0ead981b4d9a35b37f30d36160575d60931ec30 with: sdk: ${{ matrix.sdk }} @@ -52,7 +52,7 @@ sdk: [dev] shard: [0, 1, 2, 3, 4, 5, 6] steps: - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 - uses: dart-lang/setup-dart@f0ead981b4d9a35b37f30d36160575d60931ec30 with: sdk: ${{ matrix.sdk }}
diff --git a/lib/src/ascii_tree.dart b/lib/src/ascii_tree.dart index fbd9c84..cf7ff43 100644 --- a/lib/src/ascii_tree.dart +++ b/lib/src/ascii_tree.dart
@@ -72,9 +72,12 @@ baseDir == null ? file : p.relative(file, from: baseDir); final parts = p.split(relativeFile); if (showFileSizes) { - final size = File(p.normalize(file)).statSync().size; - final sizeString = _readableFileSize(size); - parts.last = '${parts.last} $sizeString'; + final stat = File(p.normalize(file)).statSync(); + if (stat.type != FileSystemEntityType.directory) { + final size = stat.size; + final sizeString = _readableFileSize(size); + parts.last = '${parts.last} $sizeString'; + } } var directory = root; for (var part in parts) {
diff --git a/lib/src/command/dependency_services.dart b/lib/src/command/dependency_services.dart index 63b81cc..5f3eb72 100644 --- a/lib/src/command/dependency_services.dart +++ b/lib/src/command/dependency_services.dart
@@ -25,6 +25,7 @@ import '../package_name.dart'; import '../pubspec.dart'; import '../pubspec_utils.dart'; +import '../sdk.dart'; import '../solver.dart'; import '../solver/version_solver.dart'; import '../source/git.dart'; @@ -56,6 +57,7 @@ @override Future<void> runProtected() async { + _checkAtRoot(entrypoint); final stdinString = await utf8.decodeStream(stdin); final input = json.decode(stdinString.isEmpty ? '{}' : stdinString) as Map<String, Object?>; @@ -65,27 +67,21 @@ throw FormatException('"target" should be a String.'); } - final compatiblePubspec = - stripDependencyOverrides(entrypoint.workspaceRoot.pubspec); + final compatibleWorkspace = entrypoint.workspaceRoot + .transformWorkspace((p) => stripDependencyOverrides(p.pubspec)); - final breakingPubspec = stripVersionBounds(compatiblePubspec); + final breakingWorkspace = compatibleWorkspace.transformWorkspace( + (p) => stripVersionBounds(p.pubspec), + ); final compatiblePackagesResult = await _tryResolve( - Package( - compatiblePubspec, - entrypoint.workspaceRoot.dir, - entrypoint.workspaceRoot.workspaceChildren, - ), + compatibleWorkspace, cache, additionalConstraints: additionalConstraints, ); final breakingPackagesResult = await _tryResolve( - Package( - breakingPubspec, - entrypoint.workspaceRoot.dir, - entrypoint.workspaceRoot.workspaceChildren, - ), + breakingWorkspace, cache, additionalConstraints: additionalConstraints, ); @@ -105,22 +101,19 @@ ?.firstWhereOrNull((element) => element.name == package.name); final multiBreakingVersion = breakingPackagesResult ?.firstWhereOrNull((element) => element.name == package.name); - final singleBreakingPubspec = compatiblePubspec.copyWith(); - final dependencySet = - _dependencySetOfPackage(singleBreakingPubspec, package); - final kind = _kindString(compatiblePubspec, package.name); + final kind = _kindString(compatibleWorkspace, package.name); PackageId? singleBreakingVersion; - if (dependencySet != null) { - dependencySet[package.name] = package - .toRef() - .withConstraint(stripUpperBound(package.toRange().constraint)); + + if (kind != 'transitive') { + final singleBreakingWorkspace = compatibleWorkspace.transformWorkspace( + (p) { + final r = stripVersionBounds(p.pubspec, stripOnly: [package.name]); + return r; + }, + ); final singleBreakingPackagesResult = await _tryResolve( - Package( - singleBreakingPubspec, - entrypoint.workspaceRoot.dir, - entrypoint.workspaceRoot.workspaceChildren, - ), + singleBreakingWorkspace, cache, ); singleBreakingVersion = singleBreakingPackagesResult @@ -131,17 +124,15 @@ (c) => c.range.toRef() == package.toRef() && !c.range.allows(package), )) { // Current version disallowed by restrictions. - final atLeastCurrentPubspec = atLeastCurrent( - compatiblePubspec, - entrypoint.lockFile.packages.values.toList(), + final atLeastCurrentWorkspace = compatibleWorkspace.transformWorkspace( + (p) => atLeastCurrent( + p.pubspec, + entrypoint.lockFile.packages.values.toList(), + ), ); final smallestUpgradeResult = await _tryResolve( - Package( - atLeastCurrentPubspec, - entrypoint.workspaceRoot.dir, - entrypoint.workspaceRoot.workspaceChildren, - ), + atLeastCurrentWorkspace, cache, solveType: SolveType.downgrade, additionalConstraints: additionalConstraints, @@ -156,7 +147,7 @@ _UpgradeType upgradeType, ) async { return await _computeUpgradeSet( - compatiblePubspec, + compatibleWorkspace, package, entrypoint, cache, @@ -174,8 +165,8 @@ 'latest': (await cache.getLatest(package.toRef(), version: package.version)) ?.versionOrHash(), - 'constraint': - _constraintOf(compatiblePubspec, package.name)?.toString(), + 'constraint': _constraintIntersection(compatibleWorkspace, package.name) + ?.toString(), 'compatible': await computeUpgradeSet( compatibleVersion, _UpgradeType.compatible, @@ -225,16 +216,11 @@ @override Future<void> runProtected() async { - final pubspec = entrypoint.workspaceRoot.pubspec; - + _checkAtRoot(entrypoint); final currentPackages = fileExists(entrypoint.lockFilePath) ? entrypoint.lockFile.packages.values.toList() : (await _tryResolve( - Package( - pubspec, - entrypoint.workspaceRoot.dir, - entrypoint.workspaceRoot.workspaceChildren, - ), + entrypoint.workspaceRoot, cache, ) ?? <PackageId>[]); @@ -246,8 +232,10 @@ dependencies.add({ 'name': package.name, 'version': package.versionOrHash(), - 'kind': _kindString(pubspec, package.name), - 'constraint': _constraintOf(pubspec, package.name).toString(), + 'kind': _kindString(entrypoint.workspaceRoot, package.name), + 'constraint': + _constraintIntersection(entrypoint.workspaceRoot, package.name) + ?.toString(), 'source': _source(package, containingDir: directory), }); } @@ -303,7 +291,6 @@ @override Future<void> runProtected() async { - YamlEditor(readTextFile(entrypoint.workspaceRoot.pubspecPath)); final toApply = <_PackageVersion>[]; final input = json.decode(await utf8.decodeStream(stdin)); for (final change in input['dependencyChanges'] as Iterable) { @@ -317,10 +304,46 @@ ), ); } - - final pubspec = entrypoint.workspaceRoot.pubspec; - final pubspecEditor = - YamlEditor(readTextFile(entrypoint.workspaceRoot.pubspecPath)); + final updatedPubspecs = <String, YamlEditor>{}; + _checkAtRoot(entrypoint); + for (final package in entrypoint.workspaceRoot.transitiveWorkspace) { + final pubspec = package.pubspec; + final pubspecEditor = YamlEditor(readTextFile(package.pubspecPath)); + for (final p in toApply) { + final targetConstraint = p.constraint; + final targetPackage = p.name; + final targetVersion = p.version; + late final section = pubspec.dependencies[targetPackage] != null + ? 'dependencies' + : 'dev_dependencies'; + if (targetConstraint != null) { + final packageConfig = + pubspecEditor.parseAt([section, targetPackage]).value; + if (packageConfig == null || packageConfig is String) { + pubspecEditor + .update([section, targetPackage], targetConstraint.toString()); + } else if (packageConfig is Map) { + pubspecEditor.update( + [section, targetPackage, 'version'], + targetConstraint.toString(), + ); + } else { + fail( + 'The dependency $targetPackage does not have a map or string as a description', + ); + } + } else if (targetVersion != null) { + final constraint = _constraintOf(pubspec, targetPackage); + if (constraint != null && !constraint.allows(targetVersion)) { + pubspecEditor.update( + [section, targetPackage], + VersionConstraint.compatibleWith(targetVersion).toString(), + ); + } + } + updatedPubspecs[package.dir] = pubspecEditor; + } + } final lockFile = fileExists(entrypoint.lockFilePath) ? readTextFile(entrypoint.lockFilePath) : null; @@ -331,40 +354,8 @@ for (final p in toApply) { final targetPackage = p.name; final targetVersion = p.version; - final targetConstraint = p.constraint; final targetRevision = p.gitRevision; - if (targetConstraint != null) { - final section = pubspec.dependencies[targetPackage] != null - ? 'dependencies' - : 'dev_dependencies'; - final packageConfig = - pubspecEditor.parseAt([section, targetPackage]).value; - if (packageConfig == null || packageConfig is String) { - pubspecEditor - .update([section, targetPackage], targetConstraint.toString()); - } else if (packageConfig is Map) { - pubspecEditor.update( - [section, targetPackage, 'version'], - targetConstraint.toString(), - ); - } else { - fail( - 'The dependency $targetPackage does not have a map or string as a description', - ); - } - } else if (targetVersion != null) { - final constraint = _constraintOf(pubspec, targetPackage); - if (constraint != null && !constraint.allows(targetVersion)) { - final section = pubspec.dependencies[targetPackage] != null - ? 'dependencies' - : 'dev_dependencies'; - pubspecEditor.update( - [section, targetPackage], - VersionConstraint.compatibleWith(targetVersion).toString(), - ); - } - } if (lockFileEditor != null) { if (targetVersion != null && (lockFileYaml['packages'] as Map).containsKey(targetPackage)) { @@ -431,7 +422,14 @@ ); await log.errorsOnlyUnlessTerminal( () async { - final updatedPubspec = pubspecEditor.toString(); + final updatedWorkspace = entrypoint.workspaceRoot.transformWorkspace( + (package) => Pubspec.parse( + updatedPubspecs[package.dir].toString(), + cache.sources, + location: toUri(package.pubspecPath), + containingDescription: RootDescription(package.dir), + ), + ); // Resolve versions, this will update transitive dependencies that were // not passed in the input. And also counts as a validation of the input // by ensuring the resolution is valid. @@ -442,21 +440,17 @@ final solveResult = await resolveVersions( SolveType.get, cache, - Package( - Pubspec.parse( - updatedPubspec, - cache.sources, - location: toUri(entrypoint.workspaceRoot.pubspecPath), - containingDescription: - RootDescription(entrypoint.workspaceRoot.dir), - ), - entrypoint.workspaceRoot.dir, - entrypoint.workspaceRoot.workspaceChildren, - ), + updatedWorkspace, lockFile: updatedLockfile, ); - if (pubspecEditor.edits.isNotEmpty) { - writeTextFile(entrypoint.workspaceRoot.pubspecPath, updatedPubspec); + for (final package in entrypoint.workspaceRoot.transitiveWorkspace) { + final updatedPubspec = updatedPubspecs[package.dir]!; + if (updatedPubspec.edits.isNotEmpty) { + writeTextFile( + package.pubspecPath, + updatedPubspec.toString(), + ); + } } // Only if we originally had a lock-file we write the resulting lockfile back. if (updatedLockfile != null) { @@ -521,9 +515,9 @@ final newLockFile = LockFile( updatedPackages, sdkConstraints: updatedLockfile.sdkConstraints, - mainDependencies: pubspec.dependencies.keys.toSet(), - devDependencies: pubspec.devDependencies.keys.toSet(), - overriddenDependencies: pubspec.dependencyOverrides.keys.toSet(), + mainDependencies: entrypoint.lockFile.mainDependencies, + devDependencies: entrypoint.lockFile.devDependencies, + overriddenDependencies: entrypoint.lockFile.overriddenDependencies, ); newLockFile.writeToFile(entrypoint.lockFilePath, cache); @@ -535,6 +529,12 @@ } } +void _checkAtRoot(Entrypoint entrypoint) { + if (entrypoint.workspaceRoot != entrypoint.workPackage) { + fail('Only apply dependency_services to the root of the workspace.'); + } +} + class _PackageVersion { String name; Version? version; @@ -681,16 +681,33 @@ return solveResult?.packages; } +VersionConstraint? _constraintIntersection( + Package workspace, + String packageName, +) { + final constraints = workspace.transitiveWorkspace + .map((p) => _constraintOf(p.pubspec, packageName)) + .whereNotNull(); + if (constraints.isEmpty) { + return null; + } + return constraints + .reduce((a, b) => a.intersect(b)) + .asCompatibleWithIfPossible(); +} + VersionConstraint? _constraintOf(Pubspec pubspec, String packageName) { return (pubspec.dependencies[packageName] ?? pubspec.devDependencies[packageName]) ?.constraint; } -String _kindString(Pubspec pubspec, String packageName) { - return pubspec.dependencies.containsKey(packageName) +String _kindString(Package workspace, String packageName) { + return workspace.transitiveWorkspace + .any((p) => p.dependencies.containsKey(packageName)) ? 'direct' - : pubspec.devDependencies.containsKey(packageName) + : workspace.transitiveWorkspace + .any((p) => p.devDependencies.containsKey(packageName)) ? 'dev' : 'transitive'; } @@ -721,12 +738,14 @@ key: (e) => (e as PackageId).name, ); } - currentPackages.remove(entrypoint.workspaceRoot.name); + for (final p in entrypoint.workspaceRoot.transitiveWorkspace) { + currentPackages.remove(p.name); + } return currentPackages; } Future<List<Object>> _computeUpgradeSet( - Pubspec rootPubspec, + Package workspace, PackageId? package, Entrypoint entrypoint, SystemCache cache, { @@ -736,16 +755,18 @@ }) async { if (package == null) return []; final lockFile = entrypoint.lockFile; - final pubspec = (upgradeType == _UpgradeType.multiBreaking || + final upgradedWorkspace = (upgradeType == _UpgradeType.multiBreaking || upgradeType == _UpgradeType.smallestUpdate) - ? stripVersionBounds(rootPubspec) - : rootPubspec.copyWith(); + ? workspace.transformWorkspace((p) => stripVersionBounds(p.pubspec)) + : workspace.transformWorkspace((p) => p.pubspec.copyWith()); - final dependencySet = _dependencySetOfPackage(pubspec, package); - if (dependencySet != null) { - // Force the version to be the new version. - dependencySet[package.name] = - package.toRef().withConstraint(package.toRange().constraint); + for (final p in upgradedWorkspace.transitiveWorkspace) { + final dependencySet = _dependencySetOfPackage(p.pubspec, package); + if (dependencySet != null) { + // Force the version to be the new version. + dependencySet[package.name] = + package.toRef().withConstraint(package.toRange().constraint); + } } final resolution = await tryResolveVersions( @@ -753,11 +774,7 @@ ? SolveType.downgrade : SolveType.get, cache, - Package( - pubspec, - entrypoint.workspaceRoot.dir, - entrypoint.workspaceRoot.workspaceChildren, - ), + upgradedWorkspace, lockFile: lockFile, additionalConstraints: additionalConstraints, ); @@ -766,40 +783,43 @@ if (resolution == null) { return []; } - + final workspaceNames = { + ...workspace.transitiveWorkspace.map((p) => p.name), + }; return [ ...resolution.packages.where((r) { - if (r.name == rootPubspec.name) return false; + if (workspaceNames.contains(r.name)) return false; final originalVersion = currentPackages[r.name]; return originalVersion == null || r != originalVersion; }).map((p) { - final depset = _dependencySetOfPackage(rootPubspec, p); - final originalConstraint = depset?[p.name]?.constraint; + final constraintIntersection = _constraintIntersection(workspace, p.name); final currentPackage = currentPackages[p.name]; return { 'name': p.name, 'version': p.versionOrHash(), - 'kind': _kindString(pubspec, p.name), + 'kind': _kindString(workspace, p.name), 'source': _source(p, containingDir: entrypoint.workspaceRoot.dir), - 'constraintBumped': originalConstraint == null + 'constraintBumped': constraintIntersection == null ? null : upgradeType == _UpgradeType.compatible - ? originalConstraint.toString() - : _bumpConstraint(originalConstraint, p.version).toString(), - 'constraintWidened': originalConstraint == null + ? constraintIntersection.toString() + : _bumpConstraint(constraintIntersection, p.version).toString(), + 'constraintWidened': constraintIntersection == null ? null : upgradeType == _UpgradeType.compatible - ? originalConstraint.toString() - : _widenConstraint(originalConstraint, p.version).toString(), - 'constraintBumpedIfNeeded': originalConstraint == null + ? constraintIntersection.toString() + : _widenConstraint(constraintIntersection, p.version) + .toString(), + 'constraintBumpedIfNeeded': constraintIntersection == null ? null : upgradeType == _UpgradeType.compatible - ? originalConstraint.toString() - : originalConstraint.allows(p.version) - ? originalConstraint.toString() - : _bumpConstraint(originalConstraint, p.version).toString(), + ? constraintIntersection.toString() + : constraintIntersection.allows(p.version) + ? constraintIntersection.toString() + : _bumpConstraint(constraintIntersection, p.version) + .toString(), 'previousVersion': currentPackage?.versionOrHash(), - 'previousConstraint': originalConstraint?.toString(), + 'previousConstraint': constraintIntersection?.toString(), 'previousSource': currentPackage == null ? null : _source(
diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart index d24d09e..26638aa 100644 --- a/lib/src/command/lish.dart +++ b/lib/src/command/lish.dart
@@ -303,7 +303,26 @@ await entrypoint.acquireDependencies(SolveType.get); } - final files = entrypoint.workPackage.listFiles(); + // For displaying the layout we only want to explicitly mention non-empty + // directories, so first we list all files and directories, and then filter + // any non-empty directories away. + // For validation it is practical to also maintain the list of files. + final filesAndDirs = entrypoint.workPackage.listFiles(includeDirs: true); + + final files = <String>[]; + final filesAndEmptyDirs = <String>[]; + for (final entry in filesAndDirs) { + final stat = statPath(entry); + if (stat.type == FileSystemEntityType.directory) { + if (listDir(entry).isEmpty) { + filesAndEmptyDirs.add(entry); + } + } else { + files.add(entry); + filesAndEmptyDirs.add(entry); + } + } + log.fine('Archiving and publishing ${entrypoint.workPackage.name}.'); // Show the package contents so the user can verify they look OK. @@ -311,11 +330,13 @@ final host = computeHost(package.pubspec); log.message( 'Publishing ${package.name} ${package.version} to $host:\n' - '${tree.fromFiles(files, baseDir: entrypoint.workPackage.dir, showFileSizes: true)}', + '${tree.fromFiles(filesAndEmptyDirs, baseDir: entrypoint.workPackage.dir, showFileSizes: true)}', ); - final packageBytes = - await createTarGz(files, baseDir: entrypoint.workPackage.dir).toBytes(); + final packageBytes = await createTarGz( + filesAndDirs, + baseDir: entrypoint.workPackage.dir, + ).toBytes(); log.message( '\nTotal compressed archive size: ${_readableFileSize(packageBytes.length)}.\n',
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index cacd868..bd78184 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart
@@ -565,7 +565,7 @@ final report = SolveReport( type, - workspaceRoot.dir, + workspaceRoot.presentationDir, workspaceRoot.pubspec, workspaceRoot.allOverridesInWorkspace, lockFile,
diff --git a/lib/src/io.dart b/lib/src/io.dart index ec92718..730962b 100644 --- a/lib/src/io.dart +++ b/lib/src/io.dart
@@ -14,6 +14,7 @@ import 'package:async/async.dart'; import 'package:cli_util/cli_util.dart' show EnvironmentNotFoundException, applicationConfigHome; +import 'package:collection/collection.dart'; import 'package:http/http.dart' show ByteStream; import 'package:http_multi_server/http_multi_server.dart'; import 'package:meta/meta.dart'; @@ -99,6 +100,10 @@ return null; } +FileStat statPath(String path) { + return File(path).statSync(); +} + /// Returns the canonical path for [pathString]. /// /// This is the normalized, absolute path, with symlinks resolved. As in @@ -557,18 +562,26 @@ bool _isDirectoryNotEmptyException(FileSystemException e) { final errorCode = e.osError?.errorCode; return - // On Linux rename will fail with ENOTEMPTY if directory exists: - // https://man7.org/linux/man-pages/man2/rename.2.html - // #define ENOTEMPTY 39 /* Directory not empty */ + // On Linux rename will fail with either ENOTEMPTY or EEXISTS if directory + // exists: https://man7.org/linux/man-pages/man2/rename.2.html + // ``` + // #define ENOTEMPTY 39 /* Directory not empty */ + // #define EEXIST 17 /* File exists */ + // ``` + // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/asm-generic/errno-base.h#n21 // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/asm-generic/errno.h#n20 - (Platform.isLinux && errorCode == 39) || + (Platform.isLinux && (errorCode == 39 || errorCode == 17)) || // On Windows this may fail with ERROR_DIR_NOT_EMPTY or ERROR_ALREADY_EXISTS // https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499- (Platform.isWindows && (errorCode == 145 || errorCode == 183)) || // On MacOS rename will fail with ENOTEMPTY if directory exists. + // We also catch EEXIST - perhaps that could also be thrown... + // ``` // #define ENOTEMPTY 66 /* Directory not empty */ + // #define EEXIST 17 /* File exists */ + // ``` // https://github.com/apple-oss-distributions/xnu/blob/bb611c8fecc755a0d8e56e2fa51513527c5b7a0e/bsd/sys/errno.h#L190 - (Platform.isMacOS && errorCode == 66); + (Platform.isMacOS && (errorCode == 66 || errorCode == 17)); } /// Creates a new symlink at path [symlink] that points to [target]. @@ -1139,9 +1152,8 @@ /// Create a .tar.gz archive from a list of entries. /// -/// Each entry can be a [String], [Directory], or [File] object. The root of -/// the archive is considered to be [baseDir], which defaults to the current -/// working directory. +/// Each entry is the path to a directory or file. The root of the archive is +/// considered to be [baseDir], which defaults to the current working directory. /// /// Returns a [ByteStream] that emits the contents of the archive. ByteStream createTarGz( @@ -1159,7 +1171,7 @@ final tarContents = Stream.fromIterable( contents.map((entry) { entry = p.normalize(p.absolute(entry)); - if (!p.isWithin(baseDir, entry)) { + if (!p.equals(baseDir, entry) && !p.isWithin(baseDir, entry)) { throw ArgumentError('Entry $entry is not inside $baseDir.'); } @@ -1168,25 +1180,33 @@ final file = File(p.normalize(entry)); final stat = file.statSync(); + // Ensure paths in tar files use forward slashes + final name = p.url.joinAll(p.split(relative)); + if (stat.type == FileSystemEntityType.link) { log.message('$entry is a link locally, but will be uploaded as a ' 'duplicate file.'); } - - return TarEntry( - TarHeader( - // Ensure paths in tar files use forward slashes - name: p.url.joinAll(p.split(relative)), - // We want to keep executable bits, but otherwise use the default - // file mode - mode: _defaultMode | (stat.mode & _executableMask), - size: stat.size, - modified: stat.changed, - userName: 'pub', - groupName: 'pub', - ), - file.openRead(), - ); + if (stat.type == FileSystemEntityType.directory) { + return TarEntry( + TarHeader(name: name, typeFlag: TypeFlag.dir), + Stream.fromIterable([]), + ); + } else { + return TarEntry( + TarHeader( + name: name, + // We want to keep executable bits, but otherwise use the default + // file mode + mode: _defaultMode | (stat.mode & _executableMask), + size: stat.size, + modified: stat.changed, + userName: 'pub', + groupName: 'pub', + ), + file.openRead(), + ); + } }), );
diff --git a/lib/src/package.dart b/lib/src/package.dart index 1daf3a6..7a2178d 100644 --- a/lib/src/package.dart +++ b/lib/src/package.dart
@@ -255,7 +255,11 @@ /// directory) or absolute id [dir] is absolute. /// /// To convert them to paths relative to the package root, use [p.relative]. - List<String> listFiles({String? beneath, bool recursive = true}) { + List<String> listFiles({ + String? beneath, + bool recursive = true, + bool includeDirs = false, + }) { final packageDir = dir; final root = git.repoRoot(packageDir) ?? packageDir; beneath = p @@ -359,6 +363,7 @@ ); }, isDir: (dir) => dirExists(resolve(dir)), + includeDirs: includeDirs, ).map(resolve).toList(); }
diff --git a/lib/src/validator/sdk_constraint.dart b/lib/src/validator/sdk_constraint.dart index c44f9b8..b5a0adb 100644 --- a/lib/src/validator/sdk_constraint.dart +++ b/lib/src/validator/sdk_constraint.dart
@@ -11,7 +11,6 @@ /// A validator of the SDK constraint. /// /// Validates that a package's SDK constraint: -/// * doesn't use the "^" syntax. /// * has an upper bound. /// * is not depending on a prerelease, unless the package itself is a /// prerelease.
diff --git a/test/dependency_services/dependency_services_test.dart b/test/dependency_services/dependency_services_test.dart index 11ccb68..ab3516c 100644 --- a/test/dependency_services/dependency_services_test.dart +++ b/test/dependency_services/dependency_services_test.dart
@@ -18,24 +18,25 @@ import '../golden_file.dart'; import '../test_pub.dart'; -void manifestAndLockfile(GoldenTestContext context) { +void manifestAndLockfile(GoldenTestContext context, List<String> workspace) { String catFile(String filename) { final path = p.join(d.sandbox, appPath, filename); + final normalizedFilename = p.posix.joinAll(p.split(p.normalize(filename))); if (File(path).existsSync()) { final contents = File(path).readAsLinesSync().map(filterUnstableText); return ''' -\$ cat $filename +\$ cat $normalizedFilename ${contents.join('\n')}'''; } else { return ''' -\$ cat $filename -No such file $filename.'''; +\$ cat $normalizedFilename +No such file $normalizedFilename.'''; } } context.expectNextSection(''' -${catFile('pubspec.yaml')} +${workspace.map((path) => catFile(p.join(path, 'pubspec.yaml'))).join('\n')} ${catFile('pubspec.lock')} '''); } @@ -47,6 +48,7 @@ Future<String> runDependencyServices( List<String> args, { String stdin = '', + Map<String, String>? environment, }) async { final buffer = StringBuffer(); buffer.writeln('## Section ${args.join(' ')}'); @@ -61,6 +63,7 @@ environment: { ...getPubTestEnvironment(), '_PUB_TEST_DEFAULT_HOSTED_URL': globalServer.url, + ...?environment, }, workingDirectory: p.join(d.sandbox, appPath), ); @@ -96,11 +99,14 @@ Future<void> _listReportApply( GoldenTestContext context, List<_PackageVersion> upgrades, { + List<String> workspace = const ['.'], void Function(Map)? reportAssertions, + Map<String, String>? environment, }) async { - manifestAndLockfile(context); - await context.runDependencyServices(['list']); - final report = await context.runDependencyServices(['report']); + manifestAndLockfile(context, workspace); + await context.runDependencyServices(['list'], environment: environment); + final report = + await context.runDependencyServices(['report'], environment: environment); if (reportAssertions != null) { reportAssertions(json.decode(report) as Map); } @@ -108,8 +114,12 @@ 'dependencyChanges': upgrades, }); - await context.runDependencyServices(['apply'], stdin: input); - manifestAndLockfile(context); + await context.runDependencyServices( + ['apply'], + stdin: input, + environment: environment, + ); + manifestAndLockfile(context, workspace); } Future<void> _reportWithForbidden( @@ -118,7 +128,7 @@ void Function(Map)? resultAssertions, String? targetPackage, }) async { - manifestAndLockfile(context); + manifestAndLockfile(context, ['.']); final input = json.encode({ 'target': targetPackage, 'disallowed': [ @@ -136,7 +146,7 @@ } // await context.runDependencyServices(['apply'], stdin: input); - manifestAndLockfile(context); + manifestAndLockfile(context, ['.']); } Future<void> main() async { @@ -628,6 +638,83 @@ }, ); }); + + testWithGolden('can upgrade workspaces', (context) async { + (await servePackages()) + ..serve('foo', '1.2.3') + ..serve('foo', '2.2.3', deps: {'transitive': '^1.0.0'}) + ..serve('bar', '1.2.3') + ..serve('bar', '2.2.3') + ..serve('dev', '1.0.0') + ..serve('dev', '2.0.0') + ..serve('transitive', '1.0.0') + ..serveContentHashes = true; + + await dir(appPath, [ + libPubspec( + 'myapp', + '1.2.3', + extras: { + 'workspace': ['pkgs/a'], + }, + deps: { + 'foo': '^1.0.0', + 'bar': '^1.0.0', + }, + sdk: '^3.5.0', + ), + dir('pkgs', [ + dir('a', [ + libPubspec( + 'a', + '1.1.1', + deps: {'bar': '>=1.2.0 <1.5.0'}, + devDeps: { + 'foo': '^1.2.0', + 'dev': '^1.0.0', + }, + resolutionWorkspace: true, + ), + ]), + ]), + ]).create(); + await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}); + + final result = await Process.run( + Platform.resolvedExecutable, + [snapshot, 'list'], + environment: { + ...getPubTestEnvironment(), + '_PUB_TEST_SDK_VERSION': '3.5.0', + }, + workingDirectory: p.join(d.sandbox, appPath, 'pkgs', 'a'), + ); + + expect( + result.stderr, + contains( + 'Only apply dependency_services to the root of the workspace.', + ), + ); + expect(result.exitCode, 1); + + await _listReportApply( + context, + [_PackageVersion('foo', '2.2.3'), _PackageVersion('transitive', '1.0.0')], + workspace: ['.', p.join('pkgs', 'a')], + reportAssertions: (report) { + expect( + findChangeVersion(report, 'singleBreaking', 'foo'), + '2.2.3', + ); + expect( + findChangeVersion(report, 'singleBreaking', 'transitive'), + '1.0.0', + ); + }, + environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}, + ); + }); } dynamic findChangeVersion(dynamic json, String updateType, String name) {
diff --git a/test/lish/empy_directories_test.dart b/test/lish/empy_directories_test.dart new file mode 100644 index 0000000..0c24f50 --- /dev/null +++ b/test/lish/empy_directories_test.dart
@@ -0,0 +1,95 @@ +// Copyright (c) 2024, 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:path/path.dart' as p; +import 'package:pub/src/exit_codes.dart' as exit_codes; +import 'package:shelf/shelf.dart' as shelf; +import 'package:tar/tar.dart'; +import 'package:test/test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; +import 'utils.dart'; + +void main() { + test('archives and uploads empty directories in package', () async { + await d.validPackage().create(); + await d.dir(appPath, [ + d.dir('lib', [d.dir('empty')]), + ]).create(); + + await servePackages(); + await runPub( + args: ['publish', '--to-archive=archive.tar.gz'], + output: contains(''' +├── CHANGELOG.md (<1 KB) +├── LICENSE (<1 KB) +├── README.md (<1 KB) +├── lib +│ ├── empty +│ └── test_pkg.dart (<1 KB) +└── pubspec.yaml (<1 KB) +'''), + ); + expect( + File(p.join(d.sandbox, appPath, 'archive.tar.gz')).existsSync(), + isTrue, + ); + final tarReader = TarReader( + gzip.decoder.bind( + File(p.join(d.sandbox, appPath, 'archive.tar.gz')).openRead(), + ), + ); + final dirs = <String>[]; + while (await tarReader.moveNext()) { + final entry = tarReader.current; + if (entry.type == TypeFlag.dir) { + dirs.add(entry.name); + } + } + expect(dirs, ['.', 'lib', 'lib/empty']); + await d.credentialsFile(globalServer, 'access-token').create(); + final pub = await startPublish(globalServer); + + await confirmPublish(pub); + handleUploadForm(globalServer); + handleUpload(globalServer); + + globalServer.expect('GET', '/create', (request) { + return shelf.Response.ok( + jsonEncode({ + 'success': {'message': 'Package test_pkg 1.0.0 uploaded!'}, + }), + ); + }); + + expect(pub.stdout, emits(startsWith('Uploading...'))); + expect( + pub.stdout, + emits('Message from server: Package test_pkg 1.0.0 uploaded!'), + ); + await pub.shouldExit(exit_codes.SUCCESS); + }); + + test('Can download and unpack package with empty directory', () async { + final server = await servePackages(); + server.serve( + 'foo', + '1.0.0', + contents: [ + d.dir('lib', [d.dir('empty', [])]), + ], + ); + await d.appDir(dependencies: {'foo': '1.0.0'}).create(); + await pubGet(); + await d.hostedCache([ + d.dir('foo-1.0.0', [ + d.dir('lib', [d.dir('empty')]), + ]), + ]).validate(); + }); +}
diff --git a/test/test_pub.dart b/test/test_pub.dart index ad80b42..527d1af 100644 --- a/test/test_pub.dart +++ b/test/test_pub.dart
@@ -1052,6 +1052,17 @@ final entries = <TarEntry>[]; void addDescriptor(d.Descriptor descriptor, String path) { if (descriptor is d.DirectoryDescriptor) { + if (descriptor.contents.isEmpty) { + entries.add( + TarEntry( + TarHeader( + name: p.posix.join(path, descriptor.name), + typeFlag: TypeFlag.dir, + ), + Stream.fromIterable([]), + ), + ); + } for (final e in descriptor.contents) { addDescriptor(e, p.posix.join(path, descriptor.name)); }
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/No pubspec.lock.txt b/test/testdata/goldens/dependency_services/dependency_services_test/No pubspec.lock.txt index 168b486..1959aeb 100644 --- a/test/testdata/goldens/dependency_services/dependency_services_test/No pubspec.lock.txt +++ b/test/testdata/goldens/dependency_services/dependency_services_test/No pubspec.lock.txt
@@ -43,7 +43,7 @@ "name": "transitive", "version": "1.0.0", "kind": "transitive", - "constraint": "null", + "constraint": null, "source": { "type": "hosted", "description": {
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Removing transitive.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Removing transitive.txt index b665f33..7bbbac7 100644 --- a/test/testdata/goldens/dependency_services/dependency_services_test/Removing transitive.txt +++ b/test/testdata/goldens/dependency_services/dependency_services_test/Removing transitive.txt
@@ -48,7 +48,7 @@ "name": "transitive", "version": "1.0.0", "kind": "transitive", - "constraint": "null", + "constraint": null, "source": { "type": "hosted", "description": {
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/can upgrade workspaces.txt b/test/testdata/goldens/dependency_services/dependency_services_test/can upgrade workspaces.txt new file mode 100644 index 0000000..cf9e847 --- /dev/null +++ b/test/testdata/goldens/dependency_services/dependency_services_test/can upgrade workspaces.txt
@@ -0,0 +1,400 @@ +# GENERATED BY: test/dependency_services/dependency_services_test.dart + +$ cat pubspec.yaml +{"name":"myapp","version":"1.2.3","homepage":"https://pub.dev","description":"A package, I guess.","dependencies":{"foo":"^1.0.0","bar":"^1.0.0"},"environment":{"sdk":"^3.5.0"},"workspace":["pkgs/a"]} +$ cat pkgs/a/pubspec.yaml +{"name":"a","version":"1.1.1","homepage":"https://pub.dev","description":"A package, I guess.","dependencies":{"bar":">=1.2.0 <1.5.0"},"dev_dependencies":{"foo":"^1.2.0","dev":"^1.0.0"},"environment":{"sdk":"^3.5.0-0"},"resolution":"workspace"} +$ cat pubspec.lock +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + bar: + dependency: "direct main" + description: + name: bar + sha256: $SHA256 + url: "http://localhost:$PORT" + source: hosted + version: "1.2.3" + dev: + dependency: transitive + description: + name: dev + sha256: $SHA256 + url: "http://localhost:$PORT" + source: hosted + version: "1.0.0" + foo: + dependency: "direct main" + description: + name: foo + sha256: $SHA256 + url: "http://localhost:$PORT" + source: hosted + version: "1.2.3" +sdks: + dart: ">=3.5.0 <4.0.0" +-------------------------------- END OF OUTPUT --------------------------------- + +## Section list +$ echo '' | dependency_services list +{ + "dependencies": [ + { + "name": "bar", + "version": "1.2.3", + "kind": "direct", + "constraint": ">=1.2.0 <1.5.0", + "source": { + "type": "hosted", + "description": { + "name": "bar", + "url": "http://localhost:$PORT", + "sha256": "0b119406be305b6e65d33551008b5b72fdd810965f0df914478c940d5fe28e53" + } + } + }, + { + "name": "dev", + "version": "1.0.0", + "kind": "dev", + "constraint": "^1.0.0", + "source": { + "type": "hosted", + "description": { + "name": "dev", + "url": "http://localhost:$PORT", + "sha256": "fb990b7b071a76286080ee183e9ed4cd6d5538f8e1eccce8c1f2caf50c51c1bc" + } + } + }, + { + "name": "foo", + "version": "1.2.3", + "kind": "direct", + "constraint": "^1.2.0", + "source": { + "type": "hosted", + "description": { + "name": "foo", + "url": "http://localhost:$PORT", + "sha256": "b2b7fc405959806aa1f31ac7e68752534f66f66a11a280d9878ecb6cd835f01c" + } + } + } + ] +} + +-------------------------------- END OF OUTPUT --------------------------------- + +## Section report +$ echo '' | dependency_services report +{ + "dependencies": [ + { + "name": "bar", + "version": "1.2.3", + "kind": "direct", + "source": { + "type": "hosted", + "description": { + "name": "bar", + "url": "http://localhost:$PORT", + "sha256": "0b119406be305b6e65d33551008b5b72fdd810965f0df914478c940d5fe28e53" + } + }, + "latest": "2.2.3", + "constraint": ">=1.2.0 <1.5.0", + "compatible": [], + "singleBreaking": [ + { + "name": "bar", + "version": "2.2.3", + "kind": "direct", + "source": { + "type": "hosted", + "description": { + "name": "bar", + "url": "http://localhost:$PORT", + "sha256": "18169c0899ff5f0551a80839dc1618e597a6ee4508a5065f9318dd2a2fda6455" + } + }, + "constraintBumped": "^2.2.3", + "constraintWidened": ">=1.2.0 <3.0.0", + "constraintBumpedIfNeeded": "^2.2.3", + "previousVersion": "1.2.3", + "previousConstraint": ">=1.2.0 <1.5.0", + "previousSource": { + "type": "hosted", + "description": { + "name": "bar", + "url": "http://localhost:$PORT", + "sha256": "0b119406be305b6e65d33551008b5b72fdd810965f0df914478c940d5fe28e53" + } + } + } + ], + "multiBreaking": [ + { + "name": "bar", + "version": "2.2.3", + "kind": "direct", + "source": { + "type": "hosted", + "description": { + "name": "bar", + "url": "http://localhost:$PORT", + "sha256": "18169c0899ff5f0551a80839dc1618e597a6ee4508a5065f9318dd2a2fda6455" + } + }, + "constraintBumped": "^2.2.3", + "constraintWidened": ">=1.2.0 <3.0.0", + "constraintBumpedIfNeeded": "^2.2.3", + "previousVersion": "1.2.3", + "previousConstraint": ">=1.2.0 <1.5.0", + "previousSource": { + "type": "hosted", + "description": { + "name": "bar", + "url": "http://localhost:$PORT", + "sha256": "0b119406be305b6e65d33551008b5b72fdd810965f0df914478c940d5fe28e53" + } + } + } + ] + }, + { + "name": "dev", + "version": "1.0.0", + "kind": "dev", + "source": { + "type": "hosted", + "description": { + "name": "dev", + "url": "http://localhost:$PORT", + "sha256": "fb990b7b071a76286080ee183e9ed4cd6d5538f8e1eccce8c1f2caf50c51c1bc" + } + }, + "latest": "2.0.0", + "constraint": "^1.0.0", + "compatible": [], + "singleBreaking": [ + { + "name": "dev", + "version": "2.0.0", + "kind": "dev", + "source": { + "type": "hosted", + "description": { + "name": "dev", + "url": "http://localhost:$PORT", + "sha256": "e3496752d80b78354cd745f8d1e381c42022b14624233c9e5306711171418d09" + } + }, + "constraintBumped": "^2.0.0", + "constraintWidened": ">=1.0.0 <3.0.0", + "constraintBumpedIfNeeded": "^2.0.0", + "previousVersion": "1.0.0", + "previousConstraint": "^1.0.0", + "previousSource": { + "type": "hosted", + "description": { + "name": "dev", + "url": "http://localhost:$PORT", + "sha256": "fb990b7b071a76286080ee183e9ed4cd6d5538f8e1eccce8c1f2caf50c51c1bc" + } + } + } + ], + "multiBreaking": [ + { + "name": "dev", + "version": "2.0.0", + "kind": "dev", + "source": { + "type": "hosted", + "description": { + "name": "dev", + "url": "http://localhost:$PORT", + "sha256": "e3496752d80b78354cd745f8d1e381c42022b14624233c9e5306711171418d09" + } + }, + "constraintBumped": "^2.0.0", + "constraintWidened": ">=1.0.0 <3.0.0", + "constraintBumpedIfNeeded": "^2.0.0", + "previousVersion": "1.0.0", + "previousConstraint": "^1.0.0", + "previousSource": { + "type": "hosted", + "description": { + "name": "dev", + "url": "http://localhost:$PORT", + "sha256": "fb990b7b071a76286080ee183e9ed4cd6d5538f8e1eccce8c1f2caf50c51c1bc" + } + } + } + ] + }, + { + "name": "foo", + "version": "1.2.3", + "kind": "direct", + "source": { + "type": "hosted", + "description": { + "name": "foo", + "url": "http://localhost:$PORT", + "sha256": "b2b7fc405959806aa1f31ac7e68752534f66f66a11a280d9878ecb6cd835f01c" + } + }, + "latest": "2.2.3", + "constraint": "^1.2.0", + "compatible": [], + "singleBreaking": [ + { + "name": "foo", + "version": "2.2.3", + "kind": "direct", + "source": { + "type": "hosted", + "description": { + "name": "foo", + "url": "http://localhost:$PORT", + "sha256": "a7e0832c069301a6e6ba78d792a08156fbf5bbfc04e766393ad7c98e7a27f648" + } + }, + "constraintBumped": "^2.2.3", + "constraintWidened": ">=1.2.0 <3.0.0", + "constraintBumpedIfNeeded": "^2.2.3", + "previousVersion": "1.2.3", + "previousConstraint": "^1.2.0", + "previousSource": { + "type": "hosted", + "description": { + "name": "foo", + "url": "http://localhost:$PORT", + "sha256": "b2b7fc405959806aa1f31ac7e68752534f66f66a11a280d9878ecb6cd835f01c" + } + } + }, + { + "name": "transitive", + "version": "1.0.0", + "kind": "transitive", + "source": { + "type": "hosted", + "description": { + "name": "transitive", + "url": "http://localhost:$PORT", + "sha256": "d705923b5a6b4c7053e6d86a35799ede6467245151ff15dfdd5719986a61d49c" + } + }, + "constraintBumped": null, + "constraintWidened": null, + "constraintBumpedIfNeeded": null, + "previousVersion": null, + "previousConstraint": null, + "previousSource": null + } + ], + "multiBreaking": [ + { + "name": "foo", + "version": "2.2.3", + "kind": "direct", + "source": { + "type": "hosted", + "description": { + "name": "foo", + "url": "http://localhost:$PORT", + "sha256": "a7e0832c069301a6e6ba78d792a08156fbf5bbfc04e766393ad7c98e7a27f648" + } + }, + "constraintBumped": "^2.2.3", + "constraintWidened": ">=1.2.0 <3.0.0", + "constraintBumpedIfNeeded": "^2.2.3", + "previousVersion": "1.2.3", + "previousConstraint": "^1.2.0", + "previousSource": { + "type": "hosted", + "description": { + "name": "foo", + "url": "http://localhost:$PORT", + "sha256": "b2b7fc405959806aa1f31ac7e68752534f66f66a11a280d9878ecb6cd835f01c" + } + } + }, + { + "name": "transitive", + "version": "1.0.0", + "kind": "transitive", + "source": { + "type": "hosted", + "description": { + "name": "transitive", + "url": "http://localhost:$PORT", + "sha256": "d705923b5a6b4c7053e6d86a35799ede6467245151ff15dfdd5719986a61d49c" + } + }, + "constraintBumped": null, + "constraintWidened": null, + "constraintBumpedIfNeeded": null, + "previousVersion": null, + "previousConstraint": null, + "previousSource": null + } + ] + } + ] +} + +-------------------------------- END OF OUTPUT --------------------------------- + +## Section apply +$ echo '{"dependencyChanges":[{"name":"foo","version":"2.2.3"},{"name":"transitive","version":"1.0.0"}]}' | dependency_services apply +{"dependencies":[]} + +-------------------------------- END OF OUTPUT --------------------------------- + +$ cat pubspec.yaml +{"name":"myapp","version":"1.2.3","homepage":"https://pub.dev","description":"A package, I guess.","dependencies":{"foo":^2.2.3,"bar":"^1.0.0"},"environment":{"sdk":"^3.5.0"},"workspace":["pkgs/a"]} +$ cat pkgs/a/pubspec.yaml +{"name":"a","version":"1.1.1","homepage":"https://pub.dev","description":"A package, I guess.","dependencies":{"bar":">=1.2.0 <1.5.0"},"dev_dependencies":{"foo":^2.2.3,"dev":"^1.0.0"},"environment":{"sdk":"^3.5.0-0"},"resolution":"workspace"} +$ cat pubspec.lock +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + bar: + dependency: "direct main" + description: + name: bar + sha256: $SHA256 + url: "http://localhost:$PORT" + source: hosted + version: "1.2.3" + dev: + dependency: transitive + description: + name: dev + sha256: $SHA256 + url: "http://localhost:$PORT" + source: hosted + version: "1.0.0" + foo: + dependency: "direct main" + description: + name: foo + sha256: $SHA256 + url: "http://localhost:$PORT" + source: hosted + version: "2.2.3" + transitive: + dependency: transitive + description: + name: transitive + sha256: $SHA256 + url: "http://localhost:$PORT" + source: hosted + version: "1.0.0" +sdks: + dart: ">=3.5.0 <4.0.0"
diff --git a/test/workspace_test.dart b/test/workspace_test.dart index df00a0f..63cd9cd 100644 --- a/test/workspace_test.dart +++ b/test/workspace_test.dart
@@ -7,10 +7,12 @@ import 'package:path/path.dart' as p; import 'package:pub/src/exit_codes.dart'; +import 'package:shelf/shelf.dart' as shelf; import 'package:test/test.dart'; import 'package:yaml/yaml.dart'; import 'descriptor.dart'; +import 'lish/utils.dart'; import 'test_pub.dart'; void main() { @@ -344,11 +346,15 @@ ]), ]), ]).create(); + final absoluteAppPath = p.join(sandbox, appPath); await pubGet( environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}, workingDirectory: p.join(sandbox, appPath, 'pkgs'), - output: contains( - 'Resolving dependencies in `${p.join(sandbox, appPath)}`...', + output: allOf( + contains( + 'Resolving dependencies in `$absoluteAppPath`...', + ), + contains('Got dependencies in `$absoluteAppPath`'), ), ); await pubGet( @@ -1294,6 +1300,84 @@ output: contains('! foo 1.0.1 from path ..${s}foo (overridden)'), ); }); + + test('Can publish from workspace', () async { + final server = await servePackages(); + await credentialsFile(server, 'access-token').create(); + server.expect('GET', '/create', (request) { + return shelf.Response.ok( + jsonEncode({ + 'success': {'message': 'Package test_pkg 1.0.0 uploaded!'}, + }), + ); + }); + await dir('workspace', [ + libPubspec( + 'workspace', + '1.2.3', + extras: { + 'workspace': [appPath], + }, + sdk: '^3.5.0', + ), + validPackage( + pubspecExtras: { + 'environment': {'sdk': '^3.5.0'}, + 'resolution': 'workspace', + }, + ), + ]).create(); + + await runPub( + args: ['publish', '--to-archive=archive.tar.gz'], + workingDirectory: p.join(sandbox, 'workspace', appPath), + environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}, + output: contains(''' +├── CHANGELOG.md (<1 KB) +├── LICENSE (<1 KB) +├── README.md (<1 KB) +├── lib +│ └── test_pkg.dart (<1 KB) +└── pubspec.yaml (<1 KB) +'''), + ); + + final pub = await startPublish( + server, + workingDirectory: p.join(sandbox, 'workspace', appPath), + environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}, + ); + + await confirmPublish(pub); + handleUploadForm(server); + handleUpload(server); + await pub.shouldExit(SUCCESS); + }); + + test( + 'published packages with `resolution: workspace` and `workspace` sections can be consumed out of context.', + () async { + final server = await servePackages(); + server.serve( + 'foo', + '1.0.0', + pubspec: { + 'environment': {'sdk': '^3.5.0'}, + 'resolution': 'workspace', + 'workspace': ['example'], + }, + contents: [ + dir('bin', [file('foo.dart', 'main() => print("FOO");')]), + ], + ); + await appDir(dependencies: {'foo': '^1.0.0'}).create(); + await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}); + await runPub( + args: ['run', 'foo'], + environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}, + output: contains('FOO'), + ); + }); } final s = p.separator;