fix(setup): repair a moved checkout instead of keeping stale paths

Moving the checkout to another path or account left three kinds of stale
state behind (issue #142):

- configure.py kept the previous home's default prefix. A saved prefix that
  was exactly <previous home>/.darling now follows the current host home; a
  user-chosen prefix is kept, and one inside the previous home is reported
  with the --prefix escape hatch.
- build/runtime/build was an absolute symlink, so it dangled after a move and
  the "not link.exists()" guard treated the dangling entry as absent. It is
  now a relative link and an absolute or dangling leftover is replaced. A
  real directory at that path is reported and left untouched.
- A CMake cache generated for another source directory made the build fail
  with permission errors against the old path. configure.py now reports the
  cache and the exact directory to remove, and never deletes it itself.

Patches are rendered at prepare.py time, so the report also points at the
re-render path. Documents the moved-checkout steps and the opt-in RenderDoc
diagnostic, which still expects files no setup script creates.

Adds 7 tests to tests/test_setup_portability.py.

Signed-off-by: zero <zero@localhost>
This commit is contained in:
zero committed 2026-10-25 13:26:54 -06:00
1 parent 24276b91c6
commit 2f594b568d
3 files changed
+170 -4

No files matched your search

+25
View File
@@ -453,6 +453,31 @@ A separate multi-window path stopped the active async presenter when another dif
- **Prebuilt Swift file contains Git LFS pointer text:** install Git LFS and fetch the original upstream assets; pointer text is not a usable library.
- **Submodule fetch stops with no output right after `src/external/sqlite`:** the next submodule is `src/external/swift`, whose pinned `.lfsconfig` points at the retired `git-lfs.darlinghq.org`. The checkout is blocked on that host, not on sqlite. `prepare.py` redirects that submodule's LFS requests to GitHub. To finish a fetch that was interrupted before any patches were applied, run `git -C build/darling-src -c lfs.url=https://github.com/darlinghq/darling-swift.git/info/lfs submodule update --init --recursive --progress`, then re-run `./scripts/build.sh`. Do not run that submodule update once `prepare.py` has applied patches: it resets every submodule working tree to the pinned revision and discards the patches.
## Checkout moved to another path or account
`build/config.json`, the generated helpers and the CMake cache record absolute paths, so a checkout that was moved (for example from `/root/darling-lol` to `/home/<user>/darling-lol`, or renamed while staying in the same account) keeps pointing at the old location ([issue #142](https://github.com/lucannez64/darling-lol/issues/142)). `scripts/configure.py` now repairs the parts it owns and reports the rest on stderr:
- **Prefix:** a saved prefix that was exactly `<previous home>/.darling` follows the new host home. A prefix you chose yourself is kept, and a prefix inside the previous home prints a reminder to pass `--prefix` explicitly.
- **`build/runtime/build`:** recreated as a relative link, so it survives later moves. An absolute or dangling leftover is replaced; a real directory at that path is reported and left untouched.
- **CMake cache:** a `build/darling-src/build/CMakeCache.txt` whose `CMAKE_HOME_DIRECTORY` names a different checkout is reported, together with the path to remove. It is never deleted automatically because it can hold local build state.
After moving a checkout, run:
```sh
python3 scripts/configure.py
```
If the warning names another source directory, the cache still holds object files and dependency lists for the old path, and the build fails with `Permission denied` or `No such file or directory` against it. Remove it and rebuild (as the same user that owns the checkout):
```sh
rm -rf build/darling-src/build
./scripts/build.sh
```
Patches are rendered at `scripts/prepare.py` time, so `@PROJECT_ROOT@` in a patched file keeps the old path until it runs again. `./scripts/build.sh` and `./scripts/update.sh` both do that; do not run `prepare.py` by hand while the checkout has other edits in `build/darling-src`, because it refuses rather than resetting them.
The opt-in RenderDoc capture bridge (`RCLOL_RENDERDOC_CAPTURE=1`) is a developer diagnostic, not part of the installer. It expects `build/renderdoc/capture-bridge.so` and a RenderDoc build under `build/renderdoc/`, and neither setup script creates them. Without those files the capture is skipped and the game runs normally; the absolute path baked into the Indium patch is refreshed by the same `prepare.py` run.
## launchd crashes at boot after changing compiler flags
`darling shell` fails with `Error connecting to shellspawn` and `/tmp/lolsig.log` shows `LOLSIGFAIL pid=1 sig=11 code=1 addr=0x0` in `memmove`, called from `snprintf` in libkqueue's `epoll_event_dump`. The binaries were tuned for AMD Zen: clang 23 with `-mtune=znver*` (also selected by `-march=native` or `-mtune=native` on Zen) loads a thread-local variable's descriptor into a register other than `%rdi`, but dyld's `tlv_get_addr` reads `%rdi`, so every `__thread` and `thread_local` access returns NULL. Rebuild with `-mtune=generic` (the default in `scripts/build.sh` and the overlay builders), and check any binary with:
+60 -4
View File
@@ -1,15 +1,66 @@
#!/usr/bin/env python3
"""Render local paths into ignored runtime tools; never alter the source snapshot."""
import argparse,json,os,shutil
import argparse,json,os,shutil,sys
from pathlib import Path
from common import ROOT,configuration,render
def warn(message):
"""Diagnostics go to stderr: launcher/run.sh keep stdout for their own output."""
print('configure:',message,file=sys.stderr)
def rehome_prefix(c,requested):
"""The default prefix is derived from the host home, so a checkout that moved to another
machine or account must follow the new home instead of keeping the previous one (issue #142)."""
home=Path.home()
if requested is not None:return str(requested)
saved=c.get('prefix')
if not saved:return str(home/'.darling')
old_home=c.get('host_home')
if old_home and old_home!=str(home):
if saved==str(Path(old_home)/'.darling'):
warn(f'previous home {old_home} is gone; the default prefix moved to {home/".darling"}')
return str(home/'.darling')
try:relative=Path(saved).is_relative_to(old_home)
except (TypeError,ValueError):relative=False
if relative:
warn(f'prefix {saved} is inside the previous home {old_home}; pass --prefix to move it')
return saved
def link_build(runtime):
"""build/runtime/build must resolve to this checkout's build directory. A relative link keeps
working after the checkout moves, and any absolute or dangling leftover is replaced instead of
being treated as already correct (issue #142)."""
link=runtime/'build'
target='..' # build/runtime/build -> build
if link.is_symlink():
if os.readlink(link)==target:return
link.unlink()
elif os.path.lexists(link):
warn(f'{link} is not a symlink; leaving it untouched')
return
link.symlink_to(target,target_is_directory=True)
def stale_cache(build):
"""Return (cache, recorded source directory) when the CMake cache belongs to another checkout."""
cache=build/'darling-src/build/CMakeCache.txt'
try:lines=cache.read_text(errors='replace').splitlines()
except OSError:return None
expected=os.path.realpath(build/'darling-src')
for line in lines:
key,_,value=line.partition('=')
if key.split(':')[0]!='CMAKE_HOME_DIRECTORY':continue
if os.path.realpath(value)!=expected:return cache,value
return None
return None
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--guest-user');p.add_argument('--prefix',type=Path);p.add_argument('--sdk',type=Path)
a=p.parse_args();c=configuration()
if a.guest_user:
if not a.guest_user.replace('_','').replace('-','').isalnum():p.error('guest user must be a simple account name')
c['guest_user']=a.guest_user;c['guest_home']='/Users/'+a.guest_user
if a.prefix:c['prefix']=str(a.prefix.expanduser().resolve())
# Resolve the prefix before overwriting host_home, so it can still see the previous home.
c['prefix']=rehome_prefix(c,a.prefix.expanduser().resolve() if a.prefix else None)
if a.sdk:c['sdk']=str(a.sdk.expanduser().resolve())
c['project_root']=str(ROOT);c['host_home']=str(Path.home())
build=ROOT/'build';build.mkdir(exist_ok=True)
@@ -23,6 +74,11 @@ for source in (ROOT/'tools').iterdir():
target.unlink(missing_ok=True)
target.write_text(render(source.read_text(),c))
os.chmod(target,source.stat().st_mode & 0o777)
link=folder.parent/'build'
if not link.exists():link.symlink_to(build,target_is_directory=True)
link_build(folder.parent)
stale=stale_cache(build)
if stale:
cache,previous=stale
warn(f'{cache} was generated for {previous}, not {build/"darling-src"}; builds will fail with '
f'missing files or permission errors against the old path. Remove {build/"darling-src/build"} '
'and build again (it is never removed automatically because it may hold local work).')
print('Configured',len(list(folder.iterdir())),'runtime sources for',c['guest_home'])
+85
View File
@@ -120,6 +120,91 @@ exec REAL_INSTALL "$@"'''.replace('REAL_INSTALL', shutil.which('install')))
self.assertFalse(target.is_symlink())
def write_config(self, **changes):
(self.root / 'build/config.json').write_text(json.dumps(dict(self.config, **changes)))
def config_value(self, key):
return json.loads((self.root / 'build/config.json').read_text())[key]
def test_configure_rehomes_default_prefix_after_home_change(self):
previous = Path(self.tmp.name) / 'previous home'
self.write_config(host_home=str(previous), prefix=str(previous / '.darling'))
result = self.run_script('scripts/configure.py')
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(self.config_value('prefix'), str(self.home / '.darling'))
self.assertIn('previous home', result.stderr)
def test_configure_keeps_explicit_prefix_after_home_change(self):
previous = Path(self.tmp.name) / 'previous home'
self.write_config(host_home=str(previous), prefix=str(self.prefix))
self.configure()
self.assertEqual(self.config_value('prefix'), str(self.prefix))
self.assertEqual(self.config_value('host_home'), str(self.home))
def test_configure_warns_about_prefix_inside_previous_home(self):
previous = Path(self.tmp.name) / 'previous home'
self.write_config(host_home=str(previous), prefix=str(previous / 'custom prefix'))
result = self.run_script('scripts/configure.py')
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(self.config_value('prefix'), str(previous / 'custom prefix'))
self.assertIn('--prefix', result.stderr)
def test_configure_creates_relative_build_symlink(self):
self.configure()
link = self.root / 'build/runtime/build'
self.assertEqual(os.readlink(link), '..')
self.assertEqual(link.resolve(), (self.root / 'build').resolve())
def test_configure_repairs_absolute_and_dangling_build_symlink(self):
link = self.root / 'build/runtime/build'
link.parent.mkdir(parents=True)
link.symlink_to(self.root / 'build')
self.configure()
self.assertEqual(os.readlink(link), '..')
link.unlink()
link.symlink_to(self.root / 'moved away/build')
self.configure()
self.assertTrue(link.is_symlink())
self.assertEqual(link.resolve(), (self.root / 'build').resolve())
def test_configure_leaves_foreign_build_directory(self):
link = self.root / 'build/runtime/build'
link.mkdir(parents=True)
(link / 'keep').write_text('local build')
result = self.run_script('scripts/configure.py')
self.assertEqual(result.returncode, 0, result.stderr)
self.assertFalse(link.is_symlink())
self.assertEqual((link / 'keep').read_text(), 'local build')
self.assertIn('not a symlink', result.stderr)
def test_configure_warns_about_stale_cmake_cache(self):
cache = self.root / 'build/darling-src/build/CMakeCache.txt'
cache.parent.mkdir(parents=True)
cache.write_text('// generated\nCMAKE_HOME_DIRECTORY:INTERNAL=/root/old checkout/build/darling-src\n')
result = self.run_script('scripts/configure.py')
self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue(cache.is_file(), 'configure removed the CMake cache')
self.assertIn('/root/old checkout/build/darling-src', result.stderr)
self.assertIn('Remove', result.stderr)
def test_configure_accepts_current_cmake_cache(self):
cache = self.root / 'build/darling-src/build/CMakeCache.txt'
cache.parent.mkdir(parents=True)
cache.write_text(f'CMAKE_HOME_DIRECTORY:INTERNAL={self.root}/build/darling-src\n')
result = self.run_script('scripts/configure.py')
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stderr, '')
def test_presentation_creates_prerequisites_and_repeat_install(self):
self.configure()
self.runtime_stubs()