You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

356 lines
9.0 KiB

6 years ago
module VersionSet = Set.Make(String);
let lwtIgnore = lwt => Lwt.catch(() => lwt, _ => Lwt.return());
let flip = (fn, a, b) => fn(b, a);
let skip = (~amount, str) =>
Str.last_chars(str, String.length(str) - amount);
let parseSemver = version => version |> skip(~amount=1) |> Semver.fromString;
let compare = (v1, v2) =>
switch (parseSemver(v1), parseSemver(v2)) {
| (Some(v1), Some(v2)) => Semver.compare(v1, v2)
| (None, _)
| (_, None) => - Base.String.compare(v1, v2)
};
let isVersionFitsPrefix = (prefix, version) => {
let length = String.length(prefix);
String.length(version) >= length
+ 1
&& Str.first_chars(version, length + 1) == prefix
++ ".";
};
6 years ago
module Local = {
type t = {
name: string,
fullPath: string,
aliases: list(string),
6 years ago
};
let systemVersion: t = {
name: "system",
fullPath: "/dev/null/installation",
aliases: [],
};
let toDirectory = name =>
Filename.concat(
Filename.concat(Directories.nodeVersions, name),
"installation",
);
let remove = version => Fs.rmdir(version.fullPath);
let getLatestInstalledNameByPrefix = prefix => {
open Lwt.Infix;
let%lwt versions =
Lwt.catch(
() =>
Fs.readdir(Directories.nodeVersions)
>|= List.filter(isVersionFitsPrefix(prefix))
>|= List.sort(flip(compare)),
_ => Lwt.return_nil,
);
switch (versions) {
| [version, ..._xs] => Lwt.return_some(version)
| [] => Lwt.return_none
};
};
6 years ago
};
exception Version_not_found(string);
exception Already_installed(string);
6 years ago
module Aliases = {
module VersionAliasMap = Map.Make(String);
type t = {
name: string,
versionName: string,
fullPath: string,
};
let toDirectory = name => Filename.concat(Directories.aliases, name);
let getAll = () => {
let%lwt aliases =
try%lwt (Fs.readdir(Directories.aliases)) {
| _ => Lwt.return([])
};
aliases
|> Lwt_list.map_p(alias => {
let fullPath = Filename.concat(Directories.aliases, alias);
let%lwt realpath =
Filename.concat(Directories.aliases, alias)
|> Fs.realpath
|> Lwt.map(
fun
| Fs.Exists(x) => x
| Fs.Missing(x) => x,
);
Lwt.return({
name: alias,
fullPath,
versionName: realpath |> Filename.dirname |> Filename.basename,
});
});
};
let byVersion = () => {
let%lwt aliases = getAll();
aliases
|> List.fold_left(
(map, curr) => {
let value =
switch (VersionAliasMap.find_opt(curr.versionName, map)) {
| None => [curr.name]
| Some(arr) => [curr.name, ...arr]
};
VersionAliasMap.add(curr.versionName, value, map);
},
VersionAliasMap.empty,
)
|> Lwt.return;
};
let set = (~alias, ~versionPath) => {
let aliasPath = alias |> toDirectory;
let%lwt _ = System.mkdirp(Directories.aliases);
let%lwt _ = Lwt_unix.unlink(aliasPath) |> lwtIgnore;
let%lwt _ = Lwt_unix.symlink(versionPath, aliasPath);
Lwt.return();
};
};
6 years ago
module Remote = {
type t = {
name: string,
baseURL: string,
installed: bool,
};
let getInstalledVersionSet = () =>
Lwt.(
catch(() => Fs.readdir(Directories.nodeVersions), _ => return([]))
>|= List.fold_left(
(acc, curr) => VersionSet.add(curr, acc),
VersionSet.empty,
)
);
6 years ago
let getRelativeLinksFromHTML = html =>
Soup.parse(html)
|> Soup.select("pre a")
|> Soup.to_list
|> List.map(Soup.attribute("href"))
|> Base.List.filter_map(~f=x => x)
|> List.map(x => {
let parts = String.split_on_char('/', x) |> List.rev;
switch (parts) {
| ["", x, ..._xs] => x ++ "/"
| [x, ..._xs] => x
| [] => ""
};
});
6 years ago
let downloadFileSuffix = ".tar.xz";
6 years ago
let getVersionFromFilename = filename => {
let strings = filename |> String.split_on_char('-');
List.nth(strings, 1);
};
};
let format = version => {
let version =
switch (Str.first_chars(version, 1) |> Int32.of_string) {
| _ => "v" ++ version
| exception _ => version
};
version;
};
let endsWith = (~suffix, str) => {
let suffixLength = String.length(suffix);
String.length(str) > suffixLength
&& Str.last_chars(str, suffixLength) == suffix;
};
exception No_Download_For_System(System.NodeOS.t, System.NodeArch.t);
let getCurrentVersion = () => {
switch%lwt (Fs.realpath(Directories.currentVersion)) {
| Missing(x) when x == Directories.currentVersion => Lwt.return_none
| Missing(_) => Lwt.return_some(Local.systemVersion)
| Exists(installationPath) =>
6 years ago
let fullPath = Filename.dirname(installationPath);
Lwt.return_some(
Local.{
fullPath,
name:
fullPath
|> Path.absolute
|> Base.Option.bind(~f=Path.baseName)
|> Base.Option.value(~default=""),
aliases: [],
},
);
6 years ago
};
};
let getInstalledVersions = () => {
let%lwt versions =
Fs.readdir(Directories.nodeVersions) |> Lwt.map(List.sort(compare))
and aliases = Aliases.byVersion();
versions
|> List.map(name =>
Local.{
name,
fullPath: Filename.concat(Directories.nodeVersions, name),
aliases: Opt.(Aliases.VersionAliasMap.find_opt(name, aliases) or []),
}
)
|> List.append([Local.systemVersion])
|> Lwt.return;
};
6 years ago
let getRemoteVersions = () => {
let%lwt bodyString =
Config.FNM_NODE_DIST_MIRROR.get()
|> Http.makeRequest
|> Lwt.map(Http.body);
6 years ago
let versions = bodyString |> Remote.getRelativeLinksFromHTML;
let%lwt installedVersions = Remote.getInstalledVersionSet();
6 years ago
versions
|> List.filter(x =>
6 years ago
Str.last_chars(x, 1) == "/" && Str.first_chars(x, 1) != "."
)
|> List.map(x => Str.first_chars(x, String.length(x) - 1))
|> List.sort(compare)
6 years ago
|> List.map(name =>
Remote.{
name,
installed: VersionSet.find_opt(name, installedVersions) != None,
baseURL:
Printf.sprintf("%s%s/", Config.FNM_NODE_DIST_MIRROR.get(), name),
6 years ago
}
)
|> Lwt.return;
};
let getRemoteLatestVersionByPrefix = prefix => {
open Remote;
let%lwt remoteVersions = getRemoteVersions();
let compatibleVersions =
remoteVersions
|> List.map(x => x.name)
|> List.filter(isVersionFitsPrefix(prefix))
|> List.sort(flip(compare));
switch (compatibleVersions) {
| [version, ..._vs] => Lwt.return_some(version)
| [] => Lwt.return_none
};
};
let getExactFileToDownload = (~version as versionName, ~os, ~arch) => {
let versionName =
switch (Str.first_chars(versionName, 1) |> Int32.of_string) {
| _ => "v" ++ versionName
| exception _ => versionName
};
let url =
Printf.sprintf("%s%s/", Config.FNM_NODE_DIST_MIRROR.get(), versionName);
let%lwt html =
try%lwt (Http.makeRequest(url) |> Lwt.map(Http.body)) {
| Http.Not_found(_) => Lwt.fail(Version_not_found(versionName))
};
let filenames =
html
|> Remote.getRelativeLinksFromHTML
|> List.filter(
endsWith(
~suffix=
System.NodeOS.toString(os)
++ "-"
++ System.NodeArch.toString(arch)
++ Remote.downloadFileSuffix,
),
);
switch (filenames |> List.hd) {
| filename =>
let nodeVersion = List.nth(String.split_on_char('-', filename), 1);
Lwt.return((nodeVersion, url ++ filename));
| exception _ => Lwt.fail(No_Download_For_System(os, arch))
};
};
let getFileToDownload = (~version, ~os, ~arch) => {
try%lwt (getExactFileToDownload(~version, ~os, ~arch)) {
| Version_not_found(_) as e =>
switch%lwt (getRemoteLatestVersionByPrefix(version)) {
| None => Lwt.fail(e)
| Some(exactVersion) =>
getExactFileToDownload(~version=exactVersion, ~os, ~arch)
}
};
};
type t =
| System
| Alias(string)
| Local(string);
let parse = version => {
let formattedVersion = format(version);
if (formattedVersion == Local.systemVersion.name) {
Lwt.return_some(System);
} else {
let aliasPath = Aliases.toDirectory(version);
let versionPath = Local.toDirectory(formattedVersion);
let%lwt aliasExists = Fs.exists(aliasPath)
and versionExists = Fs.exists(versionPath)
and versionByPrefixPath =
Local.getLatestInstalledNameByPrefix(formattedVersion);
switch (versionExists, aliasExists, versionByPrefixPath) {
| (true, _, _) => Some(Local(formattedVersion)) |> Lwt.return
| (_, true, _) => Some(Alias(version)) |> Lwt.return
| (_, false, Some(version)) => Some(Local(version)) |> Lwt.return
| (false, false, None) => Lwt.return_none
};
};
};
let throwIfInstalled = versionName => {
let%lwt installedVersions =
try%lwt (getInstalledVersions()) {
| _ => Lwt.return([])
};
let isAlreadyInstalled =
installedVersions |> List.exists(x => Local.(x.name == versionName));
if (isAlreadyInstalled) {
Lwt.fail(Already_installed(versionName));
} else {
Lwt.return();
};
6 years ago
};