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

Add fnm exec to run commands with the fnm environment #194

Merged
merged 10 commits into from
Feb 27, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
93 changes: 93 additions & 0 deletions executable/Exec.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
open Fnm;

exception System_Version_Not_Supported;
exception Ambiguous_Arguments;

let startsWith = (~prefix, str) =>
Base.String.prefix(str, String.length(prefix)) != prefix;

let unsafeRun = (~cmd, ~version as maybeVersion, ~useFileVersion) => {
let%lwt version =
switch (maybeVersion, useFileVersion) {
| (None, false) => Lwt.return_none
| (Some(_), true) => Lwt.fail(Ambiguous_Arguments)
| (None, true) => Fnm.Dotfiles.getVersion() |> Lwt.map(x => Some(x))
| (Some(version), false) => Lwt.return_some(version)
};
let%lwt currentVersion =
switch (version) {
| None => Lwt.return(Directories.currentVersion)
| Some(version) =>
let%lwt matchingVersion = LocalVersionResolver.getVersion(version);
let matchingVersionPath =
switch (matchingVersion) {
| Alias(path) => Versions.Aliases.toDirectory(path)
| Local(path) => Versions.Local.toDirectory(path)
| System => raise(System_Version_Not_Supported)
};
Lwt.return(matchingVersionPath);
};
let fnmPath = Filename.concat(currentVersion, "bin");
let path = Opt.(Sys.getenv_opt("PATH") or "");
let pathEnv = Printf.sprintf("PATH=%s:%s", fnmPath, path);
let cmd = cmd |> Array.copy |> Array.append([|"env", pathEnv|]);
let%lwt exitCode =
Lwt_process.exec(
~stdin=`Keep,
~stdout=`Keep,
~stderr=`Keep,
~env=Unix.environment(),
("", cmd),
);

switch (exitCode) {
| Unix.WEXITED(0) => Lwt.return_ok()
| Unix.WEXITED(x)
| Unix.WSTOPPED(x)
| Unix.WSIGNALED(x) => Lwt.return_error(x)
};
};

let run = (~cmd, ~version, ~useFileVersion) => {
try%lwt(unsafeRun(~cmd, ~version, ~useFileVersion)) {
| Ambiguous_Arguments =>
Console.error(
<Pastel color=Pastel.Red>
<Pastel bold=true> "Error: " </Pastel>
"You passed both "
<Pastel color=Pastel.Cyan> "--using" </Pastel>
" and "
<Pastel color=Pastel.Cyan> "--using-file" </Pastel>
".\n"
"Please provide only one of them."
</Pastel>,
);
Lwt.return_error(1);
| System_Version_Not_Supported =>
Console.error(
<Pastel color=Pastel.Red>
<Pastel bold=true> "Error: " </Pastel>
"System version is not supported in "
<Pastel color=Pastel.Yellow> "`fnm exec`" </Pastel>
</Pastel>,
);
Lwt.return_error(1);
| LocalVersionResolver.Version_Not_Installed(versionName) =>
Console.error(
<Pastel color=Pastel.Red>
<Pastel bold=true> "Error: " </Pastel>
"Version "
<Pastel color=Pastel.Cyan> versionName </Pastel>
" is not installed."
</Pastel>,
);
Lwt.return_error(1);
| Dotfiles.Version_Not_Provided =>
Console.error(
<Pastel color=Pastel.Red>
"No .nvmrc or .node-version file was found in the current directory. Please provide a version number."
</Pastel>,
);
Lwt.return_error(1);
};
};
48 changes: 47 additions & 1 deletion executable/FnmApp.re
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ let runCmd = lwt => {
};

module Commands = {
let exec = (version, useFileVersion, cmd) =>
Exec.run(~cmd=Array.of_list(cmd), ~version, ~useFileVersion) |> runCmd;
let use = (version, quiet) => Use.run(~version, ~quiet) |> runCmd;
let alias = (version, name) => Alias.run(~name, ~version) |> runCmd;
let default = version => Alias.run(~name="default", ~version) |> runCmd;
Expand Down Expand Up @@ -233,6 +235,40 @@ let alias = {
);
};

let exec = {
let doc = "Execute a binary with the current Node.js in the PATH";
let man = help_secs;
let sdocs = Manpage.s_common_options;

let usingVersion = {
let doc = "Use a specific $(docv)";
Arg.(value & opt(some(string), None) & info(["using"], ~doc));
};

let usingFileVersion = {
let doc = "Use a version from a version file";
Arg.(value & flag & info(["using-file"], ~doc));
};

let command = {
let doc = "The $(docv) to execute";
Arg.(non_empty & pos_all(string, []) & info([], ~docv="COMMAND", ~doc));
};

(
Term.(const(Commands.exec) $ usingVersion $ usingFileVersion $ command),
Term.info(
"exec",
~envs,
~version,
~doc,
~exits=Term.default_exits,
~man,
~sdocs,
),
);
};

let default = {
let doc = "Alias a version as default";
let man = help_secs;
Expand Down Expand Up @@ -378,7 +414,17 @@ let argv =
let _ =
Term.eval_choice(
defaultCmd,
[install, uninstall, use, alias, default, listLocal, listRemote, env],
[
install,
uninstall,
use,
alias,
default,
listLocal,
listRemote,
env,
exec,
],
~argv,
)
|> Term.exit;
13 changes: 2 additions & 11 deletions executable/Uninstall.re
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,8 @@ open Fnm;
open Lwt.Infix;

let run = (~version) => {
let%lwt installedVersions = Versions.getInstalledVersions();

let formattedVersionName = Versions.format(version);
let matchingLocalVersions =
installedVersions
|> Versions.(
List.filter(v =>
isVersionFitsPrefix(formattedVersionName, Local.(v.name))
|| v.name == formattedVersionName
)
);
let%lwt matchingLocalVersions =
LocalVersionResolver.getMatchingLocalVersions(version);

switch (matchingLocalVersions) {
| [] =>
Expand Down
22 changes: 2 additions & 20 deletions executable/Use.re
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ open Fnm;

let lwtIgnore = lwt => Lwt.catch(() => lwt, _ => Lwt.return());

exception Version_Not_Installed(string);

let info = (~quiet, arg) =>
if (!quiet) {
Logger.info(arg);
Expand All @@ -19,26 +17,10 @@ let error = (~quiet, arg) =>
Logger.error(arg);
};

let getVersion = version => {
let%lwt parsed = Versions.parse(version);
let%lwt resultWithLts =
switch (parsed) {
| Ok(x) => Lwt.return_ok(x)
| Error("latest-*") =>
switch%lwt (VersionListingLts.getLatest()) {
| Error(_) => Lwt.return_error(Version_Not_Installed(version))
| Ok({VersionListingLts.lts, _}) =>
Versions.Alias("latest-" ++ lts) |> Lwt.return_ok
}
| _ => Version_Not_Installed(version) |> Lwt.return_error
};
resultWithLts |> Result.fold(Lwt.fail, Lwt.return);
};

let switchVersion = (~version, ~quiet) => {
let info = info(~quiet);
let debug = debug(~quiet);
let%lwt parsedVersion = getVersion(version);
let%lwt parsedVersion = LocalVersionResolver.getVersion(version);

let%lwt versionPath =
switch (parsedVersion) {
Expand Down Expand Up @@ -120,7 +102,7 @@ let rec askIfInstall = (~version, ~quiet, retry) => {

let rec run = (~version, ~quiet) =>
try%lwt(main(~version, ~quiet)) {
| Version_Not_Installed(versionString) =>
| LocalVersionResolver.Version_Not_Installed(versionString) =>
error(
~quiet,
<Pastel color=Pastel.Red>
Expand Down
12 changes: 12 additions & 0 deletions feature_tests/exec/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/bash

set -e

fnm install v6.10.0
fnm install v8.10.0
fnm install v10.10.0
fnm use v8.10.0

fnm exec -- node -v | grep "v8.10.0"
fnm exec --using 6 -- node -v | grep "v6.10.0"
fnm exec --using 10 -- node -v | grep "v10.10.0"
38 changes: 38 additions & 0 deletions library/LocalVersionResolver.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
exception Version_Not_Installed(string);

/** Parse a local version, including lts and aliases */
let getVersion = version => {
let%lwt parsed = Versions.parse(version);
let%lwt resultWithLts =
switch (parsed) {
| Ok(x) => Lwt.return_ok(x)
| Error("latest-*") =>
switch%lwt (VersionListingLts.getLatest()) {
| Error(_) => Lwt.return_error(Version_Not_Installed(version))
| Ok({VersionListingLts.lts, _}) =>
Versions.Alias("latest-" ++ lts) |> Lwt.return_ok
}
| _ => Version_Not_Installed(version) |> Lwt.return_error
};
resultWithLts |> Result.fold(Lwt.fail, Lwt.return);
};

/**
* Get matches for all versions that match a semver partial
*/
let getMatchingLocalVersions = version => {
open Versions.Local;

let%lwt installedVersions = Versions.getInstalledVersions();
let formattedVersionName = Versions.format(version);

let matchingVersions =
installedVersions
|> List.filter(v =>
Versions.isVersionFitsPrefix(formattedVersionName, v.name)
|| v.name == formattedVersionName
)
|> List.sort((a, b) => - compare(a.name, b.name));

Lwt.return(matchingVersions);
};
1 change: 1 addition & 0 deletions test/TestFramework.re
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ let run = args => {
Unix.environment()
|> Array.append([|
Printf.sprintf("%s=%s", Fnm.Config.FNM_DIR.name, tmpDir),
"FORCE_COLOR=false",
|]);
let result =
Lwt_process.pread_chars(~env, ("", arguments)) |> Lwt_stream.to_string;
Expand Down
6 changes: 3 additions & 3 deletions test/TestListRemote.re
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,14 @@ let allVersions6_11 = [
"v6.11.5",
];

describe("List Remote", ({test}) => {
let versionRegExp = Str.regexp(".*[0-9]+\.[0-9]+\.[0-9]+\|.*latest-*");
describe("List Remote", ({test, _}) => {
let versionRegExp = Str.regexp(".*[0-9]+\\.[0-9]+\\.[0-9]+\\|.*latest-*");

let filterVersionNumbers = response =>
response
|> String.split_on_char('\n')
|> List.filter(s => Str.string_match(versionRegExp, s, 0))
|> List.map(s => Str.replace_first(Str.regexp("\*"), "", s))
|> List.map(s => Str.replace_first(Str.regexp("\\*"), "", s))
|> List.map(String.trim);

let runAndFilterVersionNumbers = args => run(args) |> filterVersionNumbers;
Expand Down
8 changes: 4 additions & 4 deletions test/TestUninstall.re
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ let isVersionInstalled = version =>
|> String.split_on_char('\n')
|> List.exists(v => v == "* v" ++ version);

describe("Uninstall", ({test}) => {
describe("Uninstall", ({test, _}) => {
test("Should be possible to uninstall a specific version", ({expect, _}) => {
let version = "6.0.0";
let _ = installVersion(version);
Expand All @@ -29,9 +29,9 @@ describe("Uninstall", ({test}) => {
uninstallVersion("6")
|> String.split_on_char('\n')
|> String.concat(" ");
expect.string(response).toMatch(
".*multiple versions.*" ++ v1 ++ ".*" ++ v2 ++ ".*",
);
expect.string(response).toMatch("multiple versions");
expect.string(response).toMatch(" v" ++ v1 ++ " ");
expect.string(response).toMatch(" v" ++ v2 ++ " ");
expect.bool(isVersionInstalled(v1)).toBeTrue();
expect.bool(isVersionInstalled(v2)).toBeTrue();
clearTmpDir();
Expand Down