Skip to content

Commit

Permalink
feat: Add flags to skip prompts and avoid modifying PATH (#303)
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanwhit authored Oct 1, 2024
1 parent 3db6f16 commit 26c905a
Show file tree
Hide file tree
Showing 11 changed files with 726 additions and 41 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
* text=auto
*.sh text eol=lf
shell-setup/bundled.esm.js -diff
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ jobs:
shell: bash
run: |
if ! shasum -a 256 -c SHA256SUM; then
echo 'Checksum verification failed'
echo 'Checksum verification failed.'
echo 'If the installer has been updated intentionally, update the checksum with:'
echo 'shasum -a 256 install.{sh,ps1} > SHA256SUM'
exit 1
fi
- name: tests shell
Expand Down Expand Up @@ -67,3 +69,6 @@ jobs:
echo 'Bundled script is out of date, update it with `cd shell-setup; deno task bundle`'.
exit 1
fi
- name: integration tests
if: matrix.os != 'windows-latest'
run: deno test -A --permit-no-files
2 changes: 1 addition & 1 deletion SHA256SUM
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
e0497676dfe693ba9a69e808ac308f6fdaffb3979eecc2c69fd9d24e84df9cee install.sh
9f3677714f300baa745dfd6078d2a95f0d77d19ea0a67b44b93c804e183e55f0 install.sh
0e7618d4055b21fe1fe7915ebbe27814f2f6f178cb739d1cc2a0d729da1c9d58 install.ps1
8 changes: 6 additions & 2 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@
]
}
},
"lock": "./shell-setup/deno.lock",
"tasks": {
"bundle": "cd shell-setup && deno task bundle"
},
"lock": { "path": "./shell-setup/deno.lock", "frozen": true },
"fmt": {
"exclude": [
"./shell-setup/bundled.esm.js"
"./shell-setup/bundled.esm.js",
".github"
]
}
}
46 changes: 39 additions & 7 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,42 @@ else
esac
fi

if [ $# -eq 0 ]; then
print_help_and_exit() {
echo "Setup script for installing deno
Options:
-y, --yes
Skip interactive prompts and accept defaults
--no-modify-path
Don't add deno to the PATH environment variable
-h, --help
Print help
"
echo "Note: Deno was not installed"
exit 0
}

# Simple arg parsing - look for help flag, otherwise
# ignore args starting with '-' and take the first
# positional arg as the deno version to install
for arg in "$@"; do
case "$arg" in
"-h")
print_help_and_exit
;;
"--help")
print_help_and_exit
;;
"-"*) ;;
*)
if [ -z "$deno_version" ]; then
deno_version="$arg"
fi
;;
esac
done
if [ -z "$deno_version" ]; then
deno_version="$(curl -s https://dl.deno.land/release-latest.txt)"
else
deno_version=$1
fi

deno_uri="https://dl.deno.land/release/${deno_version}/deno-${target}.zip"
Expand All @@ -47,17 +79,17 @@ rm "$exe.zip"
echo "Deno was installed successfully to $exe"

run_shell_setup() {
$exe run -A --reload jsr:@deno/installer-shell-setup/bundled "$deno_install"

$exe run -A --reload jsr:@deno/installer-shell-setup/bundled "$deno_install" "$@"
}

# If stdout is a terminal, see if we can run shell setup script (which includes interactive prompts)
if [ -z "$CI" ] && [ -t 1 ] && $exe eval 'const [major, minor] = Deno.version.deno.split("."); if (major < 2 && minor < 42) Deno.exit(1)'; then
if [ -t 0 ]; then
run_shell_setup
run_shell_setup "$@"
else
# This script is probably running piped into sh, so we don't have direct access to stdin.
# Instead, explicitly connect /dev/tty to stdin
run_shell_setup </dev/tty
run_shell_setup "$@" </dev/tty
fi
fi
if command -v deno >/dev/null; then
Expand Down
165 changes: 165 additions & 0 deletions install_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import $, { Path } from "jsr:@david/dax";
import { Pty } from "jsr:@sigma/pty-ffi";
import { assert, assertEquals, assertStringIncludes } from "jsr:@std/assert";

Deno.test(
{ name: "install skip prompts", ignore: Deno.build.os === "windows" },
async () => {
await using testEnv = await TestEnv.setup();
const { env, tempDir, installScript, installDir } = testEnv;
await testEnv.homeDir.join(".bashrc").ensureFile();

console.log("installscript contents", await installScript.readText());

const shellOutput = await runInBash(
[`cat "${installScript.toString()}" | sh -s -- -y v2.0.0-rc.6`],
{ env, cwd: tempDir },
);
console.log(shellOutput);

assertStringIncludes(shellOutput, "Deno was added to the PATH");

const deno = installDir.join("bin/deno");
assert(await deno.exists());

// Check that it's on the PATH now, and that it's the correct version.
const output = await new Deno.Command("bash", {
args: ["-i", "-c", "deno --version"],
env,
}).output();
const stdout = new TextDecoder().decode(output.stdout).trim();

const versionRe = /deno (\d+\.\d+\.\d+\S*)/;
const match = stdout.match(versionRe);

assert(match !== null);
assertEquals(match[1], "2.0.0-rc.6");
},
);

Deno.test(
{ name: "install no modify path", ignore: Deno.build.os === "windows" },
async () => {
await using testEnv = await TestEnv.setup();
const { env, tempDir, installScript, installDir } = testEnv;
await testEnv.homeDir.join(".bashrc").ensureFile();

const shellOutput = await runInBash(
[`cat "${installScript.toString()}" | sh -s -- -y v2.0.0-rc.6 --no-modify-path`],
{ env, cwd: tempDir },
);

assert(
!shellOutput.includes("Deno was added to the PATH"),
`Unexpected output, shouldn't have added to the PATH:\n${shellOutput}`,
);

const deno = installDir.join("bin/deno");
assert(await deno.exists());
},
);

class TestEnv implements AsyncDisposable, Disposable {
#tempDir: Path;
private constructor(
tempDir: Path,
public homeDir: Path,
public installDir: Path,
public installScript: Path,
public env: Record<string, string>,
) {
this.#tempDir = tempDir;
}
get tempDir() {
return this.#tempDir;
}
static async setup({ env = {} }: { env?: Record<string, string> } = {}) {
const tempDir = $.path(await Deno.makeTempDir());
const homeDir = await tempDir.join("home").ensureDir();
const installDir = tempDir.join(".deno");

const tempSetup = tempDir.join("shell-setup.js");
await $.path(resolve("./shell-setup/bundled.esm.js")).copyFile(tempSetup);

// Copy the install script to a temp location, and modify it to
// run the shell setup script from the local source instead of JSR.
const contents = await Deno.readTextFile(resolve("./install.sh"));
const contentsLocal = contents.replaceAll(
"jsr:@deno/installer-shell-setup/bundled",
tempSetup.toString(),
);
if (contents === contentsLocal) {
throw new Error("Failed to point installer at local source");
}
const installScript = tempDir.join("install.sh");
await installScript.writeText(contentsLocal);

await Deno.chmod(installScript.toString(), 0o755);

// Ensure that the necessary binaries are in the PATH.
// It's not perfect, but the idea is to keep the test environment
// as clean as possible to make it less host dependent.
const needed = ["bash", "unzip", "cat", "sh"];
const binPaths = await Promise.all(needed.map((n) => $.which(n)));
const searchPaths = new Set(
binPaths.map((p, i) => {
if (p === undefined) {
throw new Error(`missing dependency: ${needed[i]}`);
}
return $.path(p).parentOrThrow().toString();
}),
);
const newEnv = {
HOME: homeDir.toString(),
XDG_CONFIG_HOME: homeDir.toString(),
DENO_INSTALL: installDir.toString(),
PATH: searchPaths.values().toArray().join(":"),
ZDOTDIR: homeDir.toString(),
SHELL: "/bin/bash",
CI: "",
};
Object.assign(newEnv, env);
return new TestEnv(tempDir, homeDir, installDir, installScript, newEnv);
}
async [Symbol.asyncDispose]() {
await this.#tempDir.remove({ recursive: true });
}
[Symbol.dispose]() {
this.#tempDir.removeSync({ recursive: true });
}
}

async function runInBash(
commands: string[],
options: { cwd?: Path; env: Record<string, string> },
): Promise<string> {
const { cwd, env } = options;
const bash = await $.which("bash") ?? "bash";
const pty = new Pty({
env: Object.entries(env),
cmd: bash,
args: [],
});
if (cwd) {
await pty.write(`cd "${cwd.toString()}"\n`);
}

for (const command of commands) {
await pty.write(command + "\n");
}
await pty.write("exit\n");
let output = "";
while (true) {
const { data, done } = await pty.read();
output += data;
if (done) {
break;
}
}
pty.close();
return output;
}

function resolve(s: string): URL {
return new URL(import.meta.resolve(s));
}
4 changes: 3 additions & 1 deletion shell-setup/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const result = await esbuild.build({
format: "esm",
});

console.log(result.outputFiles);
if (result.errors.length || result.warnings.length) {
console.error(`Errors: ${result.errors}, warnings: ${result.warnings}`);
}

await esbuild.stop();
Loading

0 comments on commit 26c905a

Please sign in to comment.