ADR 011 Relocating an installation - nekrozis/qtogo GitHub Wiki
Status: Accepted (2026-10-10).
- An extracted Qt tree does not work where it was unpacked: it was built to run from
/home/qt/work/install(Linux),/Users/qt/work/install(macOS) orc:/Users/qt/work/install(Windows), and the pieces that record that prefix have to be corrected. That correction is relocation, and it is a strategy layer because which pieces need correcting depends on the version, the host and the target. - The extraction and download layers already exist (ADR-005, ADR-007), the plan that says which archives to fetch exists (ADR-010), and this record fixes how the plan becomes an installed tree: where relocation sits, what it may change, and what publishing promises.
- The research behind the primitives is off-repository
(
dev/qtogo/QTGO_D4_RELOCATE_RESEARCH.md): the file-by-file action table, the measured facts that Qt ⼠5.14 relocates frombin/qt.confalone with no binary patch, and the ZAP constraint below.
-
Relocation is an ordered strategy table, and an unmatched target is a refusal.
type Capability struct { Hosts, Kinds []⌠// the host/target segments a policy covers Since, Until *Version // a half-open [Since, Until) version range Relocatable bool // whether a result anchored to its install path } func (c Capability) Supports(host, kind, v) bool type Relocator interface { Capability() Capability Relocate(ctx, tree, target) (Report, error) }
The selector is the table in order, and the first policy whose
Supportsis true runs. No policy matching isrelocate.unsupported_target, exit 7 â never a silent pass. The failure names the matrix the policies cover.A relocation that fails mid-way for a filesystem reason â a permission, a full disk â is a different outcome and a different code,
relocate.failed, at exit 6. The prefix and the number need not agree (ADR-002:usage.insecure_schemeis ausage.code that exits 8): the phase names where the failure came from and the exit code says what a caller should do, and a transient write error is worth a retry while an unsupported target is not. -
The capability check runs before anything is downloaded.
planandinstallask the selector whether the request is covered, and an uncovered one (<5.14desktop, or a cross target that has no policy yet) fails then â no archive is fetched and no directory is published. A failure that costs a gigabyte before it happens is not a check. -
A version below 5.14 is refused, not relocated, in this phase. Qt < 5.14 also needs a fixed-length slot patched inside
qmakeand the Qt5 core library, which is a different, riskier primitive; it lands separately. Until then the policy exists only to make the selector refuse it â with the same error a cross target gets, since both are "reached the right place, and this build cannot relocate it yet".The archive carries the tree's own layout, and the tool finds it rather than designing it.
--outputdiris the base directory â it may already exist, and it is not something this build replaces. Every archive a Qt package declares nests<version>/<arch>/inside itself, and the metadata'sExtractargument, where it is present, names the same path. So there is no<version>/<arch>for the tool to invent at the base, and it does not: it extracts each archive at the base, then locates the tree by the marker every Qt tree carries âqmakeunderbin/â within a bounded depth. Relocation runs on that tree, and only the tree is published, at the same relative path inside the base. Which unit is new, and which--overwritereplaces, is therefore the tree, not the base. Getting this wrong is not cosmetic: nesting the tree one level too deep, or relocating the base instead of the tree, both produce a tree that looks installed and does not run. -
The primitives are text, they are idempotent, and they never record the install path. For a covered target the policy writes and corrects only files where the pattern is present, and running it twice changes nothing the second time. What it does:
File Action Where it applies bin/qt.confcreate, [Paths]\nPrefix=..every covered desktop target mkspecs/qconfig.priQT_EDITION =âŚâOpenSource,QT_LICHECK =âŚâ emptywhen the file is there lib/pkgconfig/*.pcprefix=âŚâprefix=${pcfiledir}/../.., and mac's-FâŚâ${pcfiledir}/..when present lib/*.prlthe build prefix's lib directory â $$[QT_INSTALL_LIBS]when present lib/*.ladelete the file when present The
.prlsentinel is the lib directory rather than the prefix alone, because$$[QT_INSTALL_LIBS]is itself<prefix>/lib: substituting the bare prefix would leave the path's own/libbehind and write a doubled one. The macOS and Windows prefixes share a tail, so the longer Windows forms are matched first â otherwise the macOS path would match inside a Windows one and truncate its drive letter.The last row is a deliberate departure from the reference tool, which rewrites
.lainstead: it is the one downstream file with no relative form (its anchor is an absolute-isedlibdir), nothing consumes it for a Qt build, and it is a fraction of a percent of the tree.docs/compatibility.mdcarries the row.No value written may contain the install path.
Prefix=..,${pcfiledir}/../..and$$[QT_INSTALL_LIBS]are all relative or symbolic, which is what makes two installs of the same request byte-identical (the ZAP rule): the result must be the same wherever it lands, or relocation has only moved the problem. -
Publishing is atomic, and an existing target is refused unless asked for. The tree is built under a staging directory beside the destination, on the same volume, and moved into place with one rename once it is complete. A destination that already exists is a failure â the default, because overwriting is the destructive choice â and
--overwriteis the explicit opt-in that removes it first. A failure at any point leaves the staging directory removed and the destination untouched. This differs from the reference tool, which unpacks over an existing directory; the divergence gets its compatibility row. -
An installed tree carries a manifest, and the manifest carries no absolute path. A JSON document in the installed directory records the qtogo version, the request, the archives and their verified digests, the policy that ran and whether the result is relocatable. It is the diagnostic and uninstall basis, and it is part of the same byte-identical promise, so it holds relative paths only.
-
--dry-runis the plan command, and it is the plan command's own code path.install-qt --dry-runproduces exactly whatplan install-qtproduces, from the same planner â asserted by a test, not merely intended â and creates nothing. -
An install's result is a document with a frozen shape.
--jsonreports the resolved request, the plan, the policy and whether the tree is relocatable, and the destination â enough for automation to confirm an install without reading the tree. The shape is fixed here so it does not drift.
-
Staging, downloading and extraction all happen on the destination's volume.
The extraction merge is a rename, and a rename only works within one volume, so
the working directories live under the staging tree rather than in the OS
temporary directory. An install onto a second drive â an ordinary
-O D:\Qtâ would otherwise fail withEXDEVafter fetching everything. -
The solid-block budget has a finite default that clears a real archive. A Qt
6.8.0 desktop package holds a solid block of about 1.5 GiB, so a smaller default
refuses a flagship install before decoding anything; the default is above that,
and
--memory-budgetmoves it either way. The bound exists so a malformed archive cannot exhaust the machine, not to be the first thing a user meets. -
planand a dry run run the capability check too. They are the same walk as an install plus that check, so a version this build refuses is refused while planning rather than shown as a green plan that the install then fails on. - A relocation failure that is a filesystem problem â a permission, a full disk â is a filesystem outcome (exit 6), not "this target is unsupported" (exit 7). The two are kept apart because a script that reads "unsupported" would not retry, and a transient filesystem error deserves a retry.
- Adding coverage is a policy in the table plus its
Capability; the selector and the pre-download check need no change.LegacyQt5(binary slot) andCrossTarget(extra primitives) are policies that exist now to refuse, and grow aRelocatewhen their primitives land. - Because the primitives are "change only if the pattern is present", a target whose newer metadata no longer emits a file simply results in fewer changes, not a failure â which is the measured state of modern Qt and the reason this is safe.
- The refusal path is the promise this record buys: a version this build cannot relocate fails in a second, before the download, rather than producing an installation that looks finished and is not.
- The byte-identical rule is what the
--overwrite/refuse pair is checked against; it also means the manifest is testable as a fixed document modulo its own inputs.
-
Patching binaries for every version. Rejected for the covered range: measured,
Qt ⼠5.14 relocates from
qt.confalone, and the residual prefix strings in the libraries are inert at runtime (the Linuxqt_prfxpath=slot was proved load-bearing by neither the loader nor the library). Keeping a binary patch that changes nothing is risk without benefit. -
Rewriting
.laas the reference tool does. Rejected: its only anchor is an absolute path, which the byte-identical rule forbids, and nothing in a Qt build reads it. - Relocating after publishing. Rejected: the destination would be briefly unusable and a failure would leave a half-patched tree in place. Relocation belongs inside staging, before the single rename.
- Overwriting an existing destination by default. Rejected: it is the destructive outcome and should be asked for. The default is to refuse, which also makes a repeated install detectable rather than silently converging.