diff --git a/installer/mod.ts b/installer/mod.ts index ba98074f324b..5d907bef528b 100644 --- a/installer/mod.ts +++ b/installer/mod.ts @@ -1,5 +1,5 @@ #!/usr/bin/env deno --allow-all - +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. const { args, env, @@ -8,15 +8,17 @@ const { writeFile, exit, stdin, - stat, - readAll, - run, - remove + chmod, + remove, + run } = Deno; import * as path from "../fs/path.ts"; +import { exists } from "../fs/exists.ts"; const encoder = new TextEncoder(); const decoder = new TextDecoder("utf-8"); +const isWindows = Deno.platform.os === "win"; +const driverLetterReg = /^[c-z]:/i; // Regular expression to test disk driver letter. eg "C:\\User\username\path\to" enum Permission { Read, @@ -86,69 +88,103 @@ function createDirIfNotExists(path: string): void { } } -function checkIfExistsInPath(path: string): boolean { - const { PATH } = env(); +function checkIfExistsInPath(filePath: string): boolean { + // In Windows's Powershell $PATH not exist, so use $Path instead. + // $HOMEDRIVE is only used on Windows. + const { PATH, Path, HOMEDRIVE } = env(); - const paths = (PATH as string).split(":"); + let envPath = (PATH as string) || (Path as string) || ""; - return paths.includes(path); -} + const paths = envPath.split(isWindows ? ";" : ":"); -function getInstallerDir(): string { - const { HOME } = env(); + let fileAbsolutePath = filePath; - 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 { - // 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); + for (const p of paths) { + const pathInEnv = path.normalize(p); + // On Windows paths from env contain drive letter. (eg. C:\Users\username\.deno\bin) + // But in the path of Deno, there is no drive letter. (eg \Users\username\.deno\bin) + if (isWindows) { + if (driverLetterReg.test(pathInEnv)) { + fileAbsolutePath = HOMEDRIVE + "\\" + fileAbsolutePath; + } + } + if (pathInEnv === fileAbsolutePath) { + return true; } + fileAbsolutePath = filePath; } - return response; + return false; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -async function fetchModule(url: string): Promise { - const response = await fetchWithRedirects(url); +function getInstallerDir(): string { + // In Windows's Powershell $HOME environmental variable maybe null, if so use $HOMEPATH instead. + let { HOME, HOMEPATH } = env(); + + const HOME_PATH = HOME || HOMEPATH; - if (response.status !== 200) { - // TODO: show more debug information like status and maybe body - throw new Error(`Failed to get remote script ${url}.`); + if (!HOME_PATH) { + throw new Error("$HOME is not defined."); } - const body = await readAll(response.body); - return decoder.decode(body); + return path.join(HOME_PATH, ".deno", "bin"); } 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...] + deno https://deno.land/std/installer/mod.ts EXE_NAME SCRIPT_URL [FLAGS...] ARGS: - EXE_NAME Name for executable + 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. - `); +`); +} + +async function generateExecutable( + filePath: string, + commands: string[] +): Promise { + // On Windows if user is using Powershell .cmd extension is need to run the installed module. + // Generate batch script to satisfy that. + if (isWindows) { + const template = `% This executable is generated by Deno. Please don't modify it unless you know what it means. % +@IF EXIST "%~dp0\deno.exe" ( + "%~dp0\deno.exe" ${commands.slice(1).join(" ")} %* +) ELSE ( + @SETLOCAL + @SET PATHEXT=%PATHEXT:;.TS;=;% + ${commands.join(" ")} %* +) +`; + const cmdFile = filePath + ".cmd"; + await writeFile(cmdFile, encoder.encode(template)); + await chmod(cmdFile, 0o755); + } + + // generate Shell script + const template = `#/bin/sh +# This executable is generated by Deno. Please don't modify it unless you know what it means. +basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") + +case \`uname\` in + *CYGWIN*) basedir=\`cygpath -w "$basedir"\`;; +esac + +if [ -x "$basedir/deno" ]; then + "$basedir/deno" ${commands.slice(1).join(" ")} "$@" + ret=$? +else + ${commands.join(" ")} "$@" + ret=$? +fi +exit $ret +`; + await writeFile(filePath, encoder.encode(template)); + await chmod(filePath, 0o755); } export async function install( @@ -161,14 +197,7 @@ export async function install( const filePath = path.join(installerDir, moduleName); - let fileInfo; - try { - fileInfo = await stat(filePath); - } catch (e) { - // pass - } - - if (fileInfo) { + if (await exists(filePath)) { const msg = `⚠️ ${moduleName} is already installed, do you want to overwrite it?`; if (!(await yesNoPrompt(msg))) { return; @@ -176,15 +205,16 @@ export async function install( } // 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 ps = run({ + args: ["deno", "fetch", moduleUrl], + stdout: "inherit", + stderr: "inherit" + }); + + const { code } = await ps.status(); + + if (code !== 0) { + throw new Error("Failed to fetch module."); } const grantedPermissions: Permission[] = []; @@ -201,28 +231,17 @@ export async function install( const commands = [ "deno", + "run", ...grantedPermissions.map(getFlagFromPermission), moduleUrl, - ...scriptArgs, - "$@" + ...scriptArgs ]; - // TODO: add windows Version - const template = `#/bin/sh\n${commands.join(" ")}`; - await writeFile(filePath, encoder.encode(template)); - - const makeExecutable = run({ args: ["chmod", "+x", filePath] }); - const { code } = await makeExecutable.status(); - makeExecutable.close(); - - if (code !== 0) { - throw new Error("Failed to make file executable"); - } + await generateExecutable(filePath, commands); console.log(`✅ Successfully installed ${moduleName}`); console.log(filePath); - // TODO: add Windows version if (!checkIfExistsInPath(installerDir)) { console.log("\nℹ️ Add ~/.deno/bin to PATH"); console.log( @@ -235,15 +254,14 @@ export async function uninstall(moduleName: string): Promise { const installerDir = getInstallerDir(); const filePath = path.join(installerDir, moduleName); - try { - await stat(filePath); - } catch (e) { - if (e instanceof Deno.DenoError && e.kind === Deno.ErrorKind.NotFound) { - throw new Error(`ℹ️ ${moduleName} not found`); - } + if (!(await exists(filePath))) { + throw new Error(`ℹ️ ${moduleName} not found`); } await remove(filePath); + if (isWindows) { + await remove(filePath + ".cmd"); + } console.log(`ℹ️ Uninstalled ${moduleName}`); } diff --git a/installer/test.ts b/installer/test.ts index 1b1aa4200be2..d28a9d4485af 100644 --- a/installer/test.ts +++ b/installer/test.ts @@ -1,5 +1,5 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. -const { readFile, run, stat, makeTempDir, remove, env } = Deno; +const { run, stat, makeTempDir, remove, env } = Deno; import { test, runIfMain, TestFunction } from "../testing/mod.ts"; import { assert, assertEquals, assertThrowsAsync } from "../testing/asserts.ts"; @@ -7,8 +7,10 @@ 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"; +import * as fs from "../fs/mod.ts"; let fileServer: Deno.Process; +const isWindows = Deno.platform.os === "win"; // copied from `http/file_server_test.ts` async function startFileServer(): Promise { @@ -63,11 +65,40 @@ installerTest(async function installBasic(): Promise { const fileInfo = await stat(filePath); assert(fileInfo.isFile()); - const fileBytes = await readFile(filePath); - const fileContents = new TextDecoder().decode(fileBytes); + if (isWindows) { + assertEquals( + await fs.readFileStr(filePath + ".cmd"), + `% This executable is generated by Deno. Please don't modify it unless you know what it means. % +@IF EXIST "%~dp0\deno.exe" ( + "%~dp0\deno.exe" run http://localhost:4500/http/file_server.ts %* +) ELSE ( + @SETLOCAL + @SET PATHEXT=%PATHEXT:;.TS;=;% + deno run http://localhost:4500/http/file_server.ts %* +) +` + ); + } + assertEquals( - fileContents, - "#/bin/sh\ndeno http://localhost:4500/http/file_server.ts $@" + await fs.readFileStr(filePath), + `#/bin/sh +# This executable is generated by Deno. Please don't modify it unless you know what it means. +basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") + +case \`uname\` in + *CYGWIN*) basedir=\`cygpath -w "$basedir"\`;; +esac + +if [ -x "$basedir/deno" ]; then + "$basedir/deno" run http://localhost:4500/http/file_server.ts "$@" + ret=$? +else + deno run http://localhost:4500/http/file_server.ts "$@" + ret=$? +fi +exit $ret +` ); }); @@ -81,11 +112,40 @@ installerTest(async function installWithFlags(): Promise { const { HOME } = env(); const filePath = path.resolve(HOME, ".deno/bin/file_server"); - const fileBytes = await readFile(filePath); - const fileContents = new TextDecoder().decode(fileBytes); + if (isWindows) { + assertEquals( + await fs.readFileStr(filePath + ".cmd"), + `% This executable is generated by Deno. Please don't modify it unless you know what it means. % +@IF EXIST "%~dp0\deno.exe" ( + "%~dp0\deno.exe" run --allow-net --allow-read http://localhost:4500/http/file_server.ts --foobar %* +) ELSE ( + @SETLOCAL + @SET PATHEXT=%PATHEXT:;.TS;=;% + deno run --allow-net --allow-read http://localhost:4500/http/file_server.ts --foobar %* +) +` + ); + } + assertEquals( - fileContents, - "#/bin/sh\ndeno --allow-net --allow-read http://localhost:4500/http/file_server.ts --foobar $@" + await fs.readFileStr(filePath), + `#/bin/sh +# This executable is generated by Deno. Please don't modify it unless you know what it means. +basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") + +case \`uname\` in + *CYGWIN*) basedir=\`cygpath -w "$basedir"\`;; +esac + +if [ -x "$basedir/deno" ]; then + "$basedir/deno" run --allow-net --allow-read http://localhost:4500/http/file_server.ts --foobar "$@" + ret=$? +else + deno run --allow-net --allow-read http://localhost:4500/http/file_server.ts --foobar "$@" + ret=$? +fi +exit $ret +` ); }); @@ -97,16 +157,8 @@ installerTest(async function uninstallBasic(): Promise { 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); + assert(!(await fs.exists(filePath))); + assert(!(await fs.exists(filePath + ".cmd"))); }); installerTest(async function uninstallNonExistentModule(): Promise {