Skip to content

fix(wine): keep the profile reachable under both lineages' names - #236

Merged
frankea merged 1 commit into
mainfrom
fix/profile-directory-reconcile
Aug 29, 2026
Merged

fix(wine): keep the profile reachable under both lineages' names#236
frankea merged 1 commit into
mainfrom
fix/profile-directory-reconcile

Conversation

@frankea

@frankea frankea commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Closes the users/crossover gate on the v4 engine promotion (#163).

the problem

Every Wine build names the profile directory after the Unix user (set_home_dir in ntdll is identical in upstream and winecx), so a bottle created on the stock engine has drive_c/users/<name>. CrossOver-lineage builds carry "CrossOver Hack 12735" in shell32's shellpath.c, which expands %USERPROFILE% to C:\users\crossover regardless of the user. Open a stock bottle on a v4 runtime and the shell folders resolve into a fresh, empty users/crossover: Steam asks for a login, saves are gone, nothing errors. The name is a compile-time constant in that build, so no environment variable steers it; the fix has to be on disk.

the change

WineUserProfile.reconcile(bottleURL:) runs before every launch that goes through a bottle (runProgram, runWineProcess(bottle:), runWineWithBottle, and the terminal generateRunCommand). It makes the profile reachable under both names:

  • one name present as a directory, the other absent: the absent one becomes a relative symlink to it, in either direction
  • both present, one an untouched skeleton (directories and Temp contents only, no regular file anywhere else): the skeleton is moved to <bottle>/.profile-reconcile/<name>-<timestamp> and replaced by the symlink. Moved, never deleted, and parked outside drive_c/users so neither Wine nor detectWineUsername sees it as a profile
  • both populated: untouched, .bothPopulated, merging is a decision for a person
  • either name already a symlink: untouched, including hand-made ones that point somewhere else
  • no users directory or no profile under either name: no-op

The common case is a handful of stat calls. detectWineUsername already prefers the non-crossover name, and it sees through the symlink either way, so the rest of the app keeps working with the paths it has.

verified

  • 9 new Swift Testing cases covering every branch above, including the Temp exclusion and idempotence
  • WhiskyKit: 1294 XCTest plus 234 Swift Testing, 0 failures
  • Whisky app builds; pinned SwiftFormat 0.58.7 lint 0/3, SwiftLint strict clean
  • Changelog entry under Fixed

Not yet exercised against a real v4 runtime; that check is part of the beta audit on #163.

CrossOver-lineage builds hardcode users/crossover for the shell folders
while every Wine build names the real profile after the Unix user, so a
bottle opened on the other lineage booted into an empty profile. Symlink
whichever name is missing to the one holding the data before each launch,
and only displace a directory that is an untouched skeleton.
@codecov

codecov Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.41758% with 16 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...kyKit/Sources/WhiskyKit/Wine/WineUserProfile.swift 87.05% 11 Missing ⚠️
WhiskyKit/Sources/WhiskyKit/Wine/Wine.swift 16.66% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@frankea

frankea commented Aug 29, 2026

Copy link
Copy Markdown
Owner Author

Review notes, since this one cannot be self-approved.

The mechanism is verified at the source: ntdll's set_home_dir is identical in upstream wine-11.16 and winecx (name from USER, then getpwuid), and the divergence is entirely shell32's hardcoded "crossover" in shellpath.c, so a filesystem alias is the only lever the app has. The reconcile sits on every launch path that carries a bottle (runProgram, runWineProcess, runWineWithBottle, generateRunCommand) and skips wineserver, which never touches the profile.

Safety properties I checked against the tests: a directory is only replaced when it has no regular file outside Temp, the displaced skeleton is parked at .profile-reconcile inside the bottle root rather than under drive_c/users, so detectWineUsername cannot pick it up, and a failed link after the move puts the skeleton back. Hand-made symlinks are respected, two populated profiles are left alone.

Full kit suite 1294 XCTest plus 234 Swift Testing green, app target builds, both lints clean, CI green. Merging; the real-runtime exercise happens as part of the beta audit on #163.

@frankea
frankea merged commit fb03370 into main Aug 29, 2026
10 checks passed
@frankea
frankea deleted the fix/profile-directory-reconcile branch August 29, 2026 07:38
@frankea

frankea commented Aug 29, 2026

Copy link
Copy Markdown
Owner Author

Exercised against real runtimes now, closing the caveat in the description. A prefix booted by the stock 3.1.1 engine (users/afranke, with a marker file in AppData/Roaming) reconciled to crossover -> afranke through WineUserProfile.reconcile. Opened with the v4.6.4-beta.1 runtime: wineboot -u exits 0, cmd reports USERPROFILE as C:\users\crossover and APPDATA under it, the symlink is intact afterwards, the marker is readable through the crossover path, and no third profile or displaced skeleton appeared. That is the exact scenario the empty-profile reports came from.

dappermint pushed a commit to dappermint/Whisky that referenced this pull request Aug 29, 2026
…nkea#236)

CrossOver-lineage builds hardcode users/crossover for the shell folders
while every Wine build names the real profile after the Unix user, so a
bottle opened on the other lineage booted into an empty profile. Symlink
whichever name is missing to the one holding the data before each launch,
and only displace a directory that is an untouched skeleton.

(cherry picked from commit fb03370)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant