diff --git a/examples/runfiles/BUILD b/examples/runfiles/BUILD new file mode 100644 index 000000000..675e669b7 --- /dev/null +++ b/examples/runfiles/BUILD @@ -0,0 +1,13 @@ +load("//swift:swift.bzl", "swift_binary") + +swift_binary( + name = "runfiles_example", + srcs = ["main.swift"], + data = [ + "data/sample.txt", + ], + visibility = ["//visibility:public"], + deps = [ + "//swift/runfiles", + ], +) diff --git a/examples/runfiles/data/sample.txt b/examples/runfiles/data/sample.txt new file mode 100644 index 000000000..e021b42d9 --- /dev/null +++ b/examples/runfiles/data/sample.txt @@ -0,0 +1 @@ +Hello runfiles \ No newline at end of file diff --git a/examples/runfiles/main.swift b/examples/runfiles/main.swift new file mode 100644 index 000000000..8a471899d --- /dev/null +++ b/examples/runfiles/main.swift @@ -0,0 +1,17 @@ +import BazelRunfiles + +do { + let runfiles = try Runfiles.create() + // Runfiles lookup paths have the form `my_workspace/package/file`. + // Runfiles path lookup may throw. + let fileURL = try runfiles.rlocation("build_bazel_rules_swift/examples/runfiles/data/sample.txt") + print("file: \(fileURL)") + + // Runfiles path lookup may return a non-existent path. + let content = try String(contentsOf: fileURL, encoding: .utf8) + + assert(content == "Hello runfiles") + print(content) +} catch { + print("runfiles error: \(error)") +} diff --git a/swift/internal/BUILD b/swift/internal/BUILD index 21bffb5b4..4806e901c 100644 --- a/swift/internal/BUILD +++ b/swift/internal/BUILD @@ -214,6 +214,12 @@ bzl_library( ], ) +bzl_library( + name = "runfiles", + srcs = ["runfiles.bzl"], + visibility = ["//swift:__subpackages__"], +) + bzl_library( name = "swift_autoconfiguration", srcs = ["swift_autoconfiguration.bzl"], diff --git a/swift/runfiles/BUILD b/swift/runfiles/BUILD new file mode 100644 index 000000000..130d19200 --- /dev/null +++ b/swift/runfiles/BUILD @@ -0,0 +1,10 @@ +load("//swift:swift_library.bzl", "swift_library") + +swift_library( + name = "runfiles", + srcs = [ + "Runfiles.swift", + ], + module_name = "BazelRunfiles", + visibility = ["//visibility:public"], +) diff --git a/swift/runfiles/README.md b/swift/runfiles/README.md new file mode 100644 index 000000000..0b2870e90 --- /dev/null +++ b/swift/runfiles/README.md @@ -0,0 +1,78 @@ +# Swift BazelRunfiles library + +This is a Bazel Runfiles lookup library for Bazel-built Swift binaries and tests. + +Learn about runfiles: read [Runfiles guide](https://bazel.build/extending/rules#runfiles) +or watch [Fabian's BazelCon talk](https://www.youtube.com/watch?v=5NbgUMH1OGo). + +## Usage + +1. Depend on this runfiles library from your build rule: + +```python +swift_binary( + name = "my_binary", + ... + data = ["//path/to/my/data.txt"], + deps = ["@build_bazel_rules_swift//swift/runfiles"], +) +``` + +2. Include the runfiles library: + +```swift +import BazelRunfiles +``` + +3. Create a Runfiles instance and use `rlocation` to look up runfile urls: + +```swift +import BazelRunfiles + +do { + let runfiles = try Runfiles.create() + let fileURL = try runfiles.rlocation("my_workspace/path/to/my/data.txt") + print("file: \(fileURL)") +} catch { + print("runfiles error: \(error)") +} +``` + +The code above: + +- Creates a manifest- or directory-based implementation based on + the environment variables in `Process.processInfo.environment`. + See `Runfiles.create()` for more info. +- The `Runfiles.create` function uses the runfiles manifest and the runfiles + directory from the `RUNFILES_MANIFEST_FILE` and `RUNFILES_DIR` environment + variables. If not present, the function looks for the manifest and directory + near `CommandLine.arguments.first` (e.g. `argv[0]` the path of the main program). + +If you want to start subprocesses, and the subprocess can't automatically +find the correct runfiles directory, you can explicitly set the right +environment variables for them: + +```swift +import BazelRunfiles +import Foundation + +do { + + let runfiles = try Runfiles.create() + let executableURL = try runfiles.rlocation("my_workspace/path/to/binary") + + let process = Process() + process.executableURL = executableURL + process.environment = runfiles.envVars() + + do { + // Launch the process + try process.run() + process.waitUntilExit() + } catch { + // ... + } +} catch { + fatalError("runfiles error: \(error)") +} +``` diff --git a/swift/runfiles/Runfiles.swift b/swift/runfiles/Runfiles.swift new file mode 100644 index 000000000..be844964a --- /dev/null +++ b/swift/runfiles/Runfiles.swift @@ -0,0 +1,311 @@ +// Copyright 2024 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +enum RunfilesEnv { + static let runfilesManifestFile: String = "RUNFILES_MANIFEST_FILE" + static let runfilesDir: String = "RUNFILES_DIR" +} + +protocol LookupStrategy { + func rlocationChecked(path: String) throws -> URL + func envVars() -> [String: String] +} + +struct DirectoryBased: LookupStrategy { + + private let runfilesRoot: URL + init(path: URL) { + runfilesRoot = path + } + + func rlocationChecked(path: String) throws -> URL { + runfilesRoot.appendingPathComponent(path) + } + + func envVars() -> [String: String] { + [ + RunfilesEnv.runfilesDir: runfilesRoot.path, + ] + } +} + +struct ManifestBased: LookupStrategy { + + private let manifestPath: URL + private let runfiles: [String: String] + + init(manifestPath: URL) throws { + self.manifestPath = manifestPath + runfiles = try Self.loadRunfiles(from: manifestPath) + } + + func rlocationChecked(path: String) throws -> URL { + if let runfile = runfiles[path] { + return URL(fileURLWithPath: runfile) + } + + // Search for prefixes in the path + for end in path.indices.reversed() where path[end] == "/" { + let prefix = String(path[.. [String: String] { + [ + RunfilesEnv.runfilesManifestFile: manifestPath.path, + ] + } + + static func loadRunfiles(from manifestPath: URL) throws -> [String: String] { + guard let fileHandle = try? FileHandle(forReadingFrom: manifestPath) else { + throw RunfilesError.missingManifest + } + defer { + try? fileHandle.close() + } + + var pathMapping = [String: String]() + if let data = try? fileHandle.readToEnd(), let content = String(data: data, encoding: .utf8) { + let lines = content.split(separator: "\n") + for line in lines { + let fields = line.split(separator: " ", maxSplits: 1) + if fields.count == 1 { + pathMapping[String(fields[0])] = String(fields[0]) + } else { + pathMapping[String(fields[0])] = String(fields[1]) + } + } + } + + return pathMapping + } +} + +struct RepoMappingKey: Hashable { + let sourceRepoCanonicalName: String + let targetRepoApparentName: String +} + +public enum RunfilesError: Error { + case invalidRunfilePath + case invalidRunfilesLocations + case missingRunfilesLocations + case missingRunfileEntryFromManifest + case invalidRepoMappingEntry(line: String) + case missingManifest +} + +public final class Runfiles { + + private let strategy: LookupStrategy + // Value is the runfiles directory of target repository + private let repoMapping: [RepoMappingKey: String] + private let sourceRepository: String + + init(strategy: LookupStrategy, repoMapping: [RepoMappingKey: String], sourceRepository: String) { + self.strategy = strategy + self.repoMapping = repoMapping + self.sourceRepository = sourceRepository + } + + public func rlocation(_ path: String, sourceRepository: String? = nil) throws -> URL { + guard + !path.hasPrefix("../"), + !path.contains("/.."), + !path.hasPrefix("./"), + !path.contains("/./"), + !path.hasSuffix("/."), + !path.contains("//") + else { + throw RunfilesError.invalidRunfilePath + } + guard path.first != "\\" else { + throw RunfilesError.invalidRunfilePath + } + guard path.first != "/" else { + return URL(fileURLWithPath: path) + } + + let sourceRepository = sourceRepository ?? self.sourceRepository + + // Split off the first path component, which contains the repository + // name (apparent or canonical). + let components = path.split(separator: "/", maxSplits: 1) + let targetRepository = String(components[0]) + let key = RepoMappingKey(sourceRepoCanonicalName: sourceRepository, targetRepoApparentName: targetRepository) + + if components.count == 1 || repoMapping[key] == nil { + // One of the following is the case: + // - not using Bzlmod, so the repository mapping is empty and + // apparent and canonical repository names are the same + // - target_repo is already a canonical repository name and does not + // have to be mapped. + // - path did not contain a slash and referred to a root symlink, + // which also should not be mapped. + return try strategy.rlocationChecked(path: path) + } + + let remainingPath = String(components[1]) + + // target_repo is an apparent repository name. Look up the corresponding + // canonical repository name with respect to the current repository, + // identified by its canonical name. + if let targetCanonical = repoMapping[key] { + return try strategy.rlocationChecked(path: targetCanonical + "/" + remainingPath) + } else { + return try strategy.rlocationChecked(path: path) + } + } + + public func envVars() -> [String: String] { + strategy.envVars() + } + + // MARK: Factory method + + public static func create(sourceRepository: String? = nil, environment: [String: String]? = nil, _ callerFilePath: String = #filePath) throws -> Runfiles { + + let environment = environment ?? ProcessInfo.processInfo.environment + + let runfilesPath = try computeRunfilesPath( + argv0: CommandLine.arguments[0], + manifestFile: environment[RunfilesEnv.runfilesManifestFile], + runfilesDir: environment[RunfilesEnv.runfilesDir], + isRunfilesManifest: { file in FileManager.default.fileExists(atPath: file) }, + isRunfilesDirectory: { file in + var isDir: ObjCBool = false + return FileManager.default.fileExists(atPath: file, isDirectory: &isDir) && isDir.boolValue + } + ) + + let strategy: LookupStrategy = switch (runfilesPath) { + case .manifest(let path): + try ManifestBased(manifestPath: URL(fileURLWithPath: path)) + case .directory(let path): + DirectoryBased(path: URL(fileURLWithPath: path)) + } + + // If the repository mapping file can't be found, that is not an error: We + // might be running without Bzlmod enabled or there may not be any runfiles. + // In this case, just apply an empty repo mapping. + let repoMapping: [RepoMappingKey : String] = if let path = try? strategy.rlocationChecked(path: "_repo_mapping") { + try parseRepoMapping(path: path) + } else { + [:] + } + + return Runfiles(strategy: strategy, repoMapping: repoMapping, sourceRepository: sourceRepository ?? repository(from: callerFilePath)) + } + +} + + // https://github.com/bazel-contrib/rules_go/blob/6505cf2e4f0a768497b123a74363f47b711e1d02/go/runfiles/global.go#L53-L54 + private let legacyExternalGeneratedFile = /bazel-out\/[^\/]+\/bin\/external\/([^\/]+)/ + private let legacyExternalFile = /external\/([^\/]+)/ + + // Extracts the canonical name of the repository containing the file + // located at `path`. + private func repository(from path: String) -> String { + if let match = path.prefixMatch(of: legacyExternalGeneratedFile) { + return String(match.1) + } + if let match = path.prefixMatch(of: legacyExternalFile) { + return String(match.1) + } + // If a file is not in an external repository, return an empty string + return "" + } + +// MARK: Runfiles Paths Computation + +enum RunfilesPath: Equatable { + case manifest(String) + case directory(String) +} + +func computeRunfilesPath( + argv0: String, + manifestFile: String?, + runfilesDir: String?, + isRunfilesManifest: (String) -> Bool, + isRunfilesDirectory: (String) -> Bool +) throws -> RunfilesPath { + // if a manifest or a runfiles dir was provided, try to use whichever + // was valid or else error. + if (manifestFile != nil || runfilesDir != nil) { + if let manifestFile, isRunfilesManifest(manifestFile) { + return RunfilesPath.manifest(manifestFile) + } else if let runfilesDir, isRunfilesDirectory(runfilesDir) { + return RunfilesPath.directory(runfilesDir) + } else { + throw RunfilesError.invalidRunfilesLocations + } + } + + // If a manifest exists in one of the well known location, use it. + for wellKnownManifestFileSuffixes in [".runfiles/MANIFEST", ".runfiles_manifest"] { + let manifestFileCandidate = "\(argv0)\(wellKnownManifestFileSuffixes)" + if isRunfilesManifest(manifestFileCandidate) { + return RunfilesPath.manifest(manifestFileCandidate) + } + } + + // If a runfiles dir exists in the well known location, use it. + let runfilesDirCandidate = "\(argv0).runfiles" + if isRunfilesDirectory(runfilesDirCandidate) { + return RunfilesPath.directory(runfilesDirCandidate) + } + + throw RunfilesError.missingRunfilesLocations +} + +// MARK: Parsing Repo Mapping + +func parseRepoMapping(path: URL) throws -> [RepoMappingKey: String] { + guard let fileHandle = try? FileHandle(forReadingFrom: path) else { + // If the repository mapping file can't be found, that is not an error: We + // might be running without Bzlmod enabled or there may not be any runfiles. + // In this case, just apply an empty repo mapping. + return [:] + } + defer { + try? fileHandle.close() + } + + var repoMapping = [RepoMappingKey: String]() + if let data = try fileHandle.readToEnd(), let content = String(data: data, encoding: .utf8) { + let lines = content.split(separator: "\n") + for line in lines { + let fields = line.components(separatedBy: ",") + if fields.count != 3 { + throw RunfilesError.invalidRepoMappingEntry(line: String(line)) + } + let key = RepoMappingKey( + sourceRepoCanonicalName: fields[0], + targetRepoApparentName: fields[1] + ) + repoMapping[key] = fields[2] // mapping + } + } + + return repoMapping +} diff --git a/test/runfiles/BUILD b/test/runfiles/BUILD new file mode 100644 index 000000000..be733625d --- /dev/null +++ b/test/runfiles/BUILD @@ -0,0 +1,9 @@ +load("//swift:swift_test.bzl", "swift_test") + +swift_test( + name = "RunfilesTests", + srcs = ["RunfilesTests.swift"], + deps = [ + "@build_bazel_rules_swift//swift/runfiles", + ], +) diff --git a/test/runfiles/RunfilesTests.swift b/test/runfiles/RunfilesTests.swift new file mode 100644 index 000000000..a394727c6 --- /dev/null +++ b/test/runfiles/RunfilesTests.swift @@ -0,0 +1,547 @@ +// Copyright 2024 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@testable import BazelRunfiles +import Foundation +import XCTest + +// Mainly adapted from https://github.com/bazelbuild/rules_python/blob/main/tests/runfiles/runfiles_test.py +final class RunfilesTests: XCTestCase { + + func testRlocationArgumentValidation() throws { + + let (fileURL, clean) = try createMockFile(name: "MANIFEST", contents: "a/b /c/d"); defer { try? clean() } + + let runfiles = try Runfiles.create( + environment: [ + "RUNFILES_MANIFEST_FILE": fileURL.path, + "RUNFILES_DIR": "ignored when RUNFILES_MANIFEST_FILE has a value", + "TEST_SRCDIR": "always ignored", + ] + ) + XCTAssertEqual(try runfiles.rlocation("a/b").path, "/c/d") + XCTAssertNil(try? runfiles.rlocation("foo")) + } + + func testManifestBasedRunfilesEnvVarsFromArbitraryManifest() throws { + + let (manifest, clean) = try createMockFile(name: "x_manifest", contents: "a/b /c/d"); defer { try? clean() } + + let runfiles = try Runfiles.create( + environment: [ + "RUNFILES_MANIFEST_FILE": manifest.path, + "TEST_SRCDIR": "always ignored", + ] + ) + + XCTAssertEqual(runfiles.envVars(), [ + "RUNFILES_MANIFEST_FILE": manifest.path, + ]) + + } + + func testCreatesDirectoryBasedRunfiles() throws { + + let (runfilesDir, clean) = try createMockDirectory(name: "my_custom_runfiles"); defer { try? clean() } + let runfiles = try Runfiles.create( + environment: [ + "RUNFILES_DIR": runfilesDir.path, + "TEST_SRCDIR": "always ignored", + ] + ) + + XCTAssertEqual(try runfiles.rlocation("a/b").path, runfilesDir.path + "/" + "a/b") + XCTAssertEqual(try runfiles.rlocation("foo").path, runfilesDir.path + "/" + "foo") + } + + func testCreatesDirectoryBasedRunfilesEnvVars() throws { + + let (runfilesDir, clean) = try createMockDirectory(name: "my_custom_runfiles"); defer { try? clean() } + let runfiles = try Runfiles.create( + environment: [ + "RUNFILES_DIR": runfilesDir.path, + "TEST_SRCDIR": "always ignored", + ] + ) + + XCTAssertEqual(runfiles.envVars(), [ + "RUNFILES_DIR": runfilesDir.path, + ]) + } + + func testFailsToCreateManifestBasedBecauseManifestDoesNotExist() { + XCTAssertNil(try? Runfiles.create( + environment: ["RUNFILES_MANIFEST_FILE": "non-existing path"] + )) + } + + func testManifestBasedRlocation() throws { + let manifestContents = """ + /Foo/runfile1 + Foo/runfile2 /Actual Path/runfile2 + Foo/Bar/runfile3 /the path/run file 3.txt + Foo/Bar/Dir /Actual Path/Directory + """ + let (manifest, clean) = try createMockFile(name: "MANIFEST", contents: manifestContents) + defer { try? clean() } + + let runfiles = try Runfiles.create( + environment: [ + "RUNFILES_MANIFEST_FILE": manifest.path, + "TEST_SRCDIR": "always ignored", + ] + ) + + XCTAssertEqual(try runfiles.rlocation("/Foo/runfile1").path, "/Foo/runfile1") + XCTAssertEqual(try runfiles.rlocation("Foo/runfile2").path, "/Actual Path/runfile2") + XCTAssertEqual(try runfiles.rlocation("Foo/Bar/runfile3").path, "/the path/run file 3.txt") + XCTAssertEqual(try runfiles.rlocation("Foo/Bar/Dir/runfile4").path, "/Actual Path/Directory/runfile4") + XCTAssertEqual( + try runfiles.rlocation("Foo/Bar/Dir/Deeply/Nested/runfile4").path, + "/Actual Path/Directory/Deeply/Nested/runfile4" + ) + XCTAssertNil(try? runfiles.rlocation("unknown")) + + XCTAssertEqual(try runfiles.rlocation("/foo").path, "/foo") + } + + func testManifestBasedRlocationWithRepoMappingFromMain() throws { + let repoMappingContents = """ + ,config.json,config.json~1.2.3 + ,my_module,_main + ,my_protobuf,protobuf~3.19.2 + ,my_workspace,_main + protobuf~3.19.2,config.json,config.json~1.2.3 + protobuf~3.19.2,protobuf,protobuf~3.19.2 + """ + let (repoMapping, cleanRepoMapping) = try createMockFile(name: "_repo_mapping", contents: repoMappingContents) + defer { try? cleanRepoMapping() } + + let manifestContents = """ + _repo_mapping \(repoMapping.path) + config.json /etc/config.json + protobuf~3.19.2/foo/runfile /Actual Path/protobuf/runfile + _main/bar/runfile /the/path/./to/other//other runfile.txt + protobuf~3.19.2/bar/dir /Actual Path/Directory + """ + let (manifest, cleanManifest) = try createMockFile(name: "MANIFEST", contents: manifestContents) + defer { try? cleanManifest() } + + let runfiles = try Runfiles.create( + environment: [ + "RUNFILES_MANIFEST_FILE": manifest.path, + "TEST_SRCDIR": "always ignored", + ] + ) + + XCTAssertEqual( + try runfiles.rlocation("my_module/bar/runfile", sourceRepository: "").path, + "/the/path/./to/other//other runfile.txt" + ) + XCTAssertEqual( + try runfiles.rlocation("my_workspace/bar/runfile", sourceRepository: "").path, + "/the/path/./to/other//other runfile.txt" + ) + XCTAssertEqual( + try runfiles.rlocation("my_protobuf/foo/runfile", sourceRepository: "").path, + "/Actual Path/protobuf/runfile" + ) + XCTAssertEqual(try runfiles.rlocation("my_protobuf/bar/dir", sourceRepository: "").path, "/Actual Path/Directory") + XCTAssertEqual( + try runfiles.rlocation("my_protobuf/bar/dir/file", sourceRepository: "").path, + "/Actual Path/Directory/file" + ) + XCTAssertEqual( + try runfiles.rlocation("my_protobuf/bar/dir/de eply/nes ted/fi~le", sourceRepository: "").path, + "/Actual Path/Directory/de eply/nes ted/fi~le" + ) + + XCTAssertNil(try? runfiles.rlocation("protobuf/foo/runfile")) + XCTAssertNil(try? runfiles.rlocation("protobuf/bar/dir")) + XCTAssertNil(try? runfiles.rlocation("protobuf/bar/dir/file")) + XCTAssertNil(try? runfiles.rlocation("protobuf/bar/dir/dir/de eply/nes ted/fi~le")) + + XCTAssertEqual(try runfiles.rlocation("_main/bar/runfile").path, "/the/path/./to/other//other runfile.txt") + XCTAssertEqual(try runfiles.rlocation("protobuf~3.19.2/foo/runfile").path, "/Actual Path/protobuf/runfile") + XCTAssertEqual(try runfiles.rlocation("protobuf~3.19.2/bar/dir").path, "/Actual Path/Directory") + XCTAssertEqual(try runfiles.rlocation("protobuf~3.19.2/bar/dir/file").path, "/Actual Path/Directory/file") + XCTAssertEqual( + try runfiles.rlocation("protobuf~3.19.2/bar/dir/de eply/nes ted/fi~le").path, + "/Actual Path/Directory/de eply/nes ted/fi~le" + ) + + XCTAssertEqual(try runfiles.rlocation("config.json").path, "/etc/config.json") + XCTAssertNil(try? runfiles.rlocation("_main")) + XCTAssertNil(try? runfiles.rlocation("my_module")) + XCTAssertNil(try? runfiles.rlocation("protobuf")) + } + + func testManifestBasedRlocationWithRepoMappingFromOtherRepo() throws { + let repoMappingContents = """ + ,config.json,config.json~1.2.3 + ,my_module,_main + ,my_protobuf,protobuf~3.19.2 + ,my_workspace,_main + protobuf~3.19.2,config.json,config.json~1.2.3 + protobuf~3.19.2,protobuf,protobuf~3.19.2 + """ + + let (repoMapping, cleanRepoMapping) = try createMockFile(name: "_repo_mapping", contents: repoMappingContents) + defer { try? cleanRepoMapping() } + + let manifestContents = """ + _repo_mapping \(repoMapping.path) + config.json /etc/config.json + protobuf~3.19.2/foo/runfile /Actual Path/protobuf/runfile + _main/bar/runfile /the/path/./to/other//other runfile.txt + protobuf~3.19.2/bar/dir /Actual Path/Directory + """ + let (manifest, cleanManifest) = try createMockFile(name: "mock_manifest", contents: manifestContents) + defer { try? cleanManifest() } + + let runfiles = try Runfiles.create( + sourceRepository: "protobuf~3.19.2", + environment: [ + "RUNFILES_MANIFEST_FILE": manifest.path, + "TEST_SRCDIR": "always ignored", + ] + ) + + XCTAssertEqual(try runfiles.rlocation("protobuf/foo/runfile").path, "/Actual Path/protobuf/runfile") + XCTAssertEqual(try runfiles.rlocation("protobuf/bar/dir").path, "/Actual Path/Directory") + XCTAssertEqual(try runfiles.rlocation("protobuf/bar/dir/file").path, "/Actual Path/Directory/file") + XCTAssertEqual( + try runfiles.rlocation("protobuf/bar/dir/de eply/nes ted/fi~le").path, + "/Actual Path/Directory/de eply/nes ted/fi~le" + ) + + XCTAssertNil(try? runfiles.rlocation("my_module/bar/runfile")) + XCTAssertNil(try? runfiles.rlocation("my_protobuf/foo/runfile")) + XCTAssertNil(try? runfiles.rlocation("my_protobuf/bar/dir")) + XCTAssertNil(try? runfiles.rlocation("my_protobuf/bar/dir/file")) + XCTAssertNil(try? runfiles.rlocation("my_protobuf/bar/dir/de eply/nes ted/fi~le")) + + XCTAssertEqual(try runfiles.rlocation("_main/bar/runfile").path, "/the/path/./to/other//other runfile.txt") + XCTAssertEqual(try runfiles.rlocation("protobuf~3.19.2/foo/runfile").path, "/Actual Path/protobuf/runfile") + XCTAssertEqual(try runfiles.rlocation("protobuf~3.19.2/bar/dir").path, "/Actual Path/Directory") + XCTAssertEqual(try runfiles.rlocation("protobuf~3.19.2/bar/dir/file").path, "/Actual Path/Directory/file") + XCTAssertEqual( + try runfiles.rlocation("protobuf~3.19.2/bar/dir/de eply/nes ted/fi~le").path, + "/Actual Path/Directory/de eply/nes ted/fi~le" + ) + + XCTAssertEqual(try runfiles.rlocation("config.json").path, "/etc/config.json") + XCTAssertNil(try? runfiles.rlocation("_main")) + XCTAssertNil(try? runfiles.rlocation("my_module")) + XCTAssertNil(try? runfiles.rlocation("protobuf")) + } + + func testDirectoryBasedRlocation() throws { + let (runfilesDir, clean) = try createMockDirectory(name: "runfiles_dir") + defer { try? clean() } + + let runfiles = try Runfiles.create( + environment: [ + "RUNFILES_DIR": runfilesDir.path, + ] + ) + + XCTAssertEqual(try runfiles.rlocation("arg").path, runfilesDir.appendingPathComponent("arg").path) + XCTAssertEqual(try runfiles.rlocation("/foo").path, "/foo") + } + + func testDirectoryBasedRlocationWithRepoMappingFromMain() throws { + let repoMappingContents = """ + _,config.json,config.json~1.2.3 + ,my_module,_main + ,my_protobuf,protobuf~3.19.2 + ,my_workspace,_main + protobuf~3.19.2,config.json,config.json~1.2.3 + protobuf~3.19.2,protobuf,protobuf~3.19.2 + """ + let (runfilesDir, clean) = try createMockDirectory(name: "runfiles_dir") + defer { try? clean() } + + let repoMappingFile = runfilesDir.appendingPathComponent("_repo_mapping") + try repoMappingContents.write(to: repoMappingFile, atomically: true, encoding: .utf8) + defer { try? FileManager.default.removeItem(at: repoMappingFile) } + + let runfiles = try Runfiles.create( + environment: [ + "RUNFILES_DIR": runfilesDir.path, + ] + ) + + XCTAssertEqual( + try runfiles.rlocation("my_module/bar/runfile").path, + runfilesDir.appendingPathComponent("_main/bar/runfile").path + ) + XCTAssertEqual( + try runfiles.rlocation("my_workspace/bar/runfile").path, + runfilesDir.appendingPathComponent("_main/bar/runfile").path + ) + XCTAssertEqual( + try runfiles.rlocation("my_protobuf/foo/runfile").path, + runfilesDir.appendingPathComponent("protobuf~3.19.2/foo/runfile").path + ) + XCTAssertEqual( + try runfiles.rlocation("my_protobuf/bar/dir").path, + runfilesDir.appendingPathComponent("protobuf~3.19.2/bar/dir").path + ) + XCTAssertEqual( + try runfiles.rlocation("my_protobuf/bar/dir/file").path, + runfilesDir.appendingPathComponent("protobuf~3.19.2/bar/dir/file").path + ) + XCTAssertEqual( + try runfiles.rlocation("my_protobuf/bar/dir/de eply/nes ted/fi~le").path, + runfilesDir.appendingPathComponent("protobuf~3.19.2/bar/dir/de eply/nes ted/fi~le").path + ) + + XCTAssertEqual( + try runfiles.rlocation("protobuf/foo/runfile").path, + runfilesDir.appendingPathComponent("protobuf/foo/runfile").path + ) + XCTAssertEqual( + try runfiles.rlocation("protobuf/bar/dir/dir/de eply/nes ted/fi~le").path, + runfilesDir.appendingPathComponent("protobuf/bar/dir/dir/de eply/nes ted/fi~le").path + ) + + XCTAssertEqual( + try runfiles.rlocation("_main/bar/runfile").path, + runfilesDir.appendingPathComponent("_main/bar/runfile").path + ) + XCTAssertEqual( + try runfiles.rlocation("protobuf~3.19.2/foo/runfile").path, + runfilesDir.appendingPathComponent("protobuf~3.19.2/foo/runfile").path + ) + XCTAssertEqual( + try runfiles.rlocation("protobuf~3.19.2/bar/dir").path, + runfilesDir.appendingPathComponent("protobuf~3.19.2/bar/dir").path + ) + XCTAssertEqual( + try runfiles.rlocation("protobuf~3.19.2/bar/dir/file").path, + runfilesDir.appendingPathComponent("protobuf~3.19.2/bar/dir/file").path + ) + XCTAssertEqual( + try runfiles.rlocation("protobuf~3.19.2/bar/dir/de eply/nes ted/fi~le").path, + runfilesDir.appendingPathComponent("protobuf~3.19.2/bar/dir/de eply/nes ted/fi~le").path + ) + + XCTAssertEqual(try runfiles.rlocation("config.json").path, runfilesDir.appendingPathComponent("config.json").path) + } + + func testDirectoryBasedRlocationWithRepoMappingFromOtherRepo() throws { + let repoMappingContents = """ + _,config.json,config.json~1.2.3 + ,my_module,_main + ,my_protobuf,protobuf~3.19.2 + ,my_workspace,_main + protobuf~3.19.2,config.json,config.json~1.2.3 + protobuf~3.19.2,protobuf,protobuf~3.19.2 + """ + let (runfilesDir, clean) = try createMockDirectory(name: "runfiles_dir") + defer { try? clean() } + + let repoMappingFile = runfilesDir.appendingPathComponent("_repo_mapping") + try repoMappingContents.write(to: repoMappingFile, atomically: true, encoding: .utf8) + defer { try? FileManager.default.removeItem(at: repoMappingFile) } + + let runfiles = try Runfiles.create( + sourceRepository: "protobuf~3.19.2", + environment: [ + "RUNFILES_DIR": runfilesDir.path, + ] + ) + + XCTAssertEqual( + try runfiles.rlocation("protobuf/foo/runfile").path, + runfilesDir.appendingPathComponent("protobuf~3.19.2/foo/runfile").path + ) + XCTAssertEqual( + try runfiles.rlocation("protobuf/bar/dir").path, + runfilesDir.appendingPathComponent("protobuf~3.19.2/bar/dir").path + ) + XCTAssertEqual( + try runfiles.rlocation("protobuf/bar/dir/file").path, + runfilesDir.appendingPathComponent("protobuf~3.19.2/bar/dir/file").path + ) + XCTAssertEqual( + try runfiles.rlocation("protobuf/bar/dir/de eply/nes ted/fi~le").path, + runfilesDir.appendingPathComponent("protobuf~3.19.2/bar/dir/de eply/nes ted/fi~le").path + ) + + XCTAssertEqual( + try runfiles.rlocation("my_module/bar/runfile").path, + runfilesDir.appendingPathComponent("my_module/bar/runfile").path + ) + XCTAssertEqual( + try runfiles.rlocation("my_protobuf/bar/dir/de eply/nes ted/fi~le").path, + runfilesDir.appendingPathComponent("my_protobuf/bar/dir/de eply/nes ted/fi~le").path + ) + + XCTAssertEqual( + try runfiles.rlocation("_main/bar/runfile").path, + runfilesDir.appendingPathComponent("_main/bar/runfile").path + ) + XCTAssertEqual( + try runfiles.rlocation("protobuf~3.19.2/foo/runfile").path, + runfilesDir.appendingPathComponent("protobuf~3.19.2/foo/runfile").path + ) + XCTAssertEqual( + try runfiles.rlocation("protobuf~3.19.2/bar/dir").path, + runfilesDir.appendingPathComponent("protobuf~3.19.2/bar/dir").path + ) + XCTAssertEqual( + try runfiles.rlocation("protobuf~3.19.2/bar/dir/file").path, + runfilesDir.appendingPathComponent("protobuf~3.19.2/bar/dir/file").path + ) + XCTAssertEqual( + try runfiles.rlocation("protobuf~3.19.2/bar/dir/de eply/nes ted/fi~le").path, + runfilesDir.appendingPathComponent("protobuf~3.19.2/bar/dir/de eply/nes ted/fi~le").path + ) + + XCTAssertEqual(try runfiles.rlocation("config.json").path, runfilesDir.appendingPathComponent("config.json").path) + } + + func testComputeRunfilesPath_withValidManifestFile() throws { + let mockManifestFile = "/path/to/manifest" + let isRunfilesManifest: (String) -> Bool = { $0 == mockManifestFile } + let isRunfilesDirectory: (String) -> Bool = { _ in false } + + let path = try computeRunfilesPath( + argv0: "/path/to/argv0", + manifestFile: mockManifestFile, + runfilesDir: nil, + isRunfilesManifest: isRunfilesManifest, + isRunfilesDirectory: isRunfilesDirectory + ) + XCTAssertEqual(path, RunfilesPath.manifest(mockManifestFile)) + } + + func testComputeRunfilesPath_withValidRunfilesDirectory() throws { + let mockRunfilesDir = "/path/to/runfiles" + let isRunfilesManifest: (String) -> Bool = { _ in false } + let isRunfilesDirectory: (String) -> Bool = { $0 == mockRunfilesDir } + + let path = try computeRunfilesPath( + argv0: "/path/to/argv0", + manifestFile: nil, + runfilesDir: mockRunfilesDir, + isRunfilesManifest: isRunfilesManifest, + isRunfilesDirectory: isRunfilesDirectory + ) + + XCTAssertEqual(path, RunfilesPath.directory(mockRunfilesDir)) + } + + func testComputeRunfilesPath_withInvalidManifestAndDirectory() { + let isRunfilesManifest: (String) -> Bool = { _ in false } + let isRunfilesDirectory: (String) -> Bool = { _ in false } + + XCTAssertThrowsError(try computeRunfilesPath( + argv0: "/path/to/argv0", + manifestFile: "/invalid/manifest", + runfilesDir: "/invalid/runfiles", + isRunfilesManifest: isRunfilesManifest, + isRunfilesDirectory: isRunfilesDirectory + )) + } + + func testComputeRunfilesPath_withWellKnownManifestFile() throws { + let argv0 = "/path/to/argv0" + let isRunfilesManifest: (String) -> Bool = { $0 == "/path/to/argv0.runfiles/MANIFEST" } + let isRunfilesDirectory: (String) -> Bool = { _ in false } + + let path = try computeRunfilesPath( + argv0: argv0, + manifestFile: nil, + runfilesDir: nil, + isRunfilesManifest: isRunfilesManifest, + isRunfilesDirectory: isRunfilesDirectory + ) + + XCTAssertEqual(path, RunfilesPath.manifest("/path/to/argv0.runfiles/MANIFEST")) + } + + func testComputeRunfilesPath_withWellKnownRunfilesDir() throws { + let argv0 = "/path/to/argv0" + let isRunfilesManifest: (String) -> Bool = { _ in false } + let isRunfilesDirectory: (String) -> Bool = { $0 == "/path/to/argv0.runfiles" } + + let path = try computeRunfilesPath( + argv0: argv0, + manifestFile: nil, + runfilesDir: nil, + isRunfilesManifest: isRunfilesManifest, + isRunfilesDirectory: isRunfilesDirectory + ) + + XCTAssertEqual(path, RunfilesPath.directory("/path/to/argv0.runfiles")) + } + + func testComputeRunfilesPath_missingRunfilesLocations() { + let argv0 = "/path/to/argv0" + let isRunfilesManifest: (String) -> Bool = { _ in false } + let isRunfilesDirectory: (String) -> Bool = { _ in false } + + XCTAssertThrowsError(try computeRunfilesPath( + argv0: argv0, + manifestFile: nil, + runfilesDir: nil, + isRunfilesManifest: isRunfilesManifest, + isRunfilesDirectory: isRunfilesDirectory + )) + } + +} + +enum RunfilesTestError: Error { + case missingTestTmpDir +} + +func createMockFile(name: String, contents: String) throws -> (URL, () throws -> Void) { + + guard let tmpBaseDirectory = ProcessInfo.processInfo.environment["TEST_TMPDIR"] else { + XCTFail() + throw RunfilesTestError.missingTestTmpDir + } + + let fallbackTempDirectory = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) + let tempDirectory = URL(fileURLWithPath: tmpBaseDirectory).appendingPathComponent(fallbackTempDirectory.lastPathComponent) + let tempFile = tempDirectory.appendingPathComponent(name) + + try FileManager.default.createDirectory(at: tempDirectory, withIntermediateDirectories: true) + try contents.write(to: tempFile, atomically: true, encoding: .utf8) + + return (tempFile, { + try FileManager.default.removeItem(at: tempFile) + try FileManager.default.removeItem(at: tempDirectory) + }) +} + +func createMockDirectory(name _: String) throws -> (URL, () throws -> Void) { + guard let tmpBaseDirectory = ProcessInfo.processInfo.environment["TEST_TMPDIR"] else { + XCTFail() + throw RunfilesTestError.missingTestTmpDir + } + + let fallbackTempDirectory = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) + let tempDirectory = URL(fileURLWithPath: tmpBaseDirectory).appendingPathComponent(fallbackTempDirectory.lastPathComponent) + + try FileManager.default.createDirectory(at: tempDirectory, withIntermediateDirectories: true) + + return (tempDirectory, { + try FileManager.default.removeItem(at: tempDirectory) + }) +}