Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions Sources/swifterpm/FileSystemSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,63 @@ extension AbsolutePath {
}
}

extension URL {
/// Path of `self` expressed relative to `base`, using `..` components as needed.
/// Returns `"."` when `self` and `base` refer to the same path.
/// Used to keep `.build/workspace-state.json` and the swifterpm package-info index
/// relocatable: paths are anchored to `scratchDir` so a cached `.build/` works
/// after the project tree moves to a different absolute location.
func relativePathString(to base: URL) throws -> String {
let relative = try absolutePath.relative(to: base.absolutePath).pathString
return relative.isEmpty ? "." : relative
}

/// Returns `self` encoded relative to `scratchDir` only when it lives inside
/// `packageDir`; otherwise returns the absolute path verbatim.
///
/// This scopes the relative-path encoding to paths that are part of the consuming
/// project (its scratch dir, in-tree binaries, and in-tree fileSystem deps). Paths
/// that point outside the project tree (e.g. an external local fileSystem or
/// `localSourceControl` dep on another disk location) are host-specific by nature
/// and keep their absolute form, matching SwiftPM's `workspace-state.json` so the
/// e2e differential suite still holds for those entries.
func pathRelativeToScratchIfInsidePackage(
scratchDir: URL, packageDir: URL
) throws -> String {
// Relativize when this URL lives under either the project root (packageDir) or
// the scratch dir. Both are project-scoped, so paths inside either are part of
// the cacheable `.build/`-rooted unit. Paths outside both (typically external
// local fileSystem / localSourceControl deps) stay absolute so SwiftPM-compat
// against the e2e differential suite still holds for those entries.
//
// We normalize the macOS `/private/var` vs `/var` symlink layer with a string
// substitution rather than POSIX `realpath`. realpath follows every symlink,
// and on developer machines `<scratch>/swifterpm/artifacts/<id>/<target>` is a
// symlink into the global cache; following it would make a path that's
// logically inside scratch look like it lives outside.
let selfNormalized = Self.normalizedPathString(path)
let packageNormalized = Self.normalizedPathString(packageDir.path)
let scratchNormalized = Self.normalizedPathString(scratchDir.path)

let selfAbs = try AbsolutePath(validating: selfNormalized)
let packageAbs = try AbsolutePath(validating: packageNormalized)
let scratchAbs = try AbsolutePath(validating: scratchNormalized)

let packageRelative = packageAbs == selfAbs ? "" : try selfAbs.relative(to: packageAbs).pathString
let scratchRelative = scratchAbs == selfAbs ? "" : try selfAbs.relative(to: scratchAbs).pathString
let insidePackage = !packageRelative.hasPrefix("..")
let insideScratch = !scratchRelative.hasPrefix("..")
guard insidePackage || insideScratch else {
return path
}
return scratchRelative.isEmpty ? "." : scratchRelative
}

private static func normalizedPathString(_ value: String) -> String {
value.replacingOccurrences(of: "/private/var/", with: "/var/")
}
}

extension FileSystem {
/// Write `data` atomically by writing to a temp sibling and then replacing the destination.
/// Creates parent directories if missing.
Expand Down
68 changes: 52 additions & 16 deletions Sources/swifterpm/PackageInfoCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,13 @@ enum PackageInfoCacheWriter {
_ = try await cachedOrDumpPackageJSON(
packageDir: packagePath, destination: packageInfoPath,
disableSandbox: disableSandbox)
return packageEntry(
pin: pin, packagePath: packagePath, packageInfoPath: packageInfoPath)
return try packageEntry(
pin: pin,
packagePath: packagePath,
packageInfoPath: packageInfoPath,
packageDir: packageDir,
scratchDir: scratchDir
)
}.sorted { $0.identity < $1.identity }

var allPackages = packages
Expand All @@ -101,14 +106,19 @@ enum PackageInfoCacheWriter {
_ = try await cachedOrDumpPackageJSON(
packageDir: packagePath, destination: packageInfoPath,
disableSandbox: disableSandbox)
let scopedPackagePath = try packagePath.pathRelativeToScratchIfInsidePackage(
scratchDir: scratchDir, packageDir: packageDir
)
let entry = PackageInfoEntry(
identity: dependency.identity,
kind: "fileSystem",
location: packagePath.path,
location: scopedPackagePath,
version: nil,
revision: nil,
packagePath: packagePath.path,
packageInfoPath: packageInfoPath.path
packagePath: scopedPackagePath,
packageInfoPath: try packageInfoPath.pathRelativeToScratchIfInsidePackage(
scratchDir: scratchDir, packageDir: packageDir
)
)
return entry
}
Expand All @@ -119,17 +129,22 @@ enum PackageInfoCacheWriter {
}
allPackages.append(contentsOf: localPackages)

let scopedPackageDir = try packageDir.pathRelativeToScratchIfInsidePackage(
scratchDir: scratchDir, packageDir: packageDir
)
let index = PackageInfoIndex(
schemaVersion: 1,
schemaVersion: 2,
generatedAtUnix: UInt64(Date().timeIntervalSince1970),
root: PackageInfoEntry(
identity: "root",
kind: "root",
location: packageDir.path,
location: scopedPackageDir,
version: nil,
revision: resolved.originHash,
packagePath: packageDir.path,
packageInfoPath: rootPath.path
packagePath: scopedPackageDir,
packageInfoPath: try rootPath.pathRelativeToScratchIfInsidePackage(
scratchDir: scratchDir, packageDir: packageDir
)
),
packages: allPackages
)
Expand Down Expand Up @@ -182,17 +197,38 @@ enum PackageInfoCacheWriter {
return cacheDate >= manifestDate
}

private static func packageEntry(pin: ResolvedPin, packagePath: URL, packageInfoPath: URL)
-> PackageInfoEntry
{
PackageInfoEntry(
private static func packageEntry(
pin: ResolvedPin,
packagePath: URL,
packageInfoPath: URL,
packageDir: URL,
scratchDir: URL
) throws -> PackageInfoEntry {
// localSourceControl pins carry a filesystem path in `location`. Relativize only
// when that path lives inside the consuming project; external clones stay
// absolute so SwiftPM-compat against the e2e diff fixtures still holds. Remote
// and registry pins keep their URL or identity verbatim.
let location: String
if pin.kind == "localSourceControl" {
location = try URL(fileURLWithPath: pin.location)
.pathRelativeToScratchIfInsidePackage(
scratchDir: scratchDir, packageDir: packageDir
)
} else {
location = pin.location
}
return PackageInfoEntry(
identity: pin.identity,
kind: pin.kind,
location: pin.location,
location: location,
version: pin.state.version,
revision: pin.state.revision,
packagePath: packagePath.path,
packageInfoPath: packageInfoPath.path
packagePath: try packagePath.pathRelativeToScratchIfInsidePackage(
scratchDir: scratchDir, packageDir: packageDir
),
packageInfoPath: try packageInfoPath.pathRelativeToScratchIfInsidePackage(
scratchDir: scratchDir, packageDir: packageDir
)
)
}

Expand Down
66 changes: 51 additions & 15 deletions Sources/swifterpm/Restore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ enum WorkspaceRestorer {
if let packageDir {
contexts.append(
PackageContext(
packageRef: rootPackageRef(packageDir),
packageRef: try rootPackageRef(packageDir, scratchDir: scratchDir),
packagePath: packageDir,
canonicalizeLocalBinaryPaths: true
))
Expand All @@ -368,9 +368,11 @@ enum WorkspaceRestorer {
) {
contexts.append(
PackageContext(
packageRef: fileSystemPackageRef(
packageRef: try fileSystemPackageRef(
localPackage.dependency,
packagePath: localPackage.packagePath,
packageDir: packageDir,
scratchDir: scratchDir,
name: ManifestParser.packageName(localPackage.manifest)
),
packagePath: localPackage.packagePath,
Expand All @@ -389,6 +391,8 @@ enum WorkspaceRestorer {
packageRef: try await packageRef(
pin,
packagePath: packagePath,
packageDir: packageDir,
scratchDir: scratchDir,
disableSandbox: disableSandbox
),
packagePath: packagePath,
Expand All @@ -399,32 +403,40 @@ enum WorkspaceRestorer {
return contexts
}

private static func rootPackageRef(_ packageDir: URL) -> [String: String] {
private static func rootPackageRef(_ packageDir: URL, scratchDir: URL) throws -> [String: String] {
let canonical = PathCanonicalizer.realpath(packageDir)
return [
"identity": canonical.lastPathComponent.lowercased(),
"kind": "root",
"location": canonical.path,
"location": try canonical.pathRelativeToScratchIfInsidePackage(
scratchDir: scratchDir, packageDir: canonical
),
"name": canonical.lastPathComponent,
]
}

private static func fileSystemPackageRef(
_ dependency: ManifestFileSystemDependency,
packagePath: URL,
packageDir: URL,
scratchDir: URL,
name: String? = nil
)
-> [String: String]
{
) throws -> [String: String] {
[
"identity": dependency.identity,
"kind": "fileSystem",
"location": packagePath.path,
"location": try packagePath.pathRelativeToScratchIfInsidePackage(
scratchDir: scratchDir, packageDir: packageDir
),
"name": name ?? dependency.name,
]
}

private static func packageRef(_ pin: ResolvedPin) throws -> [String: String] {
private static func packageRef(
_ pin: ResolvedPin,
packageDir: URL?,
scratchDir: URL
) throws -> [String: String] {
if PinKind.isRegistry(pin.kind) {
return [
"identity": pin.identity,
Expand All @@ -433,20 +445,34 @@ enum WorkspaceRestorer {
"name": pin.identity,
]
}
// localSourceControl pins carry a filesystem path in `location`. Relativize when
// that path lives inside the consuming project; otherwise leave it absolute so
// SwiftPM-compat against external local-control deps holds.
let location: String
if pin.kind == "localSourceControl", let packageDir {
location = try URL(fileURLWithPath: pin.location)
.pathRelativeToScratchIfInsidePackage(
scratchDir: scratchDir, packageDir: packageDir
)
} else {
location = pin.location
}
return [
"identity": pin.identity,
"kind": pin.kind,
"location": pin.location,
"location": location,
"name": PinKind.checkoutDirectoryName(pin),
]
}

private static func packageRef(
_ pin: ResolvedPin,
packagePath: URL,
packageDir: URL?,
scratchDir: URL,
disableSandbox: Bool
) async throws -> [String: String] {
var ref = try packageRef(pin)
var ref = try packageRef(pin, packageDir: packageDir, scratchDir: scratchDir)
guard PinKind.isSourceControl(pin.kind) else {
return ref
}
Expand Down Expand Up @@ -827,6 +853,8 @@ enum WorkspaceRestorer {
let ref = try await packageRef(
pin,
packagePath: packagePathForPin(scratchDir: scratchDir, pin: pin),
packageDir: packageDir,
scratchDir: scratchDir,
disableSandbox: disableSandbox
)
dependencies.append([
Expand All @@ -839,7 +867,7 @@ enum WorkspaceRestorer {
"subpath": PinKind.checkoutDirectoryName(pin),
])
} else if PinKind.isRegistry(pin.kind) {
let ref = try packageRef(pin)
let ref = try packageRef(pin, packageDir: packageDir, scratchDir: scratchDir)
try dependencies.append([
"basedOn": NSNull(),
"packageRef": ref,
Expand All @@ -860,17 +888,21 @@ enum WorkspaceRestorer {
rootManifest: manifest,
disableSandbox: disableSandbox
) {
let ref = fileSystemPackageRef(
let ref = try fileSystemPackageRef(
localPackage.dependency,
packagePath: localPackage.packagePath,
packageDir: packageDir,
scratchDir: scratchDir,
name: ManifestParser.packageName(localPackage.manifest)
)
dependencies.append([
"basedOn": NSNull(),
"packageRef": ref,
"state": [
"name": "fileSystem",
"path": localPackage.packagePath.path,
"path": try localPackage.packagePath.pathRelativeToScratchIfInsidePackage(
scratchDir: scratchDir, packageDir: packageDir
),
],
"subpath": localPackage.dependency.identity,
])
Expand Down Expand Up @@ -961,6 +993,7 @@ enum WorkspaceRestorer {
try await workspaceArtifact(
target,
context: context,
packageDir: packageDir,
scratchDir: scratchDir
)
}
Expand All @@ -972,6 +1005,7 @@ enum WorkspaceRestorer {
private static func workspaceArtifact(
_ target: ManifestBinaryTarget,
context: PackageContext,
packageDir: URL,
scratchDir: URL
) async throws -> WorkspaceArtifact? {
let identity = context.packageRef["identity"] ?? target.name
Expand Down Expand Up @@ -1027,7 +1061,9 @@ enum WorkspaceRestorer {
value: [
"kind": artifact.kind,
"packageRef": context.packageRef,
"path": artifact.path.path,
"path": try artifact.path.pathRelativeToScratchIfInsidePackage(
scratchDir: scratchDir, packageDir: packageDir
),
"source": source,
"targetName": target.name,
])
Expand Down
Loading
Loading