Skip to content

Commit 30ee074

Browse files
authored
gracefuly handle pre-existing binary artifact from failed downloads (#4021)
motivation: when binary artifact download fails it may leave behind a partial or otherwise corrupt file changes: * remove pre-existing archive file before attempting to download the same file again * add test
1 parent d41c85b commit 30ee074

File tree

2 files changed

+130
-0
lines changed

2 files changed

+130
-0
lines changed

Sources/Workspace/Workspace.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2281,6 +2281,11 @@ extension Workspace {
22812281
}
22822282

22832283
let archivePath = parentDirectory.appending(component: artifact.url.lastPathComponent)
2284+
if self.fileSystem.exists(archivePath) {
2285+
guard observabilityScope.trap ({ try self.fileSystem.removeFileTree(archivePath) }) else {
2286+
continue
2287+
}
2288+
}
22842289

22852290
semaphore.wait()
22862291
group.enter()

Tests/WorkspaceTests/WorkspaceTests.swift

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6502,6 +6502,131 @@ final class WorkspaceTests: XCTestCase {
65026502
}
65036503
}
65046504

6505+
func testArtifactDownloadArchiveExists() throws {
6506+
let sandbox = AbsolutePath("/tmp/ws/")
6507+
let fs = InMemoryFileSystem()
6508+
// this relies on internal knowledge of the destination path construction
6509+
let expectedDownloadDestination = sandbox.appending(components: ".build", "artifacts", "library", "binary.zip")
6510+
6511+
// returns a dummy zipfile for the requested artifact
6512+
let httpClient = HTTPClient(handler: { request, _, completion in
6513+
do {
6514+
guard case .download(let fileSystem, let destination) = request.kind else {
6515+
throw StringError("invalid request \(request.kind)")
6516+
}
6517+
6518+
// this is to test the test's integrity, as it relied on internal knowledge of the destination path construction
6519+
guard expectedDownloadDestination == destination else {
6520+
throw StringError("expected destination of \(expectedDownloadDestination)")
6521+
}
6522+
6523+
let contents: [UInt8]
6524+
switch request.url.lastPathComponent {
6525+
case "binary.zip":
6526+
contents = [0x01]
6527+
default:
6528+
throw StringError("unexpected url \(request.url)")
6529+
}
6530+
6531+
// in-memory fs does not check for this!
6532+
if fileSystem.exists(destination) {
6533+
throw StringError("\(destination) already exists")
6534+
}
6535+
6536+
try fileSystem.writeFileContents(
6537+
destination,
6538+
bytes: ByteString(contents),
6539+
atomically: true
6540+
)
6541+
6542+
completion(.success(.okay()))
6543+
} catch {
6544+
completion(.failure(error))
6545+
}
6546+
})
6547+
6548+
// create a dummy xcframework directory from the request archive
6549+
let archiver = MockArchiver(handler: { archiver, archivePath, destinationPath, completion in
6550+
do {
6551+
let name: String
6552+
switch archivePath.basename {
6553+
case "binary.zip":
6554+
name = "binary.xcframework"
6555+
default:
6556+
throw StringError("unexpected archivePath \(archivePath)")
6557+
}
6558+
try fs.createDirectory(destinationPath.appending(component: name), recursive: false)
6559+
archiver.extractions.append(MockArchiver.Extraction(archivePath: archivePath, destinationPath: destinationPath))
6560+
completion(.success(()))
6561+
} catch {
6562+
completion(.failure(error))
6563+
}
6564+
})
6565+
6566+
let workspace = try MockWorkspace(
6567+
sandbox: sandbox,
6568+
fileSystem: fs,
6569+
roots: [
6570+
MockPackage(
6571+
name: "App",
6572+
targets: [
6573+
MockTarget(name: "App", dependencies: [
6574+
.product(name: "binary", package: "library"),
6575+
]),
6576+
],
6577+
products: [],
6578+
dependencies: [
6579+
.sourceControl(path: "./library", requirement: .exact("1.0.0")),
6580+
]
6581+
),
6582+
],
6583+
packages: [
6584+
MockPackage(
6585+
name: "library",
6586+
targets: [
6587+
MockTarget(
6588+
name: "binary",
6589+
type: .binary,
6590+
url: "https://a.com/binary.zip",
6591+
checksum: "01"
6592+
)
6593+
],
6594+
products: [
6595+
MockProduct(name: "binary", targets: ["binary"]),
6596+
],
6597+
versions: ["1.0.0"]
6598+
)
6599+
],
6600+
customHttpClient: httpClient,
6601+
customBinaryArchiver: archiver
6602+
)
6603+
6604+
// write the file to test it gets deleted
6605+
6606+
try fs.createDirectory(expectedDownloadDestination.parentDirectory, recursive: true)
6607+
try fs.writeFileContents(
6608+
expectedDownloadDestination,
6609+
bytes: [],
6610+
atomically: true
6611+
)
6612+
6613+
try workspace.checkPackageGraph(roots: ["App"]) { graph, diagnostics in
6614+
XCTAssertNoDiagnostics(diagnostics)
6615+
}
6616+
6617+
workspace.checkManagedArtifacts { result in
6618+
result.check(
6619+
packageIdentity: .plain("library"),
6620+
targetName: "binary",
6621+
source: .remote(
6622+
url: "https://a.com/binary.zip",
6623+
checksum: "01"
6624+
),
6625+
path: workspace.artifactsDir.appending(components: "library", "binary.xcframework")
6626+
)
6627+
}
6628+
}
6629+
65056630
func testDownloadArchiveIndexFilesHappyPath() throws {
65066631
let sandbox = AbsolutePath("/tmp/ws/")
65076632
let fs = InMemoryFileSystem()

0 commit comments

Comments
 (0)