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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/compatibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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-<name>.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/<bottle>/` (`title-<name>.log`). Paths in it show your macOS user name.
render: text
1 change: 1 addition & 0 deletions Sources/SakeKit/Bottle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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") }
Expand Down
4 changes: 2 additions & 2 deletions Sources/SakeKit/InstallerRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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? {
Expand Down Expand Up @@ -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() }
Expand Down
4 changes: 4 additions & 0 deletions Sources/SakeKit/Paths.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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") }
Expand Down
4 changes: 2 additions & 2 deletions Sources/SakeKit/TitleLauncher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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() }
Expand Down
8 changes: 4 additions & 4 deletions Sources/SakeKit/WineTool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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") }
}
Expand Down
2 changes: 1 addition & 1 deletion Tests/SakeKitTests/InstallTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ private func collect(_ stream: AsyncStream<InstallEvent>) 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 {
Expand Down
26 changes: 24 additions & 2 deletions Tests/SakeKitTests/TitleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)

Expand Down Expand Up @@ -148,6 +150,26 @@ private func collect(_ stream: AsyncStream<LaunchEvent>) 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) }
Expand Down
2 changes: 1 addition & 1 deletion Tests/SakeKitTests/WineTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
}
3 changes: 2 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<id>.log` for a title,
- Starting a title or an installer writes a log under
`~/Library/Caches/Sake/build/bottles/default` — `title-<id>.log` for a title,
`install-<name>.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.
3 changes: 3 additions & 0 deletions docs/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>/ 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
```
Expand Down
8 changes: 4 additions & 4 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<id>.log` for a title
- `install-<name>.log` for an installer
- `tool-<name>.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.
Expand Down
Loading