From 7d9ac68128e7f680bc00203035cc9c38e0b13e8d Mon Sep 17 00:00:00 2001 From: Daniel Grumberg Date: Tue, 15 Apr 2025 11:11:50 +0100 Subject: [PATCH] Add prototype implementation of a binary static library artifact bundle auditing tool The provided implementation is intended to be used alongside the Dockerfile to validate binary compatibility with older supported libc. --- .dockerignore | 5 + .gitignore | 3 + BinaryStaticLibraryAuditTool/Dockerfile | 18 +++ BinaryStaticLibraryAuditTool/Package.resolved | 24 ++++ BinaryStaticLibraryAuditTool/Package.swift | 50 ++++++++ BinaryStaticLibraryAuditTool/README.md | 24 ++++ .../BinaryArtifactAudit/ArtifactBundle.swift | 58 +++++++++ .../ArtifactBundleProvider.swift | 5 + .../ArtifactManifest.swift | 4 + .../ArtifactMetadata+Type.swift | 5 + .../ArtifactMetadata+Variant.swift | 40 +++++++ .../ArtifactMetadata.swift | 5 + .../ClangAdditionalLibrariesDetector.swift | 68 +++++++++++ .../BinaryArtifactAudit/FileInfo.swift | 12 ++ .../BinaryArtifactAudit/FileSystem.swift | 29 +++++ .../LocalArtifactBundleProvider.swift | 23 ++++ .../BinaryArtifactAudit/LocalFileSystem.swift | 18 +++ .../ObjdumpSymbolProvider.swift | 62 ++++++++++ .../BinaryArtifactAudit/Process+async.swift | 91 +++++++++++++++ .../RemoteArtifactBundleProvider.swift | 37 ++++++ .../BinaryArtifactAudit/Resources/main.c | 6 + .../BinaryArtifactAudit/SymbolProvider.swift | 33 ++++++ .../BinaryArtifactAudit/ZipArchiver.swift | 26 +++++ .../BinaryArtifactAuditExec.swift | 110 ++++++++++++++++++ .../LocalFileSystemTests.swift | 24 ++++ .../ObjdumpSymbolProviderTests.swift | 53 +++++++++ .../SerializationTests.swift | 89 ++++++++++++++ .../ComplexBundle.artifactbundle/Makefile | 16 +++ .../ComplexBundle.artifactbundle/complex.a | Bin 0 -> 864 bytes .../ComplexBundle.artifactbundle/complex.c | 5 + .../include/complex.h | 1 + .../ComplexBundle.artifactbundle/info.json | 16 +++ .../SimpleBundle.artifactbundle/Makefile | 16 +++ .../include/simple.h | 1 + .../SimpleBundle.artifactbundle/info.json | 16 +++ .../SimpleBundle.artifactbundle/simple.a | Bin 0 -> 640 bytes .../SimpleBundle.artifactbundle/simple.c | 1 + .../VirtualFileSystem.swift | 80 +++++++++++++ BinaryStaticLibraryAuditTool/entrypoint.sh | 12 ++ 39 files changed, 1086 insertions(+) create mode 100644 .dockerignore create mode 100644 BinaryStaticLibraryAuditTool/Dockerfile create mode 100644 BinaryStaticLibraryAuditTool/Package.resolved create mode 100644 BinaryStaticLibraryAuditTool/Package.swift create mode 100644 BinaryStaticLibraryAuditTool/README.md create mode 100644 BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactBundle.swift create mode 100644 BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactBundleProvider.swift create mode 100644 BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactManifest.swift create mode 100644 BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactMetadata+Type.swift create mode 100644 BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactMetadata+Variant.swift create mode 100644 BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactMetadata.swift create mode 100644 BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ClangAdditionalLibrariesDetector.swift create mode 100644 BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/FileInfo.swift create mode 100644 BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/FileSystem.swift create mode 100644 BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/LocalArtifactBundleProvider.swift create mode 100644 BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/LocalFileSystem.swift create mode 100644 BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ObjdumpSymbolProvider.swift create mode 100644 BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/Process+async.swift create mode 100644 BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/RemoteArtifactBundleProvider.swift create mode 100644 BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/Resources/main.c create mode 100644 BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/SymbolProvider.swift create mode 100644 BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ZipArchiver.swift create mode 100644 BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAuditExec/BinaryArtifactAuditExec.swift create mode 100644 BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/LocalFileSystemTests.swift create mode 100644 BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/ObjdumpSymbolProviderTests.swift create mode 100644 BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/SerializationTests.swift create mode 100644 BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/ComplexBundle.artifactbundle/Makefile create mode 100644 BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/ComplexBundle.artifactbundle/complex.a create mode 100644 BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/ComplexBundle.artifactbundle/complex.c create mode 100644 BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/ComplexBundle.artifactbundle/include/complex.h create mode 100644 BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/ComplexBundle.artifactbundle/info.json create mode 100644 BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/SimpleBundle.artifactbundle/Makefile create mode 100644 BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/SimpleBundle.artifactbundle/include/simple.h create mode 100644 BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/SimpleBundle.artifactbundle/info.json create mode 100644 BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/SimpleBundle.artifactbundle/simple.a create mode 100644 BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/SimpleBundle.artifactbundle/simple.c create mode 100644 BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/VirtualFileSystem.swift create mode 100755 BinaryStaticLibraryAuditTool/entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..c3a3f427eed --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.build/ +.git/ +Dockerfile +.dockerignore +README.md diff --git a/.gitignore b/.gitignore index 74c233f8e20..3430c72a718 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ Package.resolved .vscode Utilities/InstalledSwiftPMConfiguration/config.json .devcontainer + +# Ignore temp stuff from building artifacts +.o diff --git a/BinaryStaticLibraryAuditTool/Dockerfile b/BinaryStaticLibraryAuditTool/Dockerfile new file mode 100644 index 00000000000..bcf149545d7 --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Dockerfile @@ -0,0 +1,18 @@ +FROM swift:6.0-amazonlinux2 + +RUN yum update -y && \ + yum install -y llvm-devel + +WORKDIR /build + +COPY ./Package.* ./ +RUN swift package resolve + +COPY . ./ +RUN swift build -c release --static-swift-stdlib + +RUN chmod a+rx entrypoint.sh + +ENTRYPOINT ["/build/entrypoint.sh", "/usr/bin/llvm-objdump"] + + diff --git a/BinaryStaticLibraryAuditTool/Package.resolved b/BinaryStaticLibraryAuditTool/Package.resolved new file mode 100644 index 00000000000..2859375db10 --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Package.resolved @@ -0,0 +1,24 @@ +{ + "originHash" : "fc7f45f6dd3d234173d788063ed950bea2c5f0a94d5d8a051b7ee6132c9110be", + "pins" : [ + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "41982a3656a71c768319979febd796c6fd111d5c", + "version" : "1.5.0" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "a34201439c74b53f0fd71ef11741af7e7caf01e1", + "version" : "1.4.2" + } + } + ], + "version" : 3 +} diff --git a/BinaryStaticLibraryAuditTool/Package.swift b/BinaryStaticLibraryAuditTool/Package.swift new file mode 100644 index 00000000000..38d52be53f7 --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Package.swift @@ -0,0 +1,50 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let swiftSettings: [SwiftSetting] = [ + .enableUpcomingFeature("InternalImportsByDefault"), // SE-0409: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0409-access-level-on-imports.md +] + +let package = Package( + name: "BinaryArtifactAudit", + platforms: [ + .macOS(.v13), + ], + products: [ + .executable(name: "binary-artifact-audit", targets: ["BinaryArtifactAuditExec"]), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), + .package(url: "https://github.com/apple/swift-system.git", from: "1.4.2"), + ], + targets: [ + .executableTarget( + name: "BinaryArtifactAuditExec", + dependencies: [ + .target(name: "BinaryArtifactAudit"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "SystemPackage", package: "swift-system"), + ], + swiftSettings: swiftSettings + ), + .target( + name: "BinaryArtifactAudit", + dependencies: [ + .product(name: "SystemPackage", package: "swift-system"), + ], + resources: [ + .copy("Resources") + ], + swiftSettings: swiftSettings + ), + .testTarget( + name: "BinaryArtifactAuditTests", + dependencies: [.target(name: "BinaryArtifactAudit")], + resources: [ + .copy("TestBundles") + ], + swiftSettings: swiftSettings + ), + ] +) diff --git a/BinaryStaticLibraryAuditTool/README.md b/BinaryStaticLibraryAuditTool/README.md new file mode 100644 index 00000000000..ebb04deea0a --- /dev/null +++ b/BinaryStaticLibraryAuditTool/README.md @@ -0,0 +1,24 @@ +# Binary Artifact Auditing Tool + +This tool allows checking that a binary static library artifact will be compatible with any supported Linux Swift deployment platform. + +It is intended to be used in conjunction with the provided `Dockerfile` as follows: +``` +$ docker build -t my-tag . +$ docker run --rm my-tag +``` + +## Operation + +The tool uses the installed `llvm-objdump` to inspect the static library as well as the local libc and any C runtime libraries and/or object files to +determine if any symbols aren't defined. + +## Known Supported Platforms + +| Docker Image | libc.so version | +| --------------------- | --------------- | +| 6.0-fedora39 | GNU libc 2.38 | +| 6.0-rhel-ubi9-slim | GNU libc 2.34 | +| 6.0-amazonlinux2-slim | GNU libc 2.26 | +| 6.0-bookworm | GNU libc 2.36 | +| 6.0-focal-slim | GNU libc 2.31 | diff --git a/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactBundle.swift b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactBundle.swift new file mode 100644 index 00000000000..8d37320e6de --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactBundle.swift @@ -0,0 +1,58 @@ +package import SystemPackage +private import Foundation + +package struct ArtifactBundle { + package let root: FilePath + package let manifest: ArtifactManifest + + private init(root: FilePath, manifest: ArtifactManifest) { + self.root = root + self.manifest = manifest + } + + package static func create(reading root: FilePath, in fileSystem: some FileSystem = LocalFileSystem()) throws -> ArtifactBundle { + let manifestPath = root.appending("info.json") + guard fileSystem.isRegularFile(manifestPath) else { + throw Err.noManifest + } + + let manifest = try JSONDecoder().decode(ArtifactManifest.self, from: .init(contentsOf: .init(filePath: manifestPath.string))) + + let bundle = ArtifactBundle(root: root, manifest: manifest) + + for (_, artifact) in bundle.manifest.artifacts { + try bundle.validateMetadata(artifact, fileSystem: fileSystem) + } + + return bundle + } + + private func validateMetadata(_ artifact: ArtifactMetadata, fileSystem: some FileSystem) throws { + for variant in artifact.variants { + guard fileSystem.isRegularFile(root.appending(variant.path.components)) else { + throw Err.noSuchVariantPath(variant.path.string) + } + + for header in variant.headerPaths { + guard fileSystem.isDirectory(root.appending(header.components)) else { + throw Err.noSuchHeaderPath(header.string) + } + } + + if let modulePath = variant.moduleMapPath { + guard fileSystem.isRegularFile(root.appending(modulePath.components)) else { + throw Err.noSuchModuleMapPath(modulePath.string) + } + } + } + } +} + +extension ArtifactBundle { + package enum Err: Error { + case noManifest + case noSuchVariantPath(String) + case noSuchHeaderPath(String) + case noSuchModuleMapPath(String) + } +} diff --git a/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactBundleProvider.swift b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactBundleProvider.swift new file mode 100644 index 00000000000..f11e22da74f --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactBundleProvider.swift @@ -0,0 +1,5 @@ +package import Foundation + +package protocol ArtifactBundleProvider { + func artifact(for: URL) async throws -> ArtifactBundle +} diff --git a/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactManifest.swift b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactManifest.swift new file mode 100644 index 00000000000..c03d9525ea6 --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactManifest.swift @@ -0,0 +1,4 @@ +package struct ArtifactManifest: Codable { + package var schemaVersion: String + package var artifacts: [String: ArtifactMetadata] +} diff --git a/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactMetadata+Type.swift b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactMetadata+Type.swift new file mode 100644 index 00000000000..0873add3b98 --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactMetadata+Type.swift @@ -0,0 +1,5 @@ +extension ArtifactMetadata { + package enum ArtifactType: String, Equatable, Codable { + case staticLibrary + } +} diff --git a/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactMetadata+Variant.swift b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactMetadata+Variant.swift new file mode 100644 index 00000000000..b650654ad54 --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactMetadata+Variant.swift @@ -0,0 +1,40 @@ +package import SystemPackage + +extension ArtifactMetadata { + package struct Variant: Codable { + package var path: FilePath + package var headerPaths: [FilePath] + package var moduleMapPath: FilePath? = nil + package var supportedTriples: [String] + + enum CodingKeys: String, CodingKey { + case path + case headerPaths + case moduleMapPath + case supportedTriples + } + + package func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(path.string , forKey: .path) + try container.encode(headerPaths.map { $0.string }, forKey: .headerPaths) + try container.encodeIfPresent(moduleMapPath.map { $0.string }, forKey: .moduleMapPath) + try container.encode(supportedTriples, forKey: .supportedTriples) + } + + package init(from decoder: any Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + let pathString = try values.decode(String.self, forKey: .path) + self.path = FilePath(pathString) + + let headerPathStrings = try values.decode([String].self, forKey: .headerPaths) + self.headerPaths = headerPathStrings.map { FilePath($0) } + + let moduleMapPathString = try values.decodeIfPresent(String.self, forKey: .moduleMapPath) + self.moduleMapPath = moduleMapPathString.map { FilePath($0) } + + self.supportedTriples = try values.decode([String].self, forKey: .supportedTriples) + } + } +} diff --git a/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactMetadata.swift b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactMetadata.swift new file mode 100644 index 00000000000..26c456abf60 --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ArtifactMetadata.swift @@ -0,0 +1,5 @@ +package struct ArtifactMetadata: Codable { + package var version: String + package var type: ArtifactType + package var variants: [Variant] +} diff --git a/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ClangAdditionalLibrariesDetector.swift b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ClangAdditionalLibrariesDetector.swift new file mode 100644 index 00000000000..e0f5decedcb --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ClangAdditionalLibrariesDetector.swift @@ -0,0 +1,68 @@ +package import SystemPackage +private import Foundation + +package func detectAdditionalObjects() async throws -> Set { + guard let sampleExecutable = Bundle.module.url(forResource: "main", withExtension: "c") else { + throw AdditionalObjectsDetectorError.missingTestFile + } + + let clangOutput = try await Process.run(executable: "/usr/bin/clang", arguments: "-###", sampleExecutable.path()) + guard let commandStrings = String(data: clangOutput.error, encoding: .utf8)?.split(whereSeparator: \.isNewline) else { + throw AdditionalObjectsDetectorError.failedToParseClangOutput + } + + let commands = commandStrings.map { $0.split(whereSeparator: \.isWhitespace) } + guard let linkerCommand = commands.last(where: { $0.first?.contains("ld") == true }) else { + throw AdditionalObjectsDetectorError.couldNotFindLinkerCommand + } + + var libraryExtensionsToTry: [String] = [] +#if canImport(Darwin) + libraryExtensionsToTry.append(contentsOf: [".a", ".dylib", ".tbd"]) +#elseif os(Windows) + libraryExtensionsToTry.append(contentsOf: [".lib", ".dll"]) +#else + libraryExtensionsToTry.append(contentsOf: [".a", ".so"]) +#endif + + var objects: Set = [] + var searchPaths: [FilePath] = [] + let fileSystem = LocalFileSystem() + + let linkerArguments = linkerCommand.dropFirst().map { $0.replacingOccurrences(of: "\"", with: "") } + + for argument in linkerArguments { + if argument.hasPrefix("-L") { + let path = FilePath(String(argument.dropFirst(2))) + searchPaths.append(path.lexicallyNormalized()) + } else if argument.hasPrefix("-l") { + let libName = String(argument.dropFirst(2)) + searchPathLoop: for path in searchPaths { + for ext in libraryExtensionsToTry { + let potentialLibrary = path.appending("lib\(libName)\(ext)") + if fileSystem.isRegularFile(potentialLibrary) { + objects.insert(potentialLibrary.lexicallyNormalized()) + break searchPathLoop + } + } + } + + assertionFailure("Could not find lib for \(libName)") + } else if argument.hasSuffix(".o") && fileSystem.isRegularFile(FilePath(String(argument))) { + objects.insert(FilePath(String(argument)).lexicallyNormalized()) + } else if let dotIndex = argument.firstIndex(of: "."), + ["so", "dylib"].first(where: { argument[dotIndex...].contains($0) }) != nil { + objects.insert(FilePath(String(argument)).lexicallyNormalized()) + } + } + + objects.remove("/usr/lib/gcc/aarch64-redhat-linux/7/libgcc_s.so") + return objects +} + +enum AdditionalObjectsDetectorError: Error { + case missingTestFile + case failedToParseClangOutput + case couldNotFindLinkerCommand +} + diff --git a/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/FileInfo.swift b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/FileInfo.swift new file mode 100644 index 00000000000..06d98df669e --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/FileInfo.swift @@ -0,0 +1,12 @@ +package import Foundation + +package struct FileInfo { + package let type: FileAttributeType + package init(type: FileAttributeType) { + self.type = type + } + + package var isRegularFile: Bool { self.type == .typeRegular } + + package var isDirectory: Bool { self.type == .typeDirectory } +} diff --git a/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/FileSystem.swift b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/FileSystem.swift new file mode 100644 index 00000000000..466e75db5ce --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/FileSystem.swift @@ -0,0 +1,29 @@ +package import SystemPackage + +package protocol FileSystem { + func fileInfo(_ path: FilePath) throws -> FileInfo + + func isRegularFile(_ path: FilePath) -> Bool + + func isDirectory(_ path: FilePath) -> Bool + + func createTemporaryDirectory() throws -> FilePath +} + +extension FileSystem { + package func isRegularFile(_ path: FilePath) -> Bool { + do { + return try fileInfo(path).isRegularFile + } catch { + return false + } + } + + package func isDirectory(_ path: FilePath) -> Bool { + do { + return try fileInfo(path).isDirectory + } catch { + return false + } + } +} diff --git a/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/LocalArtifactBundleProvider.swift b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/LocalArtifactBundleProvider.swift new file mode 100644 index 00000000000..906f90504ed --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/LocalArtifactBundleProvider.swift @@ -0,0 +1,23 @@ +package import Foundation +private import SystemPackage + +package struct LocalArtifactBundleProvider: ArtifactBundleProvider { + package init () { } + + package func artifact(for artifactURL: URL) async throws -> ArtifactBundle { + let path = FilePath(artifactURL.path()) + let fileSystem = LocalFileSystem() + + guard fileSystem.isDirectory(path) else { + throw Err.noArtifactBundle(path.string) + } + + return try ArtifactBundle.create(reading: path, in: fileSystem) + } +} + +extension LocalArtifactBundleProvider { + package enum Err: Error { + case noArtifactBundle(String) + } +} diff --git a/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/LocalFileSystem.swift b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/LocalFileSystem.swift new file mode 100644 index 00000000000..c0da0cdb40a --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/LocalFileSystem.swift @@ -0,0 +1,18 @@ +private import Foundation +package import SystemPackage + +package struct LocalFileSystem: FileSystem { + package init() { } + + package func fileInfo(_ path: FilePath) throws -> FileInfo { + let attributes = try FileManager.default.attributesOfItem(atPath: path.string) + let type = attributes[.type] as? FileAttributeType ?? .typeUnknown + return FileInfo(type: type) + } + + package func createTemporaryDirectory() throws -> FilePath { + let temporaryDirectoryPath = FilePath(FileManager.default.temporaryDirectory.path).appending(UUID().uuidString) + try FileManager.default.createDirectory(atPath: temporaryDirectoryPath.string, withIntermediateDirectories: true) + return temporaryDirectoryPath + } +} diff --git a/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ObjdumpSymbolProvider.swift b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ObjdumpSymbolProvider.swift new file mode 100644 index 00000000000..5af2f3d3e28 --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ObjdumpSymbolProvider.swift @@ -0,0 +1,62 @@ +private import Foundation +package import SystemPackage + +package struct ObjdumpSymbolProvider: SymbolProvider { + private let objdumpPath: FilePath + + package init(objdumpPath: FilePath) { + self.objdumpPath = objdumpPath + } + + package func symbols(for binary: FilePath, symbols: inout ReferencedSymbols, recordUndefined: Bool = true) async throws { + let result = try await Process.run(executable: objdumpPath, arguments: "-t", "-T", binary.string) + guard let output = String(data: result.output, encoding: .utf8) else { + throw Err.unexpectedOutput + } + + try parse(output: output, symbols: &symbols, recordUndefined: recordUndefined) + } + + package func parse(output: String, symbols: inout ReferencedSymbols, recordUndefined: Bool = true) throws { + for line in output.split(whereSeparator: \.isNewline) { + // Ensure the line starts with an address in the binary everything else isn't a symbol + guard line.count > 16 && line.prefix(16).allSatisfy(\.isHexDigit), + let name = name(line: line) else { + continue + } + + switch try section(line: line) { + case "*UND*": + if recordUndefined { + symbols.addUndefined(String(name)) + } + default: + symbols.addDefined(String(name)) + } + } + } + + private func name(line: Substring) -> Substring? { + guard let lastspace = line.lastIndex(where: \.isWhitespace) else { return nil } + return line[line.index(after: lastspace)...] + } + + private func section(line: Substring) throws -> Substring { + guard line.count > 25 else { + throw Err.unexpectedLine(String(line)) + } + let sectionStart = line.index(line.startIndex, offsetBy: 25) + guard let sectionEnd = line[sectionStart...].firstIndex(where: \.isWhitespace) else { + throw Err.unexpectedLine(String(line)) + } + return line[sectionStart.. CollectedResult { + let process = Process() + process.executableURL = URL(fileURLWithPath: executable.string) + process.arguments = Array(arguments) + let stdout = process.collectData(for: \.standardOutputPipe) + let stderr = process.collectData(for: \.standardErrorPipe) + + try process.run() + + await withTaskCancellationHandler { + process.waitUntilExit() + } onCancel: { + process.terminate() + } + + switch process.terminationReason { + case .exit: + if process.terminationStatus != 0 { + throw Err.failed(status: Int(process.terminationStatus)) + } + case .uncaughtSignal: + throw Err.signal(code: Int(process.terminationStatus)) + @unknown default: + preconditionFailure() + } + + @Sendable func processOutputDataStream(_ stream: AsyncStream) async -> Data { + var data = Data() + for await nextDataElem in stream { + data.append(nextDataElem) + } + + return data + } + + async let maybeOutData = processOutputDataStream(stdout) + async let maybeErrData = processOutputDataStream(stderr) + + return CollectedResult( + output: await maybeOutData, + error: await maybeErrData + ) + } +} + +extension Process { + enum Err: Error { + case failed(status: Int) + case signal(code: Int) + } + + /// Type-safe accessor for `standardOutput`. + fileprivate var standardOutputPipe: Pipe? { + get { standardOutput as? Pipe } + set { standardOutput = newValue } + } + + /// Type-safe accessor for `standardError`. + fileprivate var standardErrorPipe: Pipe? { + get { standardError as? Pipe } + set { standardError = newValue } + } + + /// Attach a pipe to one of the Process objects output file descriptor and return an async sequence of data items as they are read from the pipe. + fileprivate func collectData(for keyPath: ReferenceWritableKeyPath) -> AsyncStream { + let pipe = Pipe() + self[keyPath: keyPath] = pipe + return AsyncStream { continuation in + pipe.fileHandleForReading.readabilityHandler = { file in + let data = file.availableData + guard !data.isEmpty else { + pipe.fileHandleForReading.readabilityHandler = nil + continuation.finish() + return + } + + continuation.yield(data) + } + } + } + + package struct CollectedResult { + package var output: Data + package var error: Data + } +} diff --git a/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/RemoteArtifactBundleProvider.swift b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/RemoteArtifactBundleProvider.swift new file mode 100644 index 00000000000..389b3b9dd14 --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/RemoteArtifactBundleProvider.swift @@ -0,0 +1,37 @@ +package import Foundation +#if canImport(FoundationNetworking) +private import FoundationNetworking +#endif + +private import SystemPackage + +package struct RemoteArtifactBundleProvider: ArtifactBundleProvider { + package init() { } + + package func artifact(for artifactURL: URL) async throws -> ArtifactBundle { + guard let filename = FilePath(artifactURL.path).lastComponent?.stem, + !filename.contains(".") else { + throw Err.invalidArtifactURL(artifactURL.absoluteString) + } + + let fileSystem = LocalFileSystem() + let temporaryDirectoryPath = try fileSystem.createTemporaryDirectory() + + let (downloadedURL, _ ) = try await URLSession.shared.download(from: artifactURL) + try await ZipArchiver.extract(from: FilePath(downloadedURL.path), to: temporaryDirectoryPath) + + let destination = temporaryDirectoryPath.appending(filename) + guard fileSystem.isDirectory(destination) else { + throw Err.failedExtraction(destination.string) + } + + return try ArtifactBundle.create(reading: destination) + } +} + +extension RemoteArtifactBundleProvider { + package enum Err: Error { + case invalidArtifactURL(String) + case failedExtraction(String) + } +} diff --git a/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/Resources/main.c b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/Resources/main.c new file mode 100644 index 00000000000..81be0ed1654 --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/Resources/main.c @@ -0,0 +1,6 @@ +#include + +int main(int argc, char *argv[]) { + printf("Hello, world!\n"); + return 0; +} diff --git a/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/SymbolProvider.swift b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/SymbolProvider.swift new file mode 100644 index 00000000000..443ee7ce7da --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/SymbolProvider.swift @@ -0,0 +1,33 @@ +package import SystemPackage + +package protocol SymbolProvider { + func symbols(for: FilePath, symbols: inout ReferencedSymbols, recordUndefined: Bool) async throws +} + +extension SymbolProvider { + package func symbols(for binary: FilePath, symbols: inout ReferencedSymbols) async throws { + try await self.symbols(for: binary, symbols: &symbols, recordUndefined: true) + } +} + +package struct ReferencedSymbols { + package var defined: Set + package var undefined: Set + + package init() { + self.defined = [] + self.undefined = [] + } + + mutating func addUndefined(_ name: String) { + guard !self.defined.contains(name) else { + return + } + self.undefined.insert(name) + } + + mutating func addDefined(_ name: String) { + self.defined.insert(name) + self.undefined.remove(name) + } +} diff --git a/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ZipArchiver.swift b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ZipArchiver.swift new file mode 100644 index 00000000000..c7f6bb203a7 --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAudit/ZipArchiver.swift @@ -0,0 +1,26 @@ +private import Foundation +internal import SystemPackage + +struct ZipArchiver { + static func extract( + from archivePath: FilePath, + to destinationPath: FilePath + ) async throws { + let fileSystem = LocalFileSystem() + guard fileSystem.isRegularFile(archivePath) else { + throw Err.noEntry(archivePath) + } + + guard fileSystem.isDirectory(destinationPath) else { + throw Err.noEntry(destinationPath) + } + + try await Process.run(executable: FilePath("/usr/bin/unzip"), arguments: archivePath.string, "-d", destinationPath.string) + } +} + +extension ZipArchiver{ + enum Err: Error { + case noEntry(FilePath) + } +} diff --git a/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAuditExec/BinaryArtifactAuditExec.swift b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAuditExec/BinaryArtifactAuditExec.swift new file mode 100644 index 00000000000..20c209311e0 --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Sources/BinaryArtifactAuditExec/BinaryArtifactAuditExec.swift @@ -0,0 +1,110 @@ +internal import Foundation +internal import ArgumentParser +internal import SystemPackage +private import BinaryArtifactAudit + +@main +struct BinaryArtifactAudit: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "binary-artifact-audit", + abstract: "A utility for validating library binary artifacts.", + subcommands: [ValidateRemote.self, ValidateLocal.self] + ) +} + +extension BinaryArtifactAudit { + struct ValidateRemote: AsyncParsableCommand { + @Argument( + help: "URL to the remote artifact bundle", + transform: BinaryArtifactAudit.parseURL(argument:) + ) + var remoteArtifactBundle: URL + + @OptionGroup(title: "Configuration") + var commonArgs: CommonArgs + + func run() async throws { + try await BinaryArtifactAudit.check( + bundle: remoteArtifactBundle, + bundleProvider: RemoteArtifactBundleProvider(), + provider: ObjdumpSymbolProvider(objdumpPath: commonArgs.objdump) + ) + } + } + + struct ValidateLocal: AsyncParsableCommand { + @Argument( + help: "Path to the local artifact bundle", + transform: parseURL(argument:) + ) + var localArtifactBundle: URL + + @OptionGroup(title: "Configuration") + var commonArgs: CommonArgs + + func run() async throws { + try await BinaryArtifactAudit.check( + bundle: localArtifactBundle, + bundleProvider: LocalArtifactBundleProvider(), + provider: ObjdumpSymbolProvider(objdumpPath: commonArgs.objdump) + ) + } + } + + static private func getTargetTriple() async throws -> String { + let output = try await Process.run(executable: "/usr/bin/clang", arguments: "-v") + let targetLine = String(data: output.error, encoding: .utf8)!.split(whereSeparator: \.isNewline).first { $0.hasPrefix("Target: ") }! + return String(targetLine.dropFirst("Target: ".count)) + } + + static private func check(bundle url: URL, bundleProvider: some ArtifactBundleProvider, provider: some SymbolProvider) async throws { + let localTriple = try await getTargetTriple() + let bundle = try await bundleProvider.artifact(for: url) + var platformDefaultSymbols = ReferencedSymbols() + + for binary in try await detectAdditionalObjects() { + try await provider.symbols(for: binary, symbols: &platformDefaultSymbols, recordUndefined: false) + } + + for variant in bundle.manifest.artifacts.values.flatMap({ $0.variants }) { + guard variant.supportedTriples.contains(localTriple) else { + continue + } + + let binary = variant.path + var symbols = platformDefaultSymbols + try await provider.symbols(for: bundle.root.appending(binary.components), symbols: &symbols) + + guard symbols.undefined.isEmpty else { + print("Invalid artifact binary \(binary.string), found undefined symbols:") + for name in symbols.undefined { + print("- \(name)") + } + + throw ExitCode(1) + } + } + + print("Artifact is safe to use across supported Swift deployment targets!") + } +} + +struct CommonArgs: ParsableArguments { + @Option( + name: .long, + help: "The path to the llvm-objdump command to use.", + transform: FilePath.init(_:) + ) + var objdump: FilePath +} + +extension BinaryArtifactAudit { + enum ParsingError: Error { + case invalidURLError + } + + private static func parseURL(argument: String) throws -> URL { + guard let url = URL(string: argument) else { throw ParsingError.invalidURLError } + return url + } +} diff --git a/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/LocalFileSystemTests.swift b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/LocalFileSystemTests.swift new file mode 100644 index 00000000000..20597ea608a --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/LocalFileSystemTests.swift @@ -0,0 +1,24 @@ +internal import Testing +private import BinaryArtifactAudit +private import Foundation + +@Suite +struct LocalFileSystemTests { + @Test + func createTemporaryDirectory() throws { + let fileSystem = LocalFileSystem() + let temporary = try fileSystem.createTemporaryDirectory() + defer { try? FileManager.default.removeItem(atPath: temporary.string) } + + var isDirectory: ObjCBool = false + let exists = FileManager.default.fileExists(atPath: temporary.string, isDirectory: &isDirectory) + #expect(exists && isDirectory.boolValue) + + } + + @Test + func regularFileExists() throws { + let fileSystem = LocalFileSystem() + #expect(fileSystem.isRegularFile(#filePath)) + } +} diff --git a/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/ObjdumpSymbolProviderTests.swift b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/ObjdumpSymbolProviderTests.swift new file mode 100644 index 00000000000..ba9444dc94e --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/ObjdumpSymbolProviderTests.swift @@ -0,0 +1,53 @@ +internal import Testing +private import SystemPackage +private import BinaryArtifactAudit + +@Suite +struct ObjdumpSymbolProviderTests { + private func getSymbols(_ dump: String) throws -> ReferencedSymbols { + var symbols = ReferencedSymbols() + try ObjdumpSymbolProvider(objdumpPath: FilePath()).parse(output: dump, symbols: &symbols) + return symbols + } + + @Test + func ignoresHeaderLines() throws { + let output = try getSymbols( + """ + + /usr/lib/aarch64-linux-gnu/libc.so.6: file format elf64-littleaarch64 + + SYMBOL TABLE: + + DYNAMIC SYMBOL TABLE: + """ + ) + + #expect(output.defined.isEmpty) + #expect(output.undefined.isEmpty) + } + + @Test + func detectsDefinedSymbol() throws { + let output = try getSymbols("00000000000e0618 g DF .text 0000000000000018 GLIBC_2.17 __ppoll_chk") + + #expect(output.defined.contains("__ppoll_chk")) + #expect(output.undefined.isEmpty) + } + + @Test + func detectsUndefinedSymbol() throws { + let output = try getSymbols("0000000000000000 DF *UND* 0000000000000000 (GLIBC_2.17) __tls_get_addr") + + #expect(output.defined.isEmpty) + #expect(output.undefined.contains("__tls_get_addr")) + } + + @Test + func treatsCommonSymbolsAsDefined() throws { + let output = try getSymbols("0000000000000004 O *COM* 0000000000000004 __libc_enable_secure_decided") + + #expect(output.defined.contains("__libc_enable_secure_decided")) + #expect(output.undefined.isEmpty) + } +} diff --git a/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/SerializationTests.swift b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/SerializationTests.swift new file mode 100644 index 00000000000..c02d9914dfc --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/SerializationTests.swift @@ -0,0 +1,89 @@ +internal import Testing + +private import BinaryArtifactAudit + +private import Foundation +private import SystemPackage + +@Suite("Artifact manifest serialization") +struct ManifestSerializationTests { + @Test("Decode") + func decodeSampleManifest() throws { + let sampleManifest = """ + { + "schemaVersion": "1.0", + "artifacts": { + "first": { + "version": "0.1.0", + "type": "staticLibrary", + "variants": [ + { + "path": "first-0.1.0-linux/lib/first.a", + "headerPaths": ["include/"], + "moduleMapPath": "include/first.modulemap", + "supportedTriples": ["x86_64-unknown-linux-gnu"] + }, + { + "path": "first-0.1.0-macos/lib/first.a", + "headerPaths": ["include/"], + "moduleMapPath": "include/first.modulemap", + "supportedTriples": ["x86_64-apple-macosx", "arm64-apple-macosx"] + }, + { + "path": "first-0.1.0-windows/lib/first.lib", + "headerPaths": ["include/"], + "moduleMapPath": "include/first.modulemap", + "supportedTriples": ["x86_64-unknown-windows"] + } + ] + }, + "second": { + "version": "1.0.0", + "type": "staticLibrary", + "variants": [ + { + "path": "second-1.0.0-linux/lib/second.a", + "headerPaths": ["include/"], + "supportedTriples": ["x86_64-unknown-linux-gnu"] + } + ] + } + } + } + """ + let encodedManifest = try #require(sampleManifest.data(using: .utf8)) + + let manifest = try JSONDecoder().decode(ArtifactManifest.self, from: encodedManifest) + #expect(manifest.schemaVersion == "1.0") + #expect(Array(manifest.artifacts.keys).sorted() == ["first", "second"]) + + let firstArtifact = try #require(manifest.artifacts["first"]) + #expect(firstArtifact.version == "0.1.0") + #expect(firstArtifact.type == .staticLibrary) + #expect(firstArtifact.variants.count == 3) + #expect(firstArtifact.variants[0].path == FilePath("first-0.1.0-linux/lib/first.a")) + #expect(firstArtifact.variants[0].headerPaths == [FilePath("include/")]) + #expect(firstArtifact.variants[0].moduleMapPath == FilePath("include/first.modulemap")) + #expect(firstArtifact.variants[0].supportedTriples == ["x86_64-unknown-linux-gnu"]) + + #expect(firstArtifact.variants[1].path == FilePath("first-0.1.0-macos/lib/first.a")) + #expect(firstArtifact.variants[1].headerPaths == [FilePath("include/")]) + #expect(firstArtifact.variants[1].moduleMapPath == FilePath("include/first.modulemap")) + #expect(firstArtifact.variants[1].supportedTriples.sorted() == ["arm64-apple-macosx", "x86_64-apple-macosx"]) + + #expect(firstArtifact.variants[2].path == FilePath("first-0.1.0-windows/lib/first.lib")) + #expect(firstArtifact.variants[2].headerPaths == [FilePath("include/")]) + #expect(firstArtifact.variants[2].moduleMapPath == FilePath("include/first.modulemap")) + #expect(firstArtifact.variants[2].supportedTriples == ["x86_64-unknown-windows"]) + + let secondArtifact = try #require(manifest.artifacts["second"]) + #expect(secondArtifact.version == "1.0.0") + #expect(secondArtifact.type == .staticLibrary) + #expect(secondArtifact.variants.count == 1) + #expect(secondArtifact.variants[0].path == FilePath("second-1.0.0-linux/lib/second.a")) + #expect(secondArtifact.variants[0].headerPaths == [FilePath("include/")]) + #expect(secondArtifact.variants[0].moduleMapPath == nil) + #expect(secondArtifact.variants[0].supportedTriples == ["x86_64-unknown-linux-gnu"]) + } + +} diff --git a/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/ComplexBundle.artifactbundle/Makefile b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/ComplexBundle.artifactbundle/Makefile new file mode 100644 index 00000000000..e0a88da62a4 --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/ComplexBundle.artifactbundle/Makefile @@ -0,0 +1,16 @@ +SRC_FILES := $(wildcard *.c) +OBJ_FILES := $(SRC_FILES:.c=.o) +ARCHIVE := complex.a + +.PHONY: all clean + +%.o: %.c + clang -c -o $@ $< + +$(ARCHIVE): $(OBJ_FILES) + llvm-ar rc $@ $^ + +all: $(ARCHIVE) + +clean: + $(RM) $(ARCHIVE) $(OBJ_FILES) diff --git a/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/ComplexBundle.artifactbundle/complex.a b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/ComplexBundle.artifactbundle/complex.a new file mode 100644 index 0000000000000000000000000000000000000000..94df6d52bf30451d4cad93b5614d3887b0ba250f GIT binary patch literal 864 zcmb7C%}N4M6uzbXV4*HrRJ6F}rcE|Z<4;gQQP4smXb4+f#l{qLlyOACO^;B}L)5Ni zyXpzrMK95=Rbcd;>m3@679BX}JKy=6d+(XpcF8GMcSCWJ%bn)M$yr{Qd;21u%owRu zR!7 + +void printTruth(void) { + printf("The answer to life is 42!"); +} diff --git a/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/ComplexBundle.artifactbundle/include/complex.h b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/ComplexBundle.artifactbundle/include/complex.h new file mode 100644 index 00000000000..bfadc102106 --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/ComplexBundle.artifactbundle/include/complex.h @@ -0,0 +1 @@ +void printTruth(void); diff --git a/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/ComplexBundle.artifactbundle/info.json b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/ComplexBundle.artifactbundle/info.json new file mode 100644 index 00000000000..cea7d8191ca --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/ComplexBundle.artifactbundle/info.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": "1.0", + "artifacts": { + "complex": { + "type": "staticLibrary", + "version": "1.0.0", + "variants": [ + { + "path": "complex.a", + "headerPaths": ["include"], + "supportedTriples": ["arm64-apple-macosx"] + } + ] + } + } +} diff --git a/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/SimpleBundle.artifactbundle/Makefile b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/SimpleBundle.artifactbundle/Makefile new file mode 100644 index 00000000000..6798f0f6c94 --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/SimpleBundle.artifactbundle/Makefile @@ -0,0 +1,16 @@ +SRC_FILES := $(wildcard *.c) +OBJ_FILES := $(SRC_FILES:.c=.o) +ARCHIVE := simple.a + +.PHONY: all clean + +%.o: %.c + clang -c -o $@ $< + +$(ARCHIVE): $(OBJ_FILES) + llvm-ar rc $@ $^ + +all: $(ARCHIVE) + +clean: + $(RM) $(ARCHIVE) $(OBJ_FILES) diff --git a/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/SimpleBundle.artifactbundle/include/simple.h b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/SimpleBundle.artifactbundle/include/simple.h new file mode 100644 index 00000000000..5a2ca62ad29 --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/SimpleBundle.artifactbundle/include/simple.h @@ -0,0 +1 @@ +int foo(void); diff --git a/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/SimpleBundle.artifactbundle/info.json b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/SimpleBundle.artifactbundle/info.json new file mode 100644 index 00000000000..435386c40da --- /dev/null +++ b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/SimpleBundle.artifactbundle/info.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": "1.0", + "artifacts": { + "simple": { + "type": "staticLibrary", + "version": "1.0.0", + "variants": [ + { + "path": "simple.a", + "headerPaths": ["include"], + "supportedTriples": ["arm64-apple-macosx"] + } + ] + } + } +} diff --git a/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/SimpleBundle.artifactbundle/simple.a b/BinaryStaticLibraryAuditTool/Tests/BinaryArtifactAuditTests/TestBundles/SimpleBundle.artifactbundle/simple.a new file mode 100644 index 0000000000000000000000000000000000000000..a789faa14b6850b85f6acd212c81dd6841008ce1 GIT binary patch literal 640 zcmY$iNi0gvu;WsW_wfnyjSr6WjSq2jidQx?H!(FeFg3BXR8TNAFjP=bFfsrV3T7rI zAW8wu0kaagiZgQya#Hp3^}&1v1zdU!4Gh5AO%08pDigTQ|9bn6hk=2S0SG_@3lL|3 z7z#iP1d>4f0?firXh4NKz$P#-!1(MyaS&#L%E!l-q*jzbIY25tKEyR51i^=~IG_xW zc`&_TAr^)Pm>LirAD^6`TacJs5?`8Eo|%^tAMfLWEDtkB0V+5FXdW|=y#Yu=or+?- z5l~J7h#_+PK+Ay`2tbNJ;ya-FUjS(kpBsn+fEWZoYCssI7i0%Hz=5?P=m2y4HJDzg x`%&BolLOkr28=TXK9Ge_zy_rGfU05s21)TF$uq>K<>xcxl;jo|f=L6YJ^ FileInfo { + let node = try find(path) + + switch node.kind { + case .directory(_): + return FileInfo(type: .typeDirectory) + case .regularFile: + return FileInfo(type: .typeRegular) + } + } + + func createTemporaryDirectory() throws -> FilePath { + let tmpDirPath = FilePath("/tmp") + let tmpDir = try find(tmpDirPath) + guard case .directory(var children) = tmpDir.kind else { + throw Err.unexpectedRegularFile("/tmp") + } + + let name = UUID().uuidString + children.append(.init(name: name, kind: .directory(children: []))) + + tmpDir.kind = .directory(children: children) + + return tmpDirPath.appending(name) + } + + private func find(_ path: FilePath) throws -> Node { + var currentPath = FilePath("/") + var currentNode = root + + for component in currentWorkingDirectory.pushing(path).components { + guard case .directory(let children) = currentNode.kind else { + throw Err.unexpectedRegularFile(currentPath.string) + } + + currentPath.append(component) + + guard let child = children.first(where: { $0.name == component.string }) else { + throw Err.noSuchEntry(currentPath.string) + } + + currentNode = child + } + + return currentNode + } +} + +extension VirtualFileSystem { + final class Node { + enum Kind { + case regularFile + case directory(children: [Node]) + } + + let name: String + var kind: Kind + + fileprivate init(name: String, kind: Kind) { + self.name = name + self.kind = kind + } + } +} + +extension VirtualFileSystem { + enum Err: Error { + case noSuchEntry(String) + case unexpectedRegularFile(String) + } +} diff --git a/BinaryStaticLibraryAuditTool/entrypoint.sh b/BinaryStaticLibraryAuditTool/entrypoint.sh new file mode 100755 index 00000000000..0262be02633 --- /dev/null +++ b/BinaryStaticLibraryAuditTool/entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +executable="$(swift build --package-path /build -c release --show-bin-path)/binary-artifact-audit" +echo "$*" +objdump="$1" +shift +subcommand="$1" +shift + +echo exec "$executable" "$subcommand" --objdump "$objdump" $@ +exec "$executable" "$subcommand" --objdump "$objdump" $@ +