From b57b7f02e05554ba3f6cb8ae300524b39fb766b3 Mon Sep 17 00:00:00 2001 From: gdlol <.> Date: Sun, 8 Nov 2026 14:09:15 +0000 Subject: [PATCH 1/2] Symlink feature binaries --- .devcontainer/features/src/features/install.sh | 6 ++---- features/Features/SymbolicLink.cs | 18 ++++++++++++++++++ features/code-server/Feature/Install.cs | 7 ++++--- features/dotnet/Feature/Registration.cs | 16 +++------------- features/ripgrep/NOTES.md | 16 ++++++++-------- features/ripgrep/README.md | 16 ++++++++-------- features/ripgrep/install | 2 +- features/uv/Feature/Install.cs | 3 +-- scripts/verify/ripgrep/verify.ts | 4 ++++ scripts/verify/uv/scenarios.ts | 9 ++++++++- 10 files changed, 57 insertions(+), 40 deletions(-) create mode 100644 features/Features/SymbolicLink.cs diff --git a/.devcontainer/features/src/features/install.sh b/.devcontainer/features/src/features/install.sh index faab766..94aeaab 100755 --- a/.devcontainer/features/src/features/install.sh +++ b/.devcontainer/features/src/features/install.sh @@ -7,8 +7,6 @@ set -e -feature_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) - prefix=/opt/devcontainer-config state_path=/var/lib/devcontainer-config/features @@ -20,11 +18,11 @@ case $FEATURES_PATH in esac mkdir -p "$prefix" -install -m 0755 "$feature_dir/features-run" "$prefix/features-run" +install -m 0755 ./features-run "$prefix/features-run" printf '%s\n' "$FEATURES_PATH" > "$prefix/features-path" mkdir -p "$FEATURES_PATH" "$state_path" -chmod a+rwx "$FEATURES_PATH" "$state_path" +chmod a+rwx "$state_path" exec "$prefix/features-run" install diff --git a/features/Features/SymbolicLink.cs b/features/Features/SymbolicLink.cs new file mode 100644 index 0000000..015e057 --- /dev/null +++ b/features/Features/SymbolicLink.cs @@ -0,0 +1,18 @@ +namespace DevcontainerConfig.Features; + +public static class SymbolicLink +{ + public static void Replace(string path, string target) + { + string temp = Path.Join(Path.GetDirectoryName(path), $".{Path.GetFileName(path)}-{Guid.NewGuid():N}"); + File.CreateSymbolicLink(temp, target); + try + { + File.Move(temp, path, overwrite: true); + } + finally + { + File.Delete(temp); + } + } +} diff --git a/features/code-server/Feature/Install.cs b/features/code-server/Feature/Install.cs index eb9499b..2897809 100644 --- a/features/code-server/Feature/Install.cs +++ b/features/code-server/Feature/Install.cs @@ -5,7 +5,6 @@ namespace DevcontainerConfig.CodeServer.Feature; internal static class Install { private const string BinDir = "/usr/local/bin"; - private const string LinkPath = "/usr/local/bin/code-server"; internal static void Run(string stagedDir) { @@ -13,7 +12,9 @@ internal static void Run(string stagedDir) options.Validate(); Directory.CreateDirectory(BinDir); - File.Delete(LinkPath); - File.CreateSymbolicLink(LinkPath, Path.Join(stagedDir, "code-server", "bin", "code-server")); + SymbolicLink.Replace( + Path.Join(BinDir, "code-server"), + Path.Join(stagedDir, "code-server", "bin", "code-server") + ); } } diff --git a/features/dotnet/Feature/Registration.cs b/features/dotnet/Feature/Registration.cs index 3c84fbc..0f6948f 100644 --- a/features/dotnet/Feature/Registration.cs +++ b/features/dotnet/Feature/Registration.cs @@ -1,3 +1,5 @@ +using DevcontainerConfig.Features; + namespace DevcontainerConfig.Dotnet.Feature; internal static class Registration @@ -24,19 +26,7 @@ private static void WriteInstallLocation(string root) private static void CreateSymlink(string root) { string target = Path.Join(root, "dotnet"); - - // rename(2) replaces whatever else the path holds (absent, symlink or regular file) and never resolves the - // destination, so the link lands in one step; a real directory fails the rename. - string temp = Path.Join(Path.GetDirectoryName(LinkPath), $".dotnet-{Guid.NewGuid():N}"); - File.CreateSymbolicLink(temp, target); - try - { - File.Move(temp, LinkPath, overwrite: true); - } - finally - { - File.Delete(temp); - } + SymbolicLink.Replace(LinkPath, target); Console.WriteLine($"install: linked {LinkPath} to {target}"); } diff --git a/features/ripgrep/NOTES.md b/features/ripgrep/NOTES.md index 8bbf27e..c43822d 100644 --- a/features/ripgrep/NOTES.md +++ b/features/ripgrep/NOTES.md @@ -1,10 +1,10 @@ ## Installed files -| Source | Destination | Mode | -| ------------------------------------- | ---------------------------------------------------- | ---- | -| `rg` | `/usr/local/bin/rg` | 0755 | -| `doc/rg.1` | `/usr/local/share/man/man1/rg.1` | 0644 | -| `complete/rg.bash` | `/usr/local/share/bash-completion/completions/rg` | 0644 | -| `complete/_rg` | `/usr/local/share/zsh/site-functions/_rg` | 0644 | -| `complete/rg.fish` | `/usr/local/share/fish/vendor_completions.d/rg.fish` | 0644 | -| `COPYING`, `LICENSE-MIT`, `UNLICENSE` | `/usr/local/share/doc/ripgrep/` | 0644 | +| Source | Destination | +| ------------------------------------- | ---------------------------------------------------- | +| `rg` | `/usr/local/bin/rg` | +| `doc/rg.1` | `/usr/local/share/man/man1/rg.1` | +| `complete/rg.bash` | `/usr/local/share/bash-completion/completions/rg` | +| `complete/_rg` | `/usr/local/share/zsh/site-functions/_rg` | +| `complete/rg.fish` | `/usr/local/share/fish/vendor_completions.d/rg.fish` | +| `COPYING`, `LICENSE-MIT`, `UNLICENSE` | `/usr/local/share/doc/ripgrep/` | diff --git a/features/ripgrep/README.md b/features/ripgrep/README.md index 7fb8b9c..60bd5b2 100644 --- a/features/ripgrep/README.md +++ b/features/ripgrep/README.md @@ -13,14 +13,14 @@ COPY --from=ripgrep /features/ripgrep/ /opt/devcontainer-config/features/ripgrep ## Installed files -| Source | Destination | Mode | -| ------------------------------------- | ---------------------------------------------------- | ---- | -| `rg` | `/usr/local/bin/rg` | 0755 | -| `doc/rg.1` | `/usr/local/share/man/man1/rg.1` | 0644 | -| `complete/rg.bash` | `/usr/local/share/bash-completion/completions/rg` | 0644 | -| `complete/_rg` | `/usr/local/share/zsh/site-functions/_rg` | 0644 | -| `complete/rg.fish` | `/usr/local/share/fish/vendor_completions.d/rg.fish` | 0644 | -| `COPYING`, `LICENSE-MIT`, `UNLICENSE` | `/usr/local/share/doc/ripgrep/` | 0644 | +| Source | Destination | +| ------------------------------------- | ---------------------------------------------------- | +| `rg` | `/usr/local/bin/rg` | +| `doc/rg.1` | `/usr/local/share/man/man1/rg.1` | +| `complete/rg.bash` | `/usr/local/share/bash-completion/completions/rg` | +| `complete/_rg` | `/usr/local/share/zsh/site-functions/_rg` | +| `complete/rg.fish` | `/usr/local/share/fish/vendor_completions.d/rg.fish` | +| `COPYING`, `LICENSE-MIT`, `UNLICENSE` | `/usr/local/share/doc/ripgrep/` | --- diff --git a/features/ripgrep/install b/features/ripgrep/install index e645e62..18b1095 100755 --- a/features/ripgrep/install +++ b/features/ripgrep/install @@ -2,7 +2,7 @@ set -e mkdir -p /usr/local/bin -cp -p rg /usr/local/bin/rg +ln -sfn "$PWD/rg" /usr/local/bin/rg mkdir -p /usr/local/share/man/man1 cp -p doc/rg.1 /usr/local/share/man/man1/rg.1 diff --git a/features/uv/Feature/Install.cs b/features/uv/Feature/Install.cs index 15f3d57..410a290 100644 --- a/features/uv/Feature/Install.cs +++ b/features/uv/Feature/Install.cs @@ -33,8 +33,7 @@ private static void InstallBinaries(string stagedDir) Directory.CreateDirectory(BinDir); foreach (string binary in Binaries) { - string target = Path.Join(BinDir, binary); - File.Copy(Path.Join(stagedDir, "uv", binary), target, overwrite: true); + SymbolicLink.Replace(Path.Join(BinDir, binary), Path.Join(stagedDir, "uv", binary)); } } } diff --git a/scripts/verify/ripgrep/verify.ts b/scripts/verify/ripgrep/verify.ts index 04397bd..cc028c5 100644 --- a/scripts/verify/ripgrep/verify.ts +++ b/scripts/verify/ripgrep/verify.ts @@ -44,6 +44,10 @@ const defaultExecs = (version: string): Exec[] => [ command: ["sh", "-c", "command -v rg"], expect: { stdout: ["/usr/local/bin/rg"] }, }, + { + command: ["readlink", "-f", "/usr/local/bin/rg"], + expect: { stdout: ["/opt/devcontainer-config/features/ripgrep/rg"] }, + }, { command: ["sh", "-c", "printf 'alpha\\nbeta\\n' | rg beta"], expect: { stdout: ["beta"] }, diff --git a/scripts/verify/uv/scenarios.ts b/scripts/verify/uv/scenarios.ts index d2ab2e0..022a1da 100644 --- a/scripts/verify/uv/scenarios.ts +++ b/scripts/verify/uv/scenarios.ts @@ -113,7 +113,14 @@ const binariesExecs = (selection: Selection): Exec[] => [ command: ["sh", "-c", "command -v uv; command -v uvx"], expect: { stdout: ["/usr/local/bin/uv", "/usr/local/bin/uvx"] }, }, - { command: ["stat", "-c", "%a", "/usr/local/bin/uv", "/usr/local/bin/uvx"], expect: { stdout: ["755", "755"] } }, + { + command: ["readlink", "-f", "/usr/local/bin/uv", "/usr/local/bin/uvx"], + expect: { stdout: [`${featureDir}/uv/uv`, `${featureDir}/uv/uvx`] }, + }, + { + command: ["stat", "-Lc", "%a", "/usr/local/bin/uv", "/usr/local/bin/uvx"], + expect: { stdout: ["755", "755"] }, + }, { command: ["sh", "-c", `for file in ${completionFiles.join(" ")}; do test -s "$file" || exit 1; done; echo present`], expect: { stdout: ["present"] }, From b42e41a96c25b3c94c7752625f2336ff1999716f Mon Sep 17 00:00:00 2001 From: gdlol <.> Date: Sun, 8 Nov 2026 14:13:15 +0000 Subject: [PATCH 2/2] bump revisions --- features/code-server/config.json | 2 +- features/dotnet/config.json | 2 +- features/ripgrep/config.json | 2 +- features/uv/config.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/features/code-server/config.json b/features/code-server/config.json index 36de4e7..e30bffd 100644 --- a/features/code-server/config.json +++ b/features/code-server/config.json @@ -1,5 +1,5 @@ { - "revision": 1, + "revision": 2, "versions": { "4.145.2": { "linux-x64": { diff --git a/features/dotnet/config.json b/features/dotnet/config.json index 63155fb..dbe8ebc 100644 --- a/features/dotnet/config.json +++ b/features/dotnet/config.json @@ -1,5 +1,5 @@ { - "revision": 7, + "revision": 8, "components": { "sdk": { "versions": { diff --git a/features/ripgrep/config.json b/features/ripgrep/config.json index 53ea9b2..0944c3d 100644 --- a/features/ripgrep/config.json +++ b/features/ripgrep/config.json @@ -1,5 +1,5 @@ { - "revision": 3, + "revision": 4, "versions": { "15.2.0": { "x86_64-unknown-linux-musl": { diff --git a/features/uv/config.json b/features/uv/config.json index 29193f4..6c4d3fd 100644 --- a/features/uv/config.json +++ b/features/uv/config.json @@ -1,5 +1,5 @@ { - "revision": 2, + "revision": 3, "components": { "uv": { "versions": {