summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBartek Iwańczuk <biwanczuk@gmail.com>2019-06-14 17:43:06 +0200
committerRyan Dahl <ry@tinyclouds.org>2019-06-14 08:43:06 -0700
commitd00a4beec40faa9d2ddb482ed152f00c36ae2f42 (patch)
tree39609a02ba29741d01eb1657a5871837b3ecbf54
parent926594c53c600fd65f210806d7d6d841b02c3385 (diff)
feat: installer (denoland/deno_std#489)
Original: https://github.com/denoland/deno_std/commit/a3015be14195df46486a49e5c791afba4dfe084a
-rw-r--r--.ci/template.common.yml2
-rw-r--r--installer/README.md70
-rw-r--r--installer/mod.ts270
-rw-r--r--installer/test.ts122
-rwxr-xr-xtest.ts1
5 files changed, 464 insertions, 1 deletions
diff --git a/.ci/template.common.yml b/.ci/template.common.yml
index 2d750c27b..76c98c639 100644
--- a/.ci/template.common.yml
+++ b/.ci/template.common.yml
@@ -3,4 +3,4 @@ parameters:
steps:
- bash: deno${{ parameters.exe_suffix }} run --allow-run --allow-write --allow-read format.ts --check
- - bash: deno${{ parameters.exe_suffix }} run --allow-run --allow-net --allow-write --allow-read --config=tsconfig.test.json test.ts \ No newline at end of file
+ - bash: deno${{ parameters.exe_suffix }} run --allow-run --allow-net --allow-write --allow-read --allow-env --config=tsconfig.test.json test.ts \ No newline at end of file
diff --git a/installer/README.md b/installer/README.md
new file mode 100644
index 000000000..309364fbc
--- /dev/null
+++ b/installer/README.md
@@ -0,0 +1,70 @@
+# deno_installer
+
+Install remote or local script as executables.
+
+````
+## Installation
+
+`installer` can be install using iteself:
+
+```sh
+deno -A https://deno.land/std/installer/mod.ts deno_installer https://deno.land/std/installer/mod.ts -A
+````
+
+Installer uses `~/.deno/bin` to store installed scripts so make sure it's in `$PATH`
+
+```
+echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc # change this to your shell
+```
+
+## Usage
+
+Install script
+
+```sh
+$ deno_installer file_server https://deno.land/std/http/file_server.ts --allow-net --allow-read
+> Downloading: https://deno.land/std/http/file_server.ts
+>
+> ✅ Successfully installed file_server.
+
+# local script
+$ deno_installer file_server ./deno_std/http/file_server.ts --allow-net --allow-read
+> Looking for: /dev/deno_std/http/file_server.ts
+>
+> ✅ Successfully installed file_server.
+```
+
+Use installed script:
+
+```sh
+$ file_server
+HTTP server listening on http://0.0.0.0:4500/
+```
+
+Update installed script
+
+```sh
+$ deno_installer file_server https://deno.land/std/http/file_server.ts --allow-net --allow-read
+> ⚠️ file_server is already installed, do you want to overwrite it? [yN]
+> y
+>
+> Downloading: https://deno.land/std/http/file_server.ts
+>
+> ✅ Successfully installed file_server.
+```
+
+Show help
+
+```sh
+$ deno_installer --help
+> deno installer
+ Install remote or local script as executables.
+
+USAGE:
+ deno https://deno.land/std/installer/mod.ts EXE_NAME SCRIPT_URL [FLAGS...]
+
+ARGS:
+ EXE_NAME Name for executable
+ SCRIPT_URL Local or remote URL of script to install
+ [FLAGS...] List of flags for script, both Deno permission and script specific flag can be used.
+```
diff --git a/installer/mod.ts b/installer/mod.ts
new file mode 100644
index 000000000..aff01f361
--- /dev/null
+++ b/installer/mod.ts
@@ -0,0 +1,270 @@
+#!/usr/bin/env deno --allow-all
+
+const {
+ args,
+ env,
+ readDirSync,
+ mkdirSync,
+ writeFile,
+ exit,
+ stdin,
+ stat,
+ readAll,
+ run,
+ remove
+} = Deno;
+import * as path from "../fs/path.ts";
+
+const encoder = new TextEncoder();
+const decoder = new TextDecoder("utf-8");
+
+enum Permission {
+ Read,
+ Write,
+ Net,
+ Env,
+ Run,
+ All
+}
+
+function getPermissionFromFlag(flag: string): Permission | undefined {
+ switch (flag) {
+ case "--allow-read":
+ return Permission.Read;
+ case "--allow-write":
+ return Permission.Write;
+ case "--allow-net":
+ return Permission.Net;
+ case "--allow-env":
+ return Permission.Env;
+ case "--allow-run":
+ return Permission.Run;
+ case "--allow-all":
+ return Permission.All;
+ case "-A":
+ return Permission.All;
+ }
+}
+
+function getFlagFromPermission(perm: Permission): string {
+ switch (perm) {
+ case Permission.Read:
+ return "--allow-read";
+ case Permission.Write:
+ return "--allow-write";
+ case Permission.Net:
+ return "--allow-net";
+ case Permission.Env:
+ return "--allow-env";
+ case Permission.Run:
+ return "--allow-run";
+ case Permission.All:
+ return "--allow-all";
+ }
+ return "";
+}
+
+async function readCharacter(): Promise<string> {
+ const byteArray = new Uint8Array(1024);
+ await stdin.read(byteArray);
+ const line = decoder.decode(byteArray);
+ return line[0];
+}
+
+async function yesNoPrompt(message: string): Promise<boolean> {
+ console.log(`${message} [yN]`);
+ const input = await readCharacter();
+ console.log();
+ return input === "y" || input === "Y";
+}
+
+function createDirIfNotExists(path: string): void {
+ try {
+ readDirSync(path);
+ } catch (e) {
+ mkdirSync(path, true);
+ }
+}
+
+function checkIfExistsInPath(path: string): boolean {
+ const { PATH } = env();
+
+ const paths = (PATH as string).split(":");
+
+ return paths.includes(path);
+}
+
+function getInstallerDir(): string {
+ const { HOME } = env();
+
+ if (!HOME) {
+ throw new Error("$HOME is not defined.");
+ }
+
+ return path.join(HOME, ".deno", "bin");
+}
+
+// TODO: fetch doesn't handle redirects yet - once it does this function
+// can be removed
+async function fetchWithRedirects(
+ url: string,
+ redirectLimit: number = 10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+): Promise<any> {
+ // TODO: `Response` is not exposed in global so 'any'
+ const response = await fetch(url);
+
+ if (response.status === 301 || response.status === 302) {
+ if (redirectLimit > 0) {
+ const redirectUrl = response.headers.get("location")!;
+ return await fetchWithRedirects(redirectUrl, redirectLimit - 1);
+ }
+ }
+
+ return response;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+async function fetchModule(url: string): Promise<any> {
+ const response = await fetchWithRedirects(url);
+
+ if (response.status !== 200) {
+ // TODO: show more debug information like status and maybe body
+ throw new Error(`Failed to get remote script ${url}.`);
+ }
+
+ const body = await readAll(response.body);
+ return decoder.decode(body);
+}
+
+function showHelp(): void {
+ console.log(`deno installer
+ Install remote or local script as executables.
+
+USAGE:
+ deno https://deno.land/std/installer/mod.ts EXE_NAME SCRIPT_URL [FLAGS...]
+
+ARGS:
+ EXE_NAME Name for executable
+ SCRIPT_URL Local or remote URL of script to install
+ [FLAGS...] List of flags for script, both Deno permission and script specific flag can be used.
+ `);
+}
+
+export async function install(
+ moduleName: string,
+ moduleUrl: string,
+ flags: string[]
+): Promise<void> {
+ const installerDir = getInstallerDir();
+ createDirIfNotExists(installerDir);
+
+ const FILE_PATH = path.join(installerDir, moduleName);
+
+ let fileInfo;
+ try {
+ fileInfo = await stat(FILE_PATH);
+ } catch (e) {
+ // pass
+ }
+
+ if (fileInfo) {
+ const msg = `⚠️ ${moduleName} is already installed, do you want to overwrite it?`;
+ if (!(await yesNoPrompt(msg))) {
+ return;
+ }
+ }
+
+ // ensure script that is being installed exists
+ if (moduleUrl.startsWith("http")) {
+ // remote module
+ console.log(`Downloading: ${moduleUrl}\n`);
+ await fetchModule(moduleUrl);
+ } else {
+ // assume that it's local file
+ moduleUrl = path.resolve(moduleUrl);
+ console.log(`Looking for: ${moduleUrl}\n`);
+ await stat(moduleUrl);
+ }
+
+ const grantedPermissions: Permission[] = [];
+ const scriptArgs: string[] = [];
+
+ for (const flag of flags) {
+ const permission = getPermissionFromFlag(flag);
+ if (permission === undefined) {
+ scriptArgs.push(flag);
+ } else {
+ grantedPermissions.push(permission);
+ }
+ }
+
+ const commands = [
+ "deno",
+ ...grantedPermissions.map(getFlagFromPermission),
+ moduleUrl,
+ ...scriptArgs,
+ "$@"
+ ];
+
+ // TODO: add windows Version
+ const template = `#/bin/sh\n${commands.join(" ")}`;
+ await writeFile(FILE_PATH, encoder.encode(template));
+
+ const makeExecutable = run({ args: ["chmod", "+x", FILE_PATH] });
+ const { code } = await makeExecutable.status();
+ makeExecutable.close();
+
+ if (code !== 0) {
+ throw new Error("Failed to make file executable");
+ }
+
+ console.log(`✅ Successfully installed ${moduleName}.`);
+ // TODO: add Windows version
+ if (!checkIfExistsInPath(installerDir)) {
+ console.log("\nℹ️ Add ~/.deno/bin to PATH");
+ console.log(
+ " echo 'export PATH=\"$HOME/.deno/bin:$PATH\"' >> ~/.bashrc # change this to your shell"
+ );
+ }
+}
+
+export async function uninstall(moduleName: string): Promise<void> {
+ const installerDir = getInstallerDir();
+ const FILE_PATH = path.join(installerDir, moduleName);
+
+ try {
+ await stat(FILE_PATH);
+ } catch (e) {
+ if (e instanceof Deno.DenoError && e.kind === Deno.ErrorKind.NotFound) {
+ throw new Error(`ℹ️ ${moduleName} not found`);
+ }
+ }
+
+ await remove(FILE_PATH);
+ console.log(`ℹ️ Uninstalled ${moduleName}`);
+}
+
+async function main(): Promise<void> {
+ if (args.length < 3) {
+ return showHelp();
+ }
+
+ if (["-h", "--help"].includes(args[1])) {
+ return showHelp();
+ }
+
+ const moduleName = args[1];
+ const moduleUrl = args[2];
+ const flags = args.slice(3);
+ try {
+ await install(moduleName, moduleUrl, flags);
+ } catch (e) {
+ console.log(e);
+ exit(1);
+ }
+}
+
+if (import.meta.main) {
+ main();
+}
diff --git a/installer/test.ts b/installer/test.ts
new file mode 100644
index 000000000..1b1aa4200
--- /dev/null
+++ b/installer/test.ts
@@ -0,0 +1,122 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+const { readFile, run, stat, makeTempDir, remove, env } = Deno;
+
+import { test, runIfMain, TestFunction } from "../testing/mod.ts";
+import { assert, assertEquals, assertThrowsAsync } from "../testing/asserts.ts";
+import { BufReader, EOF } from "../io/bufio.ts";
+import { TextProtoReader } from "../textproto/mod.ts";
+import { install, uninstall } from "./mod.ts";
+import * as path from "../fs/path.ts";
+
+let fileServer: Deno.Process;
+
+// copied from `http/file_server_test.ts`
+async function startFileServer(): Promise<void> {
+ fileServer = run({
+ args: [
+ "deno",
+ "run",
+ "--allow-read",
+ "--allow-net",
+ "http/file_server.ts",
+ ".",
+ "--cors"
+ ],
+ stdout: "piped"
+ });
+ // Once fileServer is ready it will write to its stdout.
+ const r = new TextProtoReader(new BufReader(fileServer.stdout!));
+ const s = await r.readLine();
+ assert(s !== EOF && s.includes("server listening"));
+}
+
+function killFileServer(): void {
+ fileServer.close();
+ fileServer.stdout!.close();
+}
+
+function installerTest(t: TestFunction): void {
+ const fn = async (): Promise<void> => {
+ await startFileServer();
+ const tempDir = await makeTempDir();
+ const envVars = env();
+ const originalHomeDir = envVars["HOME"];
+ envVars["HOME"] = tempDir;
+
+ try {
+ await t();
+ } finally {
+ killFileServer();
+ await remove(tempDir, { recursive: true });
+ envVars["HOME"] = originalHomeDir;
+ }
+ };
+
+ test(fn);
+}
+
+installerTest(async function installBasic(): Promise<void> {
+ await install("file_srv", "http://localhost:4500/http/file_server.ts", []);
+
+ const { HOME } = env();
+ const filePath = path.resolve(HOME, ".deno/bin/file_srv");
+ const fileInfo = await stat(filePath);
+ assert(fileInfo.isFile());
+
+ const fileBytes = await readFile(filePath);
+ const fileContents = new TextDecoder().decode(fileBytes);
+ assertEquals(
+ fileContents,
+ "#/bin/sh\ndeno http://localhost:4500/http/file_server.ts $@"
+ );
+});
+
+installerTest(async function installWithFlags(): Promise<void> {
+ await install("file_server", "http://localhost:4500/http/file_server.ts", [
+ "--allow-net",
+ "--allow-read",
+ "--foobar"
+ ]);
+
+ const { HOME } = env();
+ const filePath = path.resolve(HOME, ".deno/bin/file_server");
+
+ const fileBytes = await readFile(filePath);
+ const fileContents = new TextDecoder().decode(fileBytes);
+ assertEquals(
+ fileContents,
+ "#/bin/sh\ndeno --allow-net --allow-read http://localhost:4500/http/file_server.ts --foobar $@"
+ );
+});
+
+installerTest(async function uninstallBasic(): Promise<void> {
+ await install("file_server", "http://localhost:4500/http/file_server.ts", []);
+
+ const { HOME } = env();
+ const filePath = path.resolve(HOME, ".deno/bin/file_server");
+
+ await uninstall("file_server");
+
+ let thrown = false;
+ try {
+ await stat(filePath);
+ } catch (e) {
+ thrown = true;
+ assert(e instanceof Deno.DenoError);
+ assertEquals(e.kind, Deno.ErrorKind.NotFound);
+ }
+
+ assert(thrown);
+});
+
+installerTest(async function uninstallNonExistentModule(): Promise<void> {
+ await assertThrowsAsync(
+ async (): Promise<void> => {
+ await uninstall("file_server");
+ },
+ Error,
+ "file_server not found"
+ );
+});
+
+runIfMain(import.meta);
diff --git a/test.ts b/test.ts
index 864f1b511..c1381cb27 100755
--- a/test.ts
+++ b/test.ts
@@ -11,6 +11,7 @@ import "./flags/test.ts";
import "./fs/test.ts";
import "./http/test.ts";
import "./io/test.ts";
+import "./installer/test.ts";
import "./log/test.ts";
import "./media_types/test.ts";
import "./mime/test.ts";