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;