From b240c4743a4981f18b071847105881a4bb72994f Mon Sep 17 00:00:00 2001 From: Daisuke Murase Date: Thu, 1 Oct 2026 12:01:45 -0700 Subject: [PATCH] fix: log what runs in each bottle to a directory of its own, because the same title in two bottles shared one log Fixes #14. --- .github/ISSUE_TEMPLATE/compatibility.yml | 2 +- Sources/SakeKit/Bottle.swift | 1 + Sources/SakeKit/InstallerRunner.swift | 4 ++-- Sources/SakeKit/Paths.swift | 4 ++++ Sources/SakeKit/TitleLauncher.swift | 4 ++-- Sources/SakeKit/WineTool.swift | 8 ++++---- Tests/SakeKitTests/InstallTests.swift | 2 +- Tests/SakeKitTests/TitleTests.swift | 26 ++++++++++++++++++++++-- Tests/SakeKitTests/WineTests.swift | 2 +- docs/getting-started.md | 3 ++- docs/layout.md | 3 +++ docs/troubleshooting.md | 8 ++++---- 12 files changed, 49 insertions(+), 18 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/compatibility.yml b/.github/ISSUE_TEMPLATE/compatibility.yml index 2ddc8a5..ebf1819 100644 --- a/.github/ISSUE_TEMPLATE/compatibility.yml +++ b/.github/ISSUE_TEMPLATE/compatibility.yml @@ -56,5 +56,5 @@ body: id: log attributes: label: Log - description: If it did not work, the title's log from `~/Library/Caches/Sake/build/` (`title-.log`). Paths in it show your macOS user name. + description: If it did not work, the title's log from `~/Library/Caches/Sake/build/bottles//` (`title-.log`). Paths in it show your macOS user name. render: text diff --git a/Sources/SakeKit/Bottle.swift b/Sources/SakeKit/Bottle.swift index 5ef45c6..765a567 100644 --- a/Sources/SakeKit/Bottle.swift +++ b/Sources/SakeKit/Bottle.swift @@ -62,6 +62,7 @@ public struct Bottle: Sendable, Equatable { public var url: URL { paths.bottle(named: name) } public var engine: URL { paths.engine } + public var logs: URL { paths.bottleLogs(named: name) } public var driveC: URL { url.appending(path: "drive_c") } public var system32: URL { driveC.appending(path: "windows/system32") } diff --git a/Sources/SakeKit/InstallerRunner.swift b/Sources/SakeKit/InstallerRunner.swift index b843286..f73242c 100644 --- a/Sources/SakeKit/InstallerRunner.swift +++ b/Sources/SakeKit/InstallerRunner.swift @@ -43,7 +43,7 @@ public struct InstallerRunner: Sendable { } public var logURL: URL { - paths.build.appending(path: "install-\(installer.deletingPathExtension().lastPathComponent).log") + bottle.logs.appending(path: "install-\(installer.deletingPathExtension().lastPathComponent).log") } public var missingPrerequisite: String? { @@ -79,7 +79,7 @@ public struct InstallerRunner: Sendable { do { if let missing = missingPrerequisite { throw InstallError.notReady(missing) } try FileManager.default.createDirectory( - at: paths.build, withIntermediateDirectories: true + at: bottle.logs, withIntermediateDirectories: true ) let log = try LogFile(at: logURL) defer { log.close() } diff --git a/Sources/SakeKit/Paths.swift b/Sources/SakeKit/Paths.swift index 8e369f6..646be26 100644 --- a/Sources/SakeKit/Paths.swift +++ b/Sources/SakeKit/Paths.swift @@ -37,6 +37,10 @@ public struct Paths: Sendable, Equatable { public var toolchain: URL { cache.appending(path: "toolchain") } public var build: URL { cache.appending(path: "build") } + /// The logs of what runs in one bottle. A directory, not the bottle's name in each file's: + /// a name is free text, and `games` + `old-steam` would be `games-old` + `steam`. + public func bottleLogs(named name: String) -> URL { build.appending(path: "bottles").appending(path: name) } + /// Wine is built out of tree, which leaves its unpacked source as it came out of the /// tarball -- the only copy sake has of it. public var wineBuild: URL { build.appending(path: "wine") } diff --git a/Sources/SakeKit/TitleLauncher.swift b/Sources/SakeKit/TitleLauncher.swift index 9977261..e525cf8 100644 --- a/Sources/SakeKit/TitleLauncher.swift +++ b/Sources/SakeKit/TitleLauncher.swift @@ -37,7 +37,7 @@ public struct TitleLauncher: Sendable { self.title = title } - public var logURL: URL { paths.build.appending(path: "title-\(title.id).log") } + public var logURL: URL { bottle.logs.appending(path: "title-\(title.id).log") } public var missingPrerequisite: String? { guard bottle.exists else { @@ -73,7 +73,7 @@ public struct TitleLauncher: Sendable { do { if let missing = missingPrerequisite { throw LaunchError.notReady(missing) } try FileManager.default.createDirectory( - at: paths.build, withIntermediateDirectories: true + at: bottle.logs, withIntermediateDirectories: true ) let log = try LogFile(at: logURL) defer { log.close() } diff --git a/Sources/SakeKit/WineTool.swift b/Sources/SakeKit/WineTool.swift index c5cd1b9..f637944 100644 --- a/Sources/SakeKit/WineTool.swift +++ b/Sources/SakeKit/WineTool.swift @@ -30,17 +30,17 @@ public enum WineTool: String, CaseIterable, Sendable, Identifiable { bottle.command("wine", [rawValue], workingDirectory: bottle.driveC) } - public func logURL(in paths: Paths) -> URL { - paths.build.appending(path: "tool-\(rawValue).log") + public func logURL(in bottle: Bottle) -> URL { + bottle.logs.appending(path: "tool-\(rawValue).log") } /// Started and then left alone: these are windows somebody closes themselves, so there /// is nothing to report progress on and nothing to wait for. public func start(in bottle: Bottle, runner: ProcessRunner = ProcessRunner()) async { try? FileManager.default.createDirectory( - at: bottle.paths.build, withIntermediateDirectories: true + at: bottle.logs, withIntermediateDirectories: true ) - let log = try? LogFile(at: logURL(in: bottle.paths)) + let log = try? LogFile(at: logURL(in: bottle)) defer { log?.close() } _ = try? await runner.run(command(in: bottle)) { line in log?.write(line.text + "\n") } } diff --git a/Tests/SakeKitTests/InstallTests.swift b/Tests/SakeKitTests/InstallTests.swift index 6f6c7cd..8d5b024 100644 --- a/Tests/SakeKitTests/InstallTests.swift +++ b/Tests/SakeKitTests/InstallTests.swift @@ -127,7 +127,7 @@ private func collect(_ stream: AsyncStream) async -> [InstallEvent let log = try String(contentsOf: runner.logURL, encoding: .utf8) #expect(log.contains("=== install \(installer.path)")) #expect(log.contains("installing")) - #expect(runner.logURL.lastPathComponent == "install-Setup.log") + #expect(runner.logURL == Bottle(paths: paths).logs.appending(path: "install-Setup.log")) } @Test func whatCannotBeInstalledIsSaidRatherThanAttempted() async throws { diff --git a/Tests/SakeKitTests/TitleTests.swift b/Tests/SakeKitTests/TitleTests.swift index 176dddf..53ec5d3 100644 --- a/Tests/SakeKitTests/TitleTests.swift +++ b/Tests/SakeKitTests/TitleTests.swift @@ -24,7 +24,9 @@ private let battleNet = Title( /// An engine whose `wine` and `wineserver` record how they were called, and a bottle with /// the title installed in it. -private func makeEngineAndBottle(_ paths: Paths, installing title: Title? = battleNet) throws { +private func makeEngineAndBottle( + _ paths: Paths, named name: String = Bottle.defaultName, installing title: Title? = battleNet +) throws { let bin = paths.engine.appending(path: "bin") try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true) @@ -51,7 +53,7 @@ private func makeEngineAndBottle(_ paths: Paths, installing title: Title? = batt try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: url.path) } - let bottle = Bottle(paths: paths) + let bottle = Bottle(paths: paths, name: name) try FileManager.default.createDirectory(at: bottle.url, withIntermediateDirectories: true) try Data().write(to: bottle.systemRegistry) @@ -148,6 +150,26 @@ private func collect(_ stream: AsyncStream) async -> [LaunchEvent] #expect(log.contains("=== launch Battle.net.exe --use-gl=angle")) } +@Test func theSameTitleInTwoBottlesLeavesALogInEach() async throws { + let paths = temporaryRoot() + defer { remove(paths) } + // A title's id is unique only within its bottle: on 2026-09-29 `default` and `ex` both had + // a `steam`, and a run in one emptied the other's log. + try makeEngineAndBottle(paths) + try makeEngineAndBottle(paths, named: "ex") + + let here = TitleLauncher(paths: paths, title: battleNet) + let there = TitleLauncher(paths: paths, name: "ex", title: battleNet) + _ = await collect(here.launch()) + _ = await collect(there.launch()) + + #expect(here.logURL != there.logURL) + for launcher in [here, there] { + let log = try String(contentsOf: launcher.logURL, encoding: .utf8) + #expect(log.contains("=== launch Battle.net.exe")) + } +} + @Test func aStartPutsTheRuntimeInTheBottleWhateverTheTitle() async throws { let paths = temporaryRoot() defer { remove(paths) } diff --git a/Tests/SakeKitTests/WineTests.swift b/Tests/SakeKitTests/WineTests.swift index 36c7ffa..523aa02 100644 --- a/Tests/SakeKitTests/WineTests.swift +++ b/Tests/SakeKitTests/WineTests.swift @@ -473,6 +473,6 @@ private func changePatch(in paths: Paths, inserting line: String) throws { #expect(command.workingDirectory?.lastPathComponent == "drive_c") // `arch` would strip every DYLD_* variable, and wine is x86_64 already. #expect(command.architecture == .native) - #expect(tool.logURL(in: paths).lastPathComponent == "tool-\(tool.rawValue).log") + #expect(tool.logURL(in: bottle) == bottle.logs.appending(path: "tool-\(tool.rawValue).log")) } } diff --git a/docs/getting-started.md b/docs/getting-started.md index 13756ed..c68eea8 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -126,7 +126,8 @@ Nothing to set up. The engine is built with SDL2, so a controller should work. ## If it does not start -- Every run writes a log under `~/Library/Caches/Sake/build` — `title-.log` for a title, +- Starting a title or an installer writes a log under + `~/Library/Caches/Sake/build/bottles/default` — `title-.log` for a title, `install-.log` for an installer. The window shows only the last line of it. - **Wine Tools** on the bottle opens winecfg, regedit, the uninstaller and the task manager. - [Troubleshooting](troubleshooting.md) has what to try next. diff --git a/docs/layout.md b/docs/layout.md index dfe9148..8b106c3 100644 --- a/docs/layout.md +++ b/docs/layout.md @@ -33,6 +33,9 @@ measured in sake.* readable by them alone — see gdk.md ~/Library/Caches/Sake/ dl/ sources/ toolchain/ build/ downloads and build intermediates, ~4 GB + build/bottles// the last log of each title, installer and + Wine tool run in that bottle; a rename leaves + them under the old name d3dmetal/ Apple's redist/lib, kept so that the image need not stay mounted — see licensing.md ``` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 82e2e4c..91e9470 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -5,15 +5,15 @@ What to do when something goes wrong. Whether a game is known to run at all is i ## First, read the log -Every run writes a log in `~/Library/Caches/Sake/build`, and sake's window shows only its last -line: +Every run writes a log, and sake's window shows only its last line. What runs in a bottle writes +it in `~/Library/Caches/Sake/build/bottles`, in a folder named after the bottle: - `title-.log` for a title - `install-.log` for an installer - `tool-.log` for one of Wine's tools -The names do not include the bottle yet, so a run in one bottle overwrites the log of the same -title or installer in another ([#14](https://github.com/typester/sake/issues/14)). +Setup's builds, and making or importing a bottle, write theirs in `~/Library/Caches/Sake/build` +itself. **Wine Tools**, on a bottle, opens Wine's own winecfg, regedit, uninstaller and task manager in that bottle.