Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add flags to skip prompts and avoid modifying PATH #303

Merged
merged 8 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

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

This is great, nice work 👍

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
Loading