diff --git a/Sources/Container-Compose/Commands/ComposeDown.swift b/Sources/Container-Compose/Commands/ComposeDown.swift index a12faa8..6770d11 100644 --- a/Sources/Container-Compose/Commands/ComposeDown.swift +++ b/Sources/Container-Compose/Commands/ComposeDown.swift @@ -44,28 +44,39 @@ public struct ComposeDown: AsyncParsableCommand { private var cwd: String { process.cwd ?? FileManager.default.currentDirectoryPath } @Option(name: [.customShort("f"), .customLong("file")], help: "The path to your Docker Compose file") - var composeFilename: String = "compose.yml" - private var composePath: String { "\(cwd)/\(composeFilename)" } // Path to compose.yml + var composeFilename: String? - private var fileManager: FileManager { FileManager.default } - private var projectName: String? + private static let supportedComposeFilenames = [ + "compose.yml", + "compose.yaml", + "docker-compose.yml", + "docker-compose.yaml", + ] - public mutating func run() async throws { + private var cwdURL: URL { + URL(fileURLWithPath: cwd) + } - // Check for supported filenames and extensions - let filenames = [ - "compose.yml", - "compose.yaml", - "docker-compose.yml", - "docker-compose.yaml", - ] - for filename in filenames { - if fileManager.fileExists(atPath: "\(cwd)/\(filename)") { - composeFilename = filename - break + private var composePath: String { + if let composeFilename { + return resolvedPath(for: composeFilename, relativeTo: cwdURL) + } + + for filename in Self.supportedComposeFilenames { + let candidate = cwdURL.appending(path: filename).path + if fileManager.fileExists(atPath: candidate) { + return candidate } } + return cwdURL.appending(path: Self.supportedComposeFilenames[0]).path + } + + private var fileManager: FileManager { FileManager.default } + private var projectName: String? + + public mutating func run() async throws { + // Read docker-compose.yml content guard let yamlData = fileManager.contents(atPath: composePath) else { let path = URL(fileURLWithPath: composePath) diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index dea941c..756d793 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -47,8 +47,42 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { var detach: Bool = false @Option(name: [.customShort("f"), .customLong("file")], help: "The path to your Docker Compose file") - var composeFilename: String = "compose.yml" - private var composePath: String { "\(cwd)/\(composeFilename)" } // Path to compose.yml + var composeFilename: String? + + private static let supportedComposeFilenames = [ + "compose.yml", + "compose.yaml", + "docker-compose.yml", + "docker-compose.yaml", + ] + + private var cwdURL: URL { + URL(fileURLWithPath: cwd) + } + + private var composePath: String { + if let composeFilename { + return resolvedPath(for: composeFilename, relativeTo: cwdURL) + } + + for filename in Self.supportedComposeFilenames { + let candidate = cwdURL.appending(path: filename).path + if fileManager.fileExists(atPath: candidate) { + return candidate + } + } + + return cwdURL.appending(path: Self.supportedComposeFilenames[0]).path + } + + private var envFilePath: String { + let envFile = process.envFile.first ?? ".env" + return resolvedPath(for: envFile, relativeTo: cwdURL) + } + + private var composeDirectory: String { + URL(fileURLWithPath: composePath).deletingLastPathComponent().path + } @Flag(name: [.customShort("b"), .customLong("build")]) var rebuild: Bool = false @@ -63,7 +97,6 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { var logging: Flags.Logging private var cwd: String { process.cwd ?? FileManager.default.currentDirectoryPath } - var envFilePath: String { "\(cwd)/\(process.envFile.first ?? ".env")" } // Path to optional .env file private var fileManager: FileManager { FileManager.default } private var projectName: String? @@ -76,20 +109,6 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { ] public mutating func run() async throws { - // Check for supported filenames and extensions - let filenames = [ - "compose.yml", - "compose.yaml", - "docker-compose.yml", - "docker-compose.yaml", - ] - for filename in filenames { - if fileManager.fileExists(atPath: "\(cwd)/\(filename)") { - composeFilename = filename - break - } - } - // Read compose.yml content guard let yamlData = fileManager.contents(atPath: composePath) else { let path = URL(fileURLWithPath: composePath) @@ -406,7 +425,7 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { if let envFiles = service.env_file { for envFile in envFiles { - let additionalEnvVars = loadEnvFile(path: "\(cwd)/\(envFile)") + let additionalEnvVars = loadEnvFile(path: URL(fileURLWithPath: envFile, relativeTo: URL(fileURLWithPath: composeDirectory)).path) combinedEnv.merge(additionalEnvVars) { (current, _) in current } } } @@ -610,15 +629,15 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { } // Build command arguments - var commands = ["\(self.cwd)/\(buildConfig.context)"] - + var commands = [URL(fileURLWithPath: buildConfig.context, relativeTo: URL(fileURLWithPath: composeDirectory)).path] + // Add build arguments for (key, value) in buildConfig.args ?? [:] { commands.append(contentsOf: ["--build-arg", "\(key)=\(resolveVariable(value, with: environmentVariables))"]) } - + // Add Dockerfile path - commands.append(contentsOf: ["--file", "\(self.cwd)/\(buildConfig.dockerfile ?? "Dockerfile")"]) + commands.append(contentsOf: ["--file", URL(fileURLWithPath: buildConfig.dockerfile ?? "Dockerfile", relativeTo: URL(fileURLWithPath: composeDirectory)).path]) // Add caching options if noCache { diff --git a/Sources/Container-Compose/Helper Functions.swift b/Sources/Container-Compose/Helper Functions.swift index 0dfd152..963c449 100644 --- a/Sources/Container-Compose/Helper Functions.swift +++ b/Sources/Container-Compose/Helper Functions.swift @@ -26,6 +26,12 @@ import Yams import Rainbow import ContainerCommands +public func resolvedPath(for path: String, relativeTo baseURL: URL) -> String { + let expandedPath = NSString(string: path).expandingTildeInPath + return URL(fileURLWithPath: expandedPath, relativeTo: baseURL).standardizedFileURL.path +} + + /// Loads environment variables from a .env file. /// - Parameter path: The full path to the .env file. /// - Returns: A dictionary of key-value pairs representing environment variables. diff --git a/Tests/Container-Compose-StaticTests/HelperFunctionsTests.swift b/Tests/Container-Compose-StaticTests/HelperFunctionsTests.swift index c682828..f793e6a 100644 --- a/Tests/Container-Compose-StaticTests/HelperFunctionsTests.swift +++ b/Tests/Container-Compose-StaticTests/HelperFunctionsTests.swift @@ -32,6 +32,24 @@ struct HelperFunctionsTests { #expect(projectName == "_devcontainers") } + @Test("Resolve explicit relative paths against base URL") + func testResolvedPathRelativeSegments() throws { + let baseURL = URL(fileURLWithPath: "/tmp/project/compose/compose.yml").deletingLastPathComponent() + + #expect(resolvedPath(for: "./file.yaml", relativeTo: baseURL) == "/tmp/project/compose/file.yaml") + #expect(resolvedPath(for: "../shared/file.yaml", relativeTo: baseURL) == "/tmp/project/shared/file.yaml") + #expect(resolvedPath(for: "configs/dev/compose.yaml", relativeTo: baseURL) == "/tmp/project/compose/configs/dev/compose.yaml") + } + + @Test("Resolve absolute and tilde paths without rebasing") + func testResolvedPathAbsoluteAndTilde() throws { + let baseURL = URL(fileURLWithPath: "/tmp/project/compose") + let homePath = FileManager.default.homeDirectoryForCurrentUser.path + + #expect(resolvedPath(for: "/var/tmp/compose.yaml", relativeTo: baseURL) == "/var/tmp/compose.yaml") + #expect(resolvedPath(for: "~/compose.yaml", relativeTo: baseURL) == "\(homePath)/compose.yaml") + } + @Test("Compose port - simple container port") func testPortSimple() throws { let result = composePortToRunArg("3000")