Skip to content

fix: relative symlink resolution in install script - #1244

Open
V-Subhankar-infy wants to merge 5 commits into
devcontainers:mainfrom
V-Subhankar-infy:main
Open

fix: relative symlink resolution in install script#1244
V-Subhankar-infy wants to merge 5 commits into
devcontainers:mainfrom
V-Subhankar-infy:main

Conversation

@V-Subhankar-infy

@V-Subhankar-infy V-Subhankar-infy commented Jun 11, 2026

Copy link
Copy Markdown
Member

Summary

Fixes relative symlink resolution in the install script for split local layouts (e.g. a launcher in ~/.local/bin pointing to a payload in ~/.local/lib/devcontainers). The wrapper now resolves its own location with portable POSIX shell built-ins instead of GNU-specific readlink -f, so devcontainer works regardless of the caller's working directory and shell environment (Linux, macOS, BSD).

Fixes #1232

Root Cause

A single readlink call plus cd "$(dirname ...)" && pwd resolved relative symlink targets against the caller's CWD rather than the link's own directory, and the readlink -f fallback isn't portable to macOS/BSD.

Reproduction of error

./install.sh --prefix "$HOME/.local/lib/devcontainers"
ln -s ../lib/devcontainers/bin/devcontainer "$HOME/.local/bin/devcontainer"
export PATH="$HOME/.local/bin:$PATH"
cd "$HOME" && devcontainer --version   # cd: ../lib/devcontainers/bin: No such file or directory

Fix

The script-location resolver now walks the symlink chain one hop at a time, cd-ing into each link's directory before reading the next target, then canonicalizes the final directory. This replaces the previous single readlink resolution.

Why It Works

  • Correct relative resolution: enters each link's directory before reading the next target, so relative symlinks resolve against the link, not the CWD.
  • CWD-independent: finalized with cd -P … && pwd for an absolute, canonical path.
  • Portable: POSIX-only (readlink, ${var%/*}, cd -P, pwd) — no readlink -f/realpath.
  • Safe: runs in a subshell with CDPATH= cleared; no leaked cd or stray output.

Validation

Regression test confirms a relative symlink launched from another directory exits 0 and reports the installed CLI version.

Scope & Risk

One resolution block; no install/download changes. Direct execution and absolute symlinks are unchanged. Low risk, plus improved cross-platform portability.

Credits

Thanks to @klmr for the portable POSIX symlink-resolution approach.

@V-Subhankar-infy V-Subhankar-infy changed the title Fix relative symlink resolution in install script fix: relative symlink resolution in install script Jun 11, 2026
@V-Subhankar-infy
V-Subhankar-infy marked this pull request as ready for review June 14, 2026 09:37
@V-Subhankar-infy
V-Subhankar-infy requested a review from a team as a code owner June 14, 2026 09:37
@V-Subhankar-infy
V-Subhankar-infy marked this pull request as draft June 19, 2026 10:05
@klmr

klmr commented Jun 19, 2026

Copy link
Copy Markdown

Beware that readlink -f support has only been added to macOS fairly recently. A portable alternative is much more convoluted, alas:

--- a/devcontainer      2026-06-19 15:12:58
+++ b/devcontainer      2026-06-19 15:13:11
@@ -6,15 +6,22 @@

 # Resolve the installation directory
 # Handle both direct execution and symlinked scenarios
-if [ -L "$0" ]; then
-    # Follow symlink
-    SCRIPT_PATH="$(readlink "$0" 2>/dev/null || readlink -f "$0" 2>/dev/null || echo "$0")"
-else
-    SCRIPT_PATH="$0"
-fi
-
-# Get absolute path to script directory
-SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
+SCRIPT_DIR="$(
+    CDPATH=
+    self=$0
+
+    while [ -L "$self" ]; do
+        cd -- "${self%/*}" >/dev/null || :
+        self=$(readlink "${self##*/}")
+    done
+
+    case "$self" in
+        (*/*) ;;
+        (*) self=./$self ;;
+    esac
+
+    cd -P -- "${self%/*}" >/dev/null && pwd
+)"
 INSTALL_DIR="$(dirname "$SCRIPT_DIR")"

 # Paths to bundled components

(Reference/explanation: https://stackoverflow.com/a/246128/1968)

@V-Subhankar-infy
V-Subhankar-infy force-pushed the main branch 3 times, most recently from d34e588 to e612da1 Compare June 22, 2026 10:20
@V-Subhankar-infy
V-Subhankar-infy marked this pull request as ready for review July 13, 2026 10:49
@abdurriq
abdurriq requested a review from Copilot July 28, 2026 10:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the generated devcontainer wrapper’s self-location logic so that relative symlink targets are resolved relative to the symlink’s directory (not the caller’s CWD), and adds a regression test covering the reported split-local-layout scenario.

Changes:

  • Replace the wrapper’s single-hop readlink/readlink -f resolution with a hop-by-hop symlink-chain resolver.
  • Canonicalize the resolved script directory before deriving INSTALL_DIR.
  • Add an integration regression test for invoking a relative symlinked wrapper from a different working directory.
Show a summary per file
File Description
scripts/install.sh Reworks wrapper script-location resolution to correctly handle relative symlink chains without readlink -f.
scripts/install.test.sh Adds a regression test for relative symlink invocation from a different directory.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comments suppressed due to low confidence (1)

scripts/install.sh:444

  • cd -P -- and dirname -- are not portable; -- is not specified by POSIX for cd or dirname and can be interpreted as a literal path/operand on some systems. Since SCRIPT_DIR is already an absolute path, dropping -- is safe and avoids breaking the wrapper on macOS/BSD and dash-based /bin/sh.
    cd -P -- "${self%/*}" >/dev/null && pwd
)"
INSTALL_DIR="$(dirname -- "$SCRIPT_DIR")"
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment thread scripts/install.sh
Comment thread scripts/install.sh Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Comments suppressed due to low confidence (2)

scripts/install.sh:443

  • cd in POSIX sh does not specify support for a -- end-of-options marker. Using cd -P -- ... can be interpreted as trying to cd into a directory literally named -- on some shells, which would make SCRIPT_DIR resolution fail (and the wrapper exit due to set -e).
    cd -P -- "${self%/*}" >/dev/null && pwd

scripts/install.sh:445

  • dirname is specified without options in POSIX, so dirname -- "$SCRIPT_DIR" is not portable: on systems where dirname treats -- as a path operand (common on BSD/macOS), this can emit an extra line and corrupt INSTALL_DIR. Since SCRIPT_DIR comes from pwd, it won't start with - anyway, so -- is unnecessary.
INSTALL_DIR="$(dirname -- "$SCRIPT_DIR")"
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Low

Comment thread scripts/install.sh
# Get absolute path to script directory
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
INSTALL_DIR="$(dirname "$SCRIPT_DIR")"
cd -P -- "${self%/*}" >/dev/null && pwd

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think cd -- would work on POSIX/macOS/BSD environments.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cd -- (option terminator) has been part of systems since 1995 and I have just checked on zsh shell in my macos and it supports this.

To be precise, this is completely safe to use cd -- on any system from 2005 onward:

  • macOS 10.0+ (2001+)
  • FreeBSD 6.0+ (2005+)
  • OpenBSD 5.0+ (2008+)
  • NetBSD 1.5+ (1999+)

I am not aware of how much backward compatibility do we need to have, but it seems like a valid decision, considering readlink even the basic version is relatively more recent.

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.

install.sh wrapper relative link not resolved correctly

4 participants