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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions Sources/CodexBarCLI/CLIIO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ extension CodexBarCLI {

static func currentVersion(
bundle: Bundle = .main,
executablePath: String? = CommandLine.arguments.first) -> String?
executablePath: String? = nil) -> String?
{
let executablePath = executablePath ?? Self.runningExecutablePath(bundle: bundle)
if let version = self.currentVersion(bundleVersion: nil, executablePath: executablePath) {
return version
}
Expand All @@ -65,6 +66,28 @@ extension CodexBarCLI {
executablePath: nil)
}

static func runningExecutablePath(bundle: Bundle = .main) -> String? {
// Bundle.executableURL keeps this seam deterministic for app bundles and tests; the
// platform-specific fallbacks cover direct standalone launches without a usable bundle.
if let path = bundle.executableURL?.path, !path.isEmpty {
return path
}

#if canImport(Darwin)
var size: UInt32 = 0
guard _NSGetExecutablePath(nil, &size) != 0 else { return nil }
var buffer = [Int8](repeating: 0, count: Int(size))
guard _NSGetExecutablePath(&buffer, &size) == 0 else { return nil }
return String(cString: buffer)
#elseif os(Linux)
let path = "/proc/self/exe"
guard FileManager.default.fileExists(atPath: path) else { return nil }
return URL(fileURLWithPath: path).resolvingSymlinksInPath().path
#else
return nil
#endif
}

static func currentVersion(bundleVersion: String?, executablePath: String?) -> String? {
if let executablePath, !executablePath.isEmpty {
let executableURL = URL(fileURLWithPath: executablePath).resolvingSymlinksInPath()
Expand All @@ -90,7 +113,7 @@ extension CodexBarCLI {
guard let data = fileManager.contents(atPath: infoURL.path),
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any]
else { return nil }
return plist["CFBundleShortVersionString"] as? String
return Self.normalizedBundleVersion(plist["CFBundleShortVersionString"] as? String)
}
currentURL.deleteLastPathComponent()
}
Expand Down
87 changes: 87 additions & 0 deletions Tests/CodexBarTests/CLIEntryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,45 @@ final class CLIEntryTests: XCTestCase {
try self.expectAdjacentVersionFile(raw: "version-3.2.3\n", expected: "version-3.2.3")
}

func test_cliVersionFindsAdjacentVersionWhenInvokedViaRelativePathAndSymlink() throws {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-cli-version-invocation-\(UUID().uuidString)", isDirectory: true)
defer { try? FileManager.default.removeItem(at: root) }

let installURL = root.appendingPathComponent("install/bin", isDirectory: true)
let linksURL = root.appendingPathComponent("links", isDirectory: true)
let workingDirectoryURL = root.appendingPathComponent("work", isDirectory: true)
try FileManager.default.createDirectory(at: installURL, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: linksURL, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: workingDirectoryURL, withIntermediateDirectories: true)

let executableURL = installURL.appendingPathComponent("CodexBarCLI")
try FileManager.default.copyItem(at: Self.cliExecutableURL, to: executableURL)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executableURL.path)
try "8.7.6\n".write(
to: installURL.appendingPathComponent("VERSION"),
atomically: false,
encoding: .utf8)

XCTAssertEqual(
try Self.runVersionCommand(
executableURL: executableURL,
argv0: "install/bin/CodexBarCLI",
currentDirectoryURL: workingDirectoryURL),
"CodexBar 8.7.6\n")

let symlinkURL = linksURL.appendingPathComponent("codexbar")
try FileManager.default.createSymbolicLink(
atPath: symlinkURL.path,
withDestinationPath: "../install/bin/CodexBarCLI")
XCTAssertEqual(
try Self.runVersionCommand(
executableURL: symlinkURL,
argv0: "codexbar",
currentDirectoryURL: workingDirectoryURL),
"CodexBar 8.7.6\n")
}

func test_cliVersionPrefersAdjacentVersionOverStandaloneBundleName() throws {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-cli-version-bundle-\(UUID().uuidString)", isDirectory: true)
Expand Down Expand Up @@ -130,6 +169,54 @@ final class CLIEntryTests: XCTestCase {
XCTAssertEqual(CodexBarCLI.currentVersion(bundleVersion: nil, executablePath: helperURL.path), expected)
}

private static func runVersionCommand(
executableURL: URL,
argv0: String,
currentDirectoryURL: URL) throws -> String
{
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
process.arguments = [
"-c",
"exec -a \"$1\" \"$2\" --version",
"codexbar-version-test",
argv0,
executableURL.path,
]
process.currentDirectoryURL = currentDirectoryURL

let stdout = Pipe()
let stderr = Pipe()
process.standardOutput = stdout
process.standardError = stderr
try process.run()
process.waitUntilExit()

let output = stdout.fileHandleForReading.readDataToEndOfFile()
let errorOutput = stderr.fileHandleForReading.readDataToEndOfFile()
guard process.terminationStatus == 0 else {
let message = String(bytes: errorOutput, encoding: .utf8)
?? "CodexBarCLI exited without an error message"
throw NSError(domain: "CLIEntryTests", code: Int(process.terminationStatus), userInfo: [
NSLocalizedDescriptionKey: message,
])
}
guard let text = String(bytes: output, encoding: .utf8) else {
throw NSError(domain: "CLIEntryTests", code: -1, userInfo: [
NSLocalizedDescriptionKey: "CodexBarCLI produced non-UTF-8 output",
])
}
return text
}

private static var cliExecutableURL: URL {
URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.appendingPathComponent(".build/debug/CodexBarCLI")
}

func test_renderOpenAIWebDashboardTextIncludesSummary() {
let event = CreditEvent(
date: Date(timeIntervalSince1970: 1_700_000_000),
Expand Down