Skip to content

Commit d52fdaa

Browse files
authored
Fix WSL shell startup by preserving LF in Windows packages (#1253)
2 parents 55cdf11 + ca0a28e commit d52fdaa

4 files changed

Lines changed: 61 additions & 1 deletion

File tree

‎.gitattributes‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
# The POSIX `dor` launcher (no extension) must stay LF.
1111
dor/bin/dor text eol=lf
1212

13+
# Windows bundles also supply these scripts to WSL and Git Bash.
14+
# CRLF breaks parsing before the login profile can restore the user's PATH.
15+
standalone/sidecar/shell-integration/bash/** text eol=lf
16+
standalone/sidecar/shell-integration/zsh/** text eol=lf
17+
1318
# Markdown merges sentence by sentence once `pnpm setup:git` defines this
1419
# driver (scripts/md-merge.mjs); until then Git merges these files by line.
1520
*.md merge=md-sentences

‎docs/specs/terminal-state.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ Source of truth: `TerminalProtocolParser` / `commandLineEvents` in `lib/src/lib/
7979

8080
**bash's `E` is the submitted line, read back from history only when the last entry provably is it, else its first simple command** (`$BASH_COMMAND`) (rationale).
8181

82-
**Both distributions ship the scripts**: standalone per `docs/specs/standalone.md` -> "Build and development", the VS Code build into `dist/shell-integration`, which the host names in `DORMOUSE_SHELL_INTEGRATION_DIR`.
82+
**Both distributions ship scripts**: standalone via sidecar resources (`docs/specs/standalone.md` -> "Build and development"), VS Code in `dist/shell-integration`, located through `DORMOUSE_SHELL_INTEGRATION_DIR`.
83+
84+
**Must ship bash/zsh scripts as LF, including Windows packages** (rationale).
8385

8486
**Emitted fields must be filtered before they are written — a security boundary.** An attacker-chosen directory name or command can carry an OSC terminator (BEL, `ESC \`, or the C1 ST `U+009C`) that ends the `633` sequence early, so the remainder arrives as a fresh, fully-trusted OSC. **The parser cannot defend against this** — it scans raw bytes (rationale). The field grammar the parser decodes:
8587

‎docs/specs/terminal-state.rationale.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Native path payloads are not URLs: decoding `%20` or trimming edge spaces change
1414

1515
## Shell-integration injection
1616

17+
**Why Unix integration scripts stay LF in Windows builds.** A Windows checkout with `core.autocrlf=true` converted the bundled Bash script to CRLF; WSL Bash failed on `do\r` before sourcing the user profile, so commands installed through that profile disappeared from PATH (observed on WSL2, 2026-10). `.gitattributes` keeps the packaged source valid for Unix shells.
18+
1719
**Why the mechanism cannot be uniform.** One env var guarantees a `PATH` binary is *found*, but no shell has an env var for *run our hook code on every prompt* — hence a per-shell mechanism, and hence the Channel column: an env-var channel is as reliable as the `PATH` prepend, while a `shellArgs` channel only fires for the launch shapes Dormouse recognizes.
1820

1921
**Why bash injection keys on the launch args.** `--init-file` and login mode are mutually exclusive, so the script has to replace login-profile sourcing itself, which is only safe when the launch was a plain interactive/login shell — Git Bash's `--login -i` is why login flags stay in the allowed set, while anything with a specific `-c <cmd>` is a job, not a session.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Exercise the bytes a Windows checkout packages, even when CI runs on Linux.
2+
const test = require('node:test');
3+
const assert = require('node:assert/strict');
4+
const { execFileSync, spawnSync } = require('node:child_process');
5+
const { cpSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } = require('node:fs');
6+
const os = require('node:os');
7+
const path = require('node:path');
8+
9+
test('Windows checkout preserves executable Unix shell integration and login-profile startup', (t) => {
10+
const root = path.resolve(__dirname, '../..');
11+
const fixture = mkdtempSync(path.join(os.tmpdir(), 'dormouse-shell-checkout-'));
12+
t.after(() => rmSync(fixture, { recursive: true, force: true }));
13+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('GIT_')));
14+
env.GIT_CONFIG_GLOBAL = os.devNull;
15+
env.GIT_CONFIG_NOSYSTEM = '1';
16+
const git = (...args) => execFileSync('git', [
17+
'-c', 'core.autocrlf=true', '-c', 'core.safecrlf=false',
18+
'-c', `core.attributesFile=${os.devNull}`, ...args,
19+
], { cwd: fixture, env, stdio: 'pipe' });
20+
const relative = 'standalone/sidecar/shell-integration';
21+
cpSync(path.join(root, '.gitattributes'), path.join(fixture, '.gitattributes'));
22+
cpSync(path.join(root, relative), path.join(fixture, relative), { recursive: true });
23+
git('init', '-q');
24+
git('add', '.gitattributes', relative);
25+
const checkout = path.join(fixture, 'Windows checkout');
26+
mkdirSync(checkout);
27+
git('checkout-index', '--all', `--prefix=${checkout}${path.sep}`);
28+
29+
for (const shell of ['bash', 'zsh']) {
30+
const directory = path.join(checkout, relative, shell);
31+
for (const file of readdirSync(directory).filter((name) => name !== '.gitignore')) {
32+
assert.ok(!readFileSync(path.join(directory, file)).includes(Buffer.from('\r\n')),
33+
`${shell}/${file} must remain LF in a Windows checkout`);
34+
}
35+
}
36+
37+
const home = path.join(fixture, 'home');
38+
mkdirSync(home);
39+
writeFileSync(path.join(home, '.bash_profile'), 'export DORMOUSE_TEST_PROFILE=loaded\n');
40+
const result = spawnSync('bash', [
41+
'--init-file', path.join(checkout, relative, 'bash/shellIntegration.bash'), '-ic',
42+
'test "$DORMOUSE_TEST_PROFILE" = loaded && test "$__dormouse_633_installed" = 1',
43+
], {
44+
cwd: home,
45+
env: { PATH: process.env.PATH, HOME: home, TERM: 'xterm' },
46+
encoding: 'utf8',
47+
timeout: 10000,
48+
});
49+
assert.ifError(result.error);
50+
assert.equal(result.status, 0, result.stderr);
51+
});

0 commit comments

Comments
 (0)