diff --git a/executable/Exec.re b/executable/Exec.re new file mode 100644 index 000000000..6a2667c0d --- /dev/null +++ b/executable/Exec.re @@ -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( + + "Error: " + "You passed both " + "--using" + " and " + "--using-file" + ".\n" + "Please provide only one of them." + , + ); + Lwt.return_error(1); + | System_Version_Not_Supported => + Console.error( + + "Error: " + "System version is not supported in " + "`fnm exec`" + , + ); + Lwt.return_error(1); + | LocalVersionResolver.Version_Not_Installed(versionName) => + Console.error( + + "Error: " + "Version " + versionName + " is not installed." + , + ); + Lwt.return_error(1); + | Dotfiles.Version_Not_Provided => + Console.error( + + "No .nvmrc or .node-version file was found in the current directory. Please provide a version number." + , + ); + Lwt.return_error(1); + }; +}; diff --git a/executable/FnmApp.re b/executable/FnmApp.re index c6281563f..dc477105f 100644 --- a/executable/FnmApp.re +++ b/executable/FnmApp.re @@ -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; @@ -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; @@ -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; diff --git a/executable/Uninstall.re b/executable/Uninstall.re index f61da514e..f4468450f 100644 --- a/executable/Uninstall.re +++ b/executable/Uninstall.re @@ -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) { | [] => diff --git a/executable/Use.re b/executable/Use.re index 25e46806a..38dee1ea8 100644 --- a/executable/Use.re +++ b/executable/Use.re @@ -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); @@ -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) { @@ -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, diff --git a/feature_tests/exec/run.sh b/feature_tests/exec/run.sh new file mode 100755 index 000000000..f5a388893 --- /dev/null +++ b/feature_tests/exec/run.sh @@ -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" diff --git a/library/LocalVersionResolver.re b/library/LocalVersionResolver.re new file mode 100644 index 000000000..f848efba4 --- /dev/null +++ b/library/LocalVersionResolver.re @@ -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); +}; diff --git a/test/TestFramework.re b/test/TestFramework.re index 191dd93bc..a5b607809 100644 --- a/test/TestFramework.re +++ b/test/TestFramework.re @@ -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; diff --git a/test/TestListRemote.re b/test/TestListRemote.re index ee2759086..b1f9cda99 100644 --- a/test/TestListRemote.re +++ b/test/TestListRemote.re @@ -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; diff --git a/test/TestUninstall.re b/test/TestUninstall.re index e3e35fdda..85eeb1b93 100644 --- a/test/TestUninstall.re +++ b/test/TestUninstall.re @@ -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); @@ -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();