Skip to content

Commit 71cc845

Browse files
committed
SwiftBuild: add scratch path symbolic links
The native build system would create the `debug` and `release` symbolic link in the scratch path as they were the original output location before triple support was added. Although this is not an officially support feature, add the `debug` and `release` symbolic links when using Swift Build to ease the transition until a better solution is avaiable. Relates to: #9963 Issue: rdar://175144467
1 parent 6dd99ed commit 71cc845

10 files changed

Lines changed: 270 additions & 48 deletions

File tree

Sources/CoreCommands/BuildSystemSupport.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ private struct SwiftBuildSystemFactory: BuildSystemFactory {
141141
workDirectory: try self.swiftCommandState.getActiveWorkspace().location.pluginWorkingDirectory,
142142
disableSandbox: self.swiftCommandState.shouldDisableSandbox
143143
),
144-
delegate: delegate
144+
delegate: delegate,
145+
scratchDirectory: self.swiftCommandState.scratchDirectory,
145146
)
146147
}
147148
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import struct Basics.AbsolutePath
14+
import protocol Basics.FileSystem
15+
import var Basics.localFileSystem
16+
import class Basics.ObservabilityScope
17+
18+
package func createBuildSymbolicLinks(
19+
_ path: Basics.AbsolutePath,
20+
pointingAt: Basics.AbsolutePath,
21+
fileSystem: FileSystem = localFileSystem,
22+
observabilityScope: ObservabilityScope,
23+
) {
24+
if fileSystem.exists(path) {
25+
do {
26+
// This does not delete the directory pointed to by the symbolic link
27+
try fileSystem.removeFileTree(path)
28+
}
29+
catch {
30+
observabilityScope.emit(
31+
warning: "unable to delete \(path), skip creating symbolic link",
32+
underlyingError: error
33+
)
34+
}
35+
}
36+
37+
do {
38+
try fileSystem.createSymbolicLink(
39+
path,
40+
pointingAt: pointingAt,
41+
relative: true,
42+
)
43+
} catch {
44+
observabilityScope.emit(
45+
warning: "unable to create symbolic link at \(path)",
46+
underlyingError: error
47+
)
48+
}
49+
}

Sources/SwiftBuildSupport/SwiftBuildSystem.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ package final class SwiftBuildSystemPlanningOperationDelegate: SWBPlanningOperat
242242
}
243243

244244
public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
245+
internal let scratchDirectory: Basics.AbsolutePath
245246
package let buildParameters: BuildParameters
246247
package let hostBuildParameters: BuildParameters
247248
private let packageGraphLoader: () async throws -> ModulesGraph
@@ -317,7 +318,8 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
317318
fileSystem: FileSystem,
318319
observabilityScope: ObservabilityScope,
319320
pluginConfiguration: PluginConfiguration,
320-
delegate: BuildSystemDelegate?
321+
delegate: BuildSystemDelegate?,
322+
scratchDirectory: Basics.AbsolutePath, // currently used to create the symbolic links
321323
) throws {
322324
self.buildParameters = buildParameters
323325
self.hostBuildParameters = hostBuildParameters
@@ -330,6 +332,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
330332
self.observabilityScope = observabilityScope.makeChildScope(description: "Swift Build System")
331333
self.pluginConfiguration = pluginConfiguration
332334
self.delegate = delegate
335+
self.scratchDirectory = scratchDirectory
333336
}
334337

335338
private func createREPLArguments(
@@ -416,6 +419,15 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
416419
return result
417420
}
418421

422+
defer {
423+
createBuildSymbolicLinks(
424+
self.scratchDirectory.appending(component: self.buildParameters.configuration.dirname),
425+
pointingAt: self.buildParameters.buildPath,
426+
fileSystem: self.fileSystem,
427+
observabilityScope: self.observabilityScope,
428+
)
429+
}
430+
419431
try await writePIF(buildParameters: self.buildParameters)
420432

421433
return try await startSWBuildOperation(

Sources/_InternalTestSupport/SwiftTesting+Helpers.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,32 @@ package func expectDirectoryContainsFile(
193193
Issue.record("Directory \(dir) does not contain \(filename)", sourceLocation: sourceLocation)
194194
}
195195

196+
public func expectSymlink(
197+
_ path: AbsolutePath,
198+
pointsTo target: AbsolutePath,
199+
fileSystem fs: FileSystem = localFileSystem,
200+
sourceLocation: SourceLocation = #_sourceLocation,
201+
) throws {
202+
#expect(
203+
fs.isSymlink(path),
204+
"Source (\(path)) is not a symbolic link",
205+
sourceLocation: sourceLocation,
206+
)
207+
208+
#expect(
209+
fs.exists(path, followSymlink: true),
210+
"Source (\(path)) does not exists while following symliks",
211+
sourceLocation: sourceLocation,
212+
)
213+
214+
let resolvedSymlinkLocation = try resolveSymlinks(path)
215+
let resolvedtarget = try resolveSymlinks(target)
216+
#expect(
217+
resolvedSymlinkLocation == resolvedtarget,
218+
"Resolved symlink location does not match the resolved target",
219+
sourceLocation: sourceLocation,
220+
)
221+
}
196222

197223
/// Expects that the expression throws a CommandExecutionError and passes it to the provided throwing error handler.
198224
/// - Parameters:

Sources/_InternalTestSupport/misc.swift

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -610,39 +610,6 @@ private func swiftArgs(
610610
return args
611611
}
612612

613-
@available(*,
614-
deprecated,
615-
renamed: "loadModulesGraph",
616-
message: "Rename for consistency: the type of this functions return value is named `ModulesGraph`."
617-
)
618-
public func loadPackageGraph(
619-
identityResolver: IdentityResolver = DefaultIdentityResolver(),
620-
fileSystem: FileSystem,
621-
manifests: [Manifest],
622-
binaryArtifacts: [PackageIdentity: [String: BinaryArtifact]] = [:],
623-
explicitProduct: String? = .none,
624-
shouldCreateMultipleTestProducts: Bool = false,
625-
createREPLProduct: Bool = false,
626-
useXCBuildFileRules: Bool = false,
627-
customXCTestMinimumDeploymentTargets: [PackageModel.Platform: PlatformVersion]? = .none,
628-
observabilityScope: ObservabilityScope,
629-
traitConfiguration: TraitConfiguration = .default
630-
) throws -> ModulesGraph {
631-
try loadModulesGraph(
632-
identityResolver: identityResolver,
633-
fileSystem: fileSystem,
634-
manifests: manifests,
635-
binaryArtifacts: binaryArtifacts,
636-
explicitProduct: explicitProduct,
637-
shouldCreateMultipleTestProducts: shouldCreateMultipleTestProducts,
638-
createREPLProduct: createREPLProduct,
639-
useXCBuildFileRules: useXCBuildFileRules,
640-
customXCTestMinimumDeploymentTargets: customXCTestMinimumDeploymentTargets,
641-
observabilityScope: observabilityScope,
642-
traitConfiguration: traitConfiguration
643-
)
644-
}
645-
646613
public let emptyZipFile = ByteString([0x80, 0x75, 0x05, 0x06] + [UInt8](repeating: 0x00, count: 18))
647614

648615
extension FileSystem {

Sources/swift-bootstrap/main.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,7 @@ struct SwiftBootstrapBuildTool: AsyncParsableCommand {
437437
disableSandbox: false
438438
),
439439
delegate: nil,
440+
scratchDirectory: scratchDirectory,
440441
)
441442
}
442443
}

Tests/BuildTests/CrossCompilationBuildPlanTests.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import class PackageModel.Manifest
2222
import struct PackageModel.TargetDescription
2323
import enum PackageModel.ProductType
2424
import struct SPMBuildCore.BuildParameters
25-
import func _InternalTestSupport.loadPackageGraph
2625

2726
import func _InternalTestSupport.embeddedCxxInteropPackageGraph
2827
import func _InternalTestSupport.macrosPackageGraph

Tests/CommandsTests/BuildCommandTests.swift

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -155,18 +155,42 @@ struct BuildCommandTestCases {
155155
let configuration = buildData.config
156156
// Test is not implemented for Xcode build system
157157
try await fixture(name: "ValidLayouts/SingleModule/ExecutableNew") { fixturePath in
158-
let fullPath = try resolveSymlinks(fixturePath)
158+
try await withTemporaryDirectory { tempDir in
159+
let scratchPath = tempDir.appending("build")
160+
let fullPath = try resolveSymlinks(fixturePath)
161+
let originalSymlink = scratchPath.appending("\(configuration)")
162+
163+
let targetPath = try scratchPath.appending(components: buildSystem.binPath(for: configuration, scratchPath: []))
164+
let commonBuildArgs = [
165+
"--scratch-path",
166+
scratchPath.pathString,
167+
]
168+
let path = try await execute(
169+
[
170+
"--show-bin-path",
171+
] + commonBuildArgs,
172+
packagePath: fullPath,
173+
configuration: configuration,
174+
buildSystem: buildSystem,
175+
).stdout.trimmingCharacters(in: .whitespacesAndNewlines)
159176

160-
let targetPath = try fullPath.appending(components: buildSystem.binPath(for: configuration))
161-
let path = try await execute(
162-
["--show-bin-path"],
163-
packagePath: fullPath,
164-
configuration: configuration,
165-
buildSystem: buildSystem,
166-
).stdout.trimmingCharacters(in: .whitespacesAndNewlines)
167-
#expect(
168-
AbsolutePath(path).pathString == targetPath.pathString
169-
)
177+
#expect(
178+
AbsolutePath(path).pathString == targetPath.pathString
179+
)
180+
181+
// The original symlink should not exists
182+
expectFileDoesNotExists(at: originalSymlink)
183+
184+
// Let's build the package
185+
try await executeSwiftBuild(
186+
fullPath,
187+
configuration: configuration,
188+
extraArgs: commonBuildArgs,
189+
buildSystem: buildSystem,
190+
)
191+
192+
try expectSymlink(originalSymlink, pointsTo: AbsolutePath(path))
193+
}
170194
}
171195
}
172196

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
// import class Basics.InMemoryFileSystem
14+
import struct Basics.AbsolutePath
15+
import protocol Basics.FileSystem
16+
import class Basics.ObservabilitySystem
17+
import func Basics.resolveSymlinks
18+
import func Basics.withTemporaryDirectory
19+
import var Basics.localFileSystem
20+
import struct TSCBasic.StringError
21+
import struct TSCBasic.AbsolutePath
22+
import struct TSCBasic.ByteString
23+
import enum TSCBasic.FileMode
24+
25+
import func SwiftBuildSupport.createBuildSymbolicLinks
26+
27+
import _InternalTestSupport
28+
import Testing
29+
30+
typealias AbsolutePath = Basics.AbsolutePath
31+
32+
@Suite(
33+
.tags(
34+
.TestSize.small,
35+
),
36+
)
37+
struct CreateBuildSymbolicLinkFunction {
38+
@Test(
39+
)
40+
func createBuildSymbolicLinkCreation() async throws {
41+
let fs = localFileSystem
42+
try withTemporaryDirectory(removeTreeOnDeinit: false) { tmpDir in
43+
// Arrange
44+
let observability = ObservabilitySystem.makeForTesting()
45+
let source = tmpDir.appending("source")
46+
let target = tmpDir.appending(components: "my","target","directory")
47+
try localFileSystem.createDirectory(target, recursive: true)
48+
49+
// Act
50+
createBuildSymbolicLinks(
51+
source,
52+
pointingAt: target,
53+
fileSystem: fs,
54+
observabilityScope: observability.topScope,
55+
)
56+
57+
// Assert
58+
try expectSymlink(
59+
source,
60+
pointsTo: target,
61+
fileSystem: fs,
62+
)
63+
expectNoDiagnostics(observability.diagnostics)
64+
}
65+
}
66+
67+
@Test
68+
func failingToRemoveSourceSymlinkGeneratedAWarning() async throws {
69+
// Arrange
70+
struct FileSystemDouble: FileSystem {
71+
func createSymbolicLink(_ path: TSCBasic.AbsolutePath, pointingAt destination: TSCBasic.AbsolutePath, relative: Bool) throws {
72+
throw StringError("Purposely failing in \(#function)")
73+
}
74+
75+
func removeFileTree(_ path: TSCBasic.AbsolutePath) throws {
76+
throw StringError("Purposely failing in \(#function)")
77+
}
78+
79+
func removeFileTree(_ path: AbsolutePath) throws {
80+
throw StringError("Purposely failing in \(#function)")
81+
}
82+
func createSymbolicLink( source: AbsolutePath, pointingAt target: AbsolutePath ) throws {
83+
throw StringError("Purposely failing in \(#function)")
84+
}
85+
86+
func move(from sourcePath: TSCBasic.AbsolutePath, to destinationPath: TSCBasic.AbsolutePath) throws {}
87+
func copy(from sourcePath: TSCBasic.AbsolutePath, to destinationPath: TSCBasic.AbsolutePath) throws {}
88+
func chmod(_ mode: TSCBasic.FileMode, path: TSCBasic.AbsolutePath, options: Set<TSCBasic.FileMode.Option>) throws {}
89+
func writeFileContents(_ path: TSCBasic.AbsolutePath, bytes: TSCBasic.ByteString) throws {}
90+
func readFileContents(_ path: TSCBasic.AbsolutePath) throws -> TSCBasic.ByteString {
91+
TSCBasic.ByteString()
92+
}
93+
func createDirectory(_ path: TSCBasic.AbsolutePath, recursive: Bool) throws {}
94+
let tempDirectory: TSCBasic.AbsolutePath
95+
let cachesDirectory: TSCBasic.AbsolutePath?
96+
let homeDirectory: TSCBasic.AbsolutePath
97+
func changeCurrentWorkingDirectory(to path: TSCBasic.AbsolutePath) throws {}
98+
let currentWorkingDirectory: TSCBasic.AbsolutePath?
99+
func getDirectoryContents(_ path: TSCBasic.AbsolutePath) throws -> [String] {
100+
return []
101+
}
102+
func isWritable(_ path: TSCBasic.AbsolutePath) -> Bool { false }
103+
func isReadable(_ path: TSCBasic.AbsolutePath) -> Bool { false }
104+
func isSymlink(_ path: TSCBasic.AbsolutePath) -> Bool { false }
105+
func isExecutableFile(_ path: TSCBasic.AbsolutePath) -> Bool { false }
106+
func isFile(_ path: TSCBasic.AbsolutePath) -> Bool { false }
107+
func isDirectory(_ path: TSCBasic.AbsolutePath) -> Bool { false }
108+
func exists(_ path: TSCBasic.AbsolutePath, followSymlink: Bool) -> Bool { false }
109+
func exists(_ path: AbsolutePath, followSymlink: Bool) -> Bool { false }
110+
}
111+
112+
let fs = FileSystemDouble(
113+
tempDirectory: TSCBasic.AbsolutePath.root.appending(components: "tmp", "\(#function)", "tmp"),
114+
cachesDirectory: nil,
115+
homeDirectory: TSCBasic.AbsolutePath.root.appending(components: "tmp", "\(#function)", "home"),
116+
currentWorkingDirectory: nil,
117+
)
118+
let observability = ObservabilitySystem.makeForTesting()
119+
120+
// Act
121+
createBuildSymbolicLinks(
122+
AbsolutePath("/foo/bar"),
123+
pointingAt: AbsolutePath("/foo/ping/pong"),
124+
fileSystem: fs,
125+
observabilityScope: observability.topScope,
126+
)
127+
128+
testDiagnostics(observability.diagnostics) { result in
129+
result.check(
130+
diagnostic: .contains("unable to delete"),
131+
severity: .warning
132+
)
133+
134+
result.check(
135+
diagnostic: .contains("unable to create symbolic link"),
136+
severity: .warning
137+
)
138+
139+
}
140+
}
141+
142+
}

0 commit comments

Comments
 (0)