Challenges in developing portable OCaml projects
Maintaining esy
I helped them build a package manager and ship multi-platform builds
Focuses on reproducibility.
For a unified workflow for common OCaml projects
The following project will pull in ocamlfind
whose latest version
isn't available on Windows.
{
"dependencies": {
"@opam/cmdliner": "*"
}
}
available: os != "win32"
Boolean expressions that tell if a package is available or not
Could be because of os, arch, compiler variant etc.
available: arch != "arm32" & arch != "x86_32"
Not available on arm32
and x86_32
(32-bit systems). Available on
systems like Windows.
available: opam-version >= "5.0"
available: !(os = "macos" & arch = "arm64")
Then packages like Eio
stop working.
Opam has many such platform specific packages.
available:
"eio_linux"
{= version & os = "linux" &
(os-distribution != "centos" | os-version > "7")}
"eio_posix" {= version & os != "win32"}
"eio_windows" {= version & os = "win32"}
On a Linux machine, ignoring the available
field would install all
three variants (eio_linux
, eio_posix
and eio_windows
), which is wrong!
And repeat the process for all platforms and persist the solution
diff --git a/bin/esy.re b/bin/esy.re
index 23d61045e..48dbeca54 100644
--- a/bin/esy.re
+++ b/bin/esy.re
@@ -1,3 +1,4 @@
+[@ocaml.warning "-69"];
open EsyPrimitives;
open EsyBuild;
open EsyPackageConfig;
@@ -999,6 +1000,31 @@ let solve = (force, dumpCudfInput, dumpCudfOutput, proj: Project.t) => {
};
};
+let checkSolutionPortability = (proj: Project.t) => {
+ open RunAsync.Syntax;
+ let lockPath = SandboxSpec.solutionLockPath(proj.projcfg.spec);
+ switch%bind (SolutionLock.ofPath(proj.installSandbox, lockPath)) {
+ | Some(solution) =>
+ let* unPortableDependencies = EsyFetch.Solution.unPortableDependencies(solution);
+ let%lwt () =
+ switch(unPortableDependencies) {
+ | [] => Esy_logs_lwt.app(m => m("Is portable"));
+ | unsupportedPlatforms => {
+ let%lwt () = Esy_logs_lwt.app(m => m("The following packages are problematic and dont build on specified platform"));
+ let f = ((package, _platform)) => {
+ Esy_logs_lwt.app(m => m("Package %a", Package.pp, package));
+ };
+ List.map(~f, unsupportedPlatforms) |> Lwt.join
+ }
+ };
+ RunAsync.return();
+ | None =>
+ error(
+ "No lock found, therefore not solution to check for its portability.",
+ )
+ };
+};
+
let fetch = (proj: Project.t) => {
open RunAsync.Syntax;
let lockPath = SandboxSpec.solutionLockPath(proj.projcfg.spec);
@@ -2093,6 +2119,12 @@ let commandsConfig = {
$ pkgTerm
),
),
+ makeProjectCommand(
+ ~name="check-solution-portability",
+ ~doc="Check if lock file is portable on other platforms",
+ ~docs=lowLevelSection,
+ Term.(const(checkSolutionPortability)),
+ ),
makeProjectCommand(
~name="solve",
~doc="Solve dependencies and store the solution",
available
field in esy's internal representationdiff --git a/esy-fetch/Package.re b/esy-fetch/Package.re
index e5cee096d..bd50c5aac 100644
--- a/esy-fetch/Package.re
+++ b/esy-fetch/Package.re
@@ -10,7 +10,7 @@ type t = {
devDependencies: PackageId.Set.t,
installConfig: InstallConfig.t,
extraSources: list(ExtraSource.t),
- available: option(string),
+ available: AvailablePlatforms.t,
};
let compare = (a, b) => PackageId.compare(a.id, b.id);
@@ -47,19 +47,6 @@ let opam = pkg =>
}
);
-let evaluateOpamPackageAvailability = pkg => {
- /*
- Allowing sources here would let us resolve to github urls for
- npm dependencies. Atleast in theory. TODO: test this
- */
- switch (pkg.version, pkg.available) {
- | (Source(_), Some(availabilityFilter))
- | (Opam(_), Some(availabilityFilter)) =>
- EsyOpamLibs.Available.eval(availabilityFilter)
- | _ => true
- };
-};
-
module Map =
Map.Make({
type nonrec t = t;
diff --git a/esy-fetch/Package.rei b/esy-fetch/Package.rei
index be9a5a5f9..b59f80cd2 100644
--- a/esy-fetch/Package.rei
+++ b/esy-fetch/Package.rei
@@ -10,13 +10,12 @@ type t = {
devDependencies: PackageId.Set.t,
installConfig: InstallConfig.t, /* currently only tells if pnp is enabled or not */
extraSources: list(ExtraSource.t), /* See opam manual */
- available: option(string),
+ available: AvailablePlatforms.t,
};
let id: t => PackageId.t;
let extraSources: t => list(ExtraSource.t);
let opam: t => RunAsync.t(option((string, Version.t, OpamFile.OPAM.t)));
-let evaluateOpamPackageAvailability: t => bool;
include S.COMPARABLE with type t := t;
include S.PRINTABLE with type t := t;
diff --git a/esy-fetch/Solution.re b/esy-fetch/Solution.re
index 5c798fad5..c4bd54545 100644
--- a/esy-fetch/Solution.re
+++ b/esy-fetch/Solution.re
@@ -119,3 +119,20 @@ let findByNameVersion = (name, version, solution) => {
let%map (_id, pkg) = Graph.findBy(solution, f);
pkg;
};
+
+let unPortableDependencies = solution => {
+ open Package;
+ let f = (pkg) => {
+ let missingPlatforms =
+ AvailablePlatforms.missing(
+ ~expected=AvailablePlatforms.default,
+ ~actual=pkg.available,
+ )
+ if (AvailablePlatforms.isEmpty(missingPlatforms)) {
+ None;
+ } else {
+ Some((pkg, missingPlatforms));
+ }
+ };
+ nodes(solution) |> List.filter_map(~f) |> RunAsync.return;
+};
diff --git a/esy-fetch/Solution.rei b/esy-fetch/Solution.rei
index 1b8e7609c..2f2987a7e 100644
--- a/esy-fetch/Solution.rei
+++ b/esy-fetch/Solution.rei
@@ -55,3 +55,9 @@ let eval: (t, FetchDepSpec.t, PackageId.t) => PackageId.Set.t;
*/
let collect: (t, FetchDepSpec.t, PackageId.t) => PackageId.Set.t;
+
+/**
+ Returns a list of dependencies that don't,ca build on other platforms. The default list of platforms
+ (os, arch) tuples are list in [DefaultPlatforms]
+*/
+let unPortableDependencies: t => RunAsync.t(list((pkg, AvailablePlatforms.t)));
We only parameterise only on os
and arch
for now.
diff --git a/esy-opam/Available.re b/esy-opam/Available.re
index 5f9116f33..c0e4f1823 100644
--- a/esy-opam/Available.re
+++ b/esy-opam/Available.re
@@ -12,18 +12,19 @@ let parseOpamFilterString = filterString => {
opamFile.available;
};
-let evalAvailabilityFilter = filter => {
+let evalAvailabilityFilter = (~os, ~arch, filter) => {
let env = (var: OpamVariable.Full.t) => {
let scope = OpamVariable.Full.scope(var);
let name = OpamVariable.Full.variable(var);
switch (scope, OpamVariable.to_string(name)) {
| (OpamVariable.Full.Global, "arch") =>
- Some(OpamVariable.string(System.Arch.show(System.Arch.host)))
+ Some(OpamVariable.string(System.Arch.show(arch)))
| (OpamVariable.Full.Global, "os") =>
+ open System.Platform;
// We could have avoided the following altogether if the System.Platform implementation
// matched opam's. TODO
let sys =
- switch (System.Platform.host) {
+ switch (os) {
| Darwin => "macos"
| Linux => "linux"
| Cygwin => "cygwin"
@@ -41,6 +42,7 @@ let evalAvailabilityFilter = filter => {
OpamFilter.eval_to_bool(~default=true, env, filter);
};
-let eval = availabilityFilter => {
- parseOpamFilterString(availabilityFilter) |> evalAvailabilityFilter;
+let eval = (~os, ~arch, availabilityFilter) => {
+ parseOpamFilterString(availabilityFilter)
+ |> evalAvailabilityFilter(~os, ~arch);
};
diff --git a/esy-opam/Available.rei b/esy-opam/Available.rei
index d8589b807..c83f71a03 100644
--- a/esy-opam/Available.rei
+++ b/esy-opam/Available.rei
@@ -1,8 +1,8 @@
+let evalAvailabilityFilter: (~os: System.Platform.t, ~arch: System.Arch.t, OpamTypes.filter) => bool;
/**
- Evaluates available field (cached in lock file) and determines if the package is available.
+ [eval(~os, ~arch, filter)] evaluates availability filter, [filter] and
+ determines if the package is available.
- Assumes [available] is currently only an opam filter, which means this function must be a no-op on
- NPM packages. Atleast till designed takes into account [platform] and [arch] fields in NPM manifests
*/
-let eval: string => bool;
+let eval: (~os: System.Platform.t, ~arch: System.Arch.t, string) => bool;
AvailablePlatform.t
diff --git a/esy-package-config/AvailablePlatforms.re b/esy-package-config/AvailablePlatforms.re
new file mode 100644
index 000000000..5eab0713b
--- /dev/null
+++ b/esy-package-config/AvailablePlatforms.re
@@ -0,0 +1,111 @@
+module Set = Set.Make({
+ [@deriving ord]
+ type t = (System.Platform.t, System.Arch.t);
+});
+
+type t = Set.t;
+
+let of_yojson =
+ fun
+| `List(availablePlatforms) => {
+ let f = acc => fun
+ | `List([`String(os), `String(arch)]) as json => switch((acc, System.Platform.parse(os), System.Arch.parse(arch))) {
+ | (Ok(acc), Ok(os), arch) => Ok(Set.add((os, arch), acc))
+ | _ => Result.errorf("AvailablePlatforms.parse couldn't parse: %a", Json.Print.ppRegular, json);
+ }
+ | json => Result.errorf("AvailablePlatforms.parse couldn't parse: %a", Json.Print.ppRegular, json);
+ List.fold_left(~f, ~init=Ok(Set.empty), availablePlatforms);
+ }
+ | json =>
+ Result.errorf(
+ "Unexpected JSON %a where AvailablePlatforms.t was expected",
+ Json.Print.ppRegular,
+ json,
+ );
+
+let to_yojson = platforms => {
+ let f = ((os, arch)) =>
+ `List([System.Platform.to_yojson(os), System.Arch.to_yojson(arch)]);
+ `List(platforms |> Set.elements |> List.map(~f));
+};
+
+let default: t = Set.of_list([
+ (System.Platform.Windows, System.Arch.X86_64),
+ (System.Platform.Linux, System.Arch.X86_64),
+ (System.Platform.Darwin, System.Arch.Arm64),
+]);
+
+let filter = (availabilityFilter, platforms) => {
+ let f = ((os, arch)) => {
+ EsyOpamLibs.Available.evalAvailabilityFilter(~os, ~arch, availabilityFilter);
+ };
+ Set.filter(f, platforms);
+};
+
+let missing = (~expected, ~actual) => Set.diff(expected, actual);
+
+let isEmpty = v => Set.is_empty(v);
+let empty = Set.empty;
+let add = (~os, ~arch, v) => Set.add((os, arch), v);
+
+let ppEntry = (ppf, (os, arch)) =>
+ Fmt.pf(ppf, "%a %a", System.Platform.pp, os, System.Arch.pp, arch);
+
+let pp = (ppf, v) => {
+ let sep = Fmt.any(", ");
+ Fmt.hbox(Fmt.list(~sep, ppEntry), ppf, Set.elements(v));
+};
+
+let union = (a, b) => Set.union(a, b);
+
+module Map = {
+ include Map.Make({
+ type t = available;
+ let compare = compare;
+ });
+
+ let to_yojson = (v_to_yojson, map) => {
+ let items = {
+ let f = (k, v, items) => {
+ let (os, arch) = k;
+ let k =
+ Format.asprintf(
+ "%a+%a",
+ System.Platform.pp,
+ os,
+ System.Arch.pp,
+ arch,
+ );
+ [(k, v_to_yojson(v)), ...items];
+ };
+
+ fold(f, map, []);
+ };
+
+ `Assoc(items);
+ };
+
+ let of_yojson = v_of_yojson =>
+ Result.Syntax.(
+ fun
+ | `Assoc(items) => {
+ let f = (map, (k, v)) => {
+ let* (os, arch) =
+ switch (String.split_on_char('+', k)) {
+ | [os, arch] =>
+ switch (System.Platform.parse(os), System.Arch.parse(arch)) {
+ | (Ok(os), arch) => Ok((os, arch))
+ | _ =>
+ Result.errorf("Couldn't parse %s into os-arch tuple", k)
+ }
+ | _ => errorf("Expect key %s to be of syntax <os>+<arch>", k)
+ };
+ let* v = v_of_yojson(v);
+ return(add((os, arch), v, map));
+ };
+
+ Result.List.foldLeft(~f, ~init=empty, items);
+ }
+ | _ => error("expected an object")
+ );
+};
available
field in internal InstallManifest.re
diff --git a/esy-package-config/InstallManifest.re b/esy-package-config/InstallManifest.re
index ed561ac0f..6665e6504 100644
--- a/esy-package-config/InstallManifest.re
+++ b/esy-package-config/InstallManifest.re
@@ -150,7 +150,7 @@ type t = {
kind,
installConfig: InstallConfig.t,
extraSources: list(ExtraSource.t),
- available: option(string),
+ available: AvailablePlatforms.t,
}
and kind =
| Esy
diff --git a/esy-package-config/InstallManifest.rei b/esy-package-config/InstallManifest.rei
index a20ada9f4..9418b5722 100644
--- a/esy-package-config/InstallManifest.rei
+++ b/esy-package-config/InstallManifest.rei
@@ -56,7 +56,7 @@ type t = {
kind,
installConfig: InstallConfig.t,
extraSources: list(ExtraSource.t),
- available: option(string) /* contains opam filter serialised to be eval'd later */
+ available: AvailablePlatforms.t,
}
and kind =
| Esy
diff --git a/esy-package-config/OfPackageJson.re b/esy-package-config/OfPackageJson.re
index f42e18f7f..5eb961e54 100644
--- a/esy-package-config/OfPackageJson.re
+++ b/esy-package-config/OfPackageJson.re
@@ -172,7 +172,7 @@ module InstallManifestV1 = {
/* */
/********************************************************************/
- let available = None;
+ let available = AvailablePlatforms.default;
return((
{
InstallManifest.name,
+ let availableFilter = OpamFile.OPAM.available(manifest.opam);
let available =
- manifest.opam |> OpamFile.OPAM.available |> OpamFilter.to_string;
+ AvailablePlatforms.default |> AvailablePlatforms.filter(availableFilter);
return((
dependencies,
@@ -645,7 +646,7 @@ let opamManifestToInstallManifest = (~source=?, ~name, ~version, manifest) => {
resolutions: Resolutions.empty,
installConfig: InstallConfig.empty,
extraSources,
- available: Some(available),
+ available,
}),
);
};
@@ -684,7 +685,7 @@ let packageOfSource = (~name, ~overrides, source: Source.t, resolver) => {
RunAsync.ofRun(
{
let version = OpamPackage.Version.of_string("dev");
- EsyOpamLibs.OpamManifest.ofString(
+ OpamManifest.ofString(
~name=opamname,
~version,
data,
os
and arch
diff --git a/esy-solve/Resolver.re b/esy-solve/Resolver.re
index b2b762c4f..ea0c608d2 100644
--- a/esy-solve/Resolver.re
+++ b/esy-solve/Resolver.re
@@ -443,10 +443,11 @@ let opamHashToChecksum = opamHash => {
let contents = OpamHash.contents(opamHash);
(kind, contents);
};
-let convertDependencies = manifest => {
+
+let convertDependencies = (~os, ~arch, manifest) => {
open Result.Syntax;
- let filterOpamFormula = (~build, ~post, ~test, ~doc, ~dev, f) => {
+ let filterOpamFormula = (~os, ~arch, ~build, ~post, ~test, ~doc, ~dev, f) => {
let f = {
let env = var => {
switch (OpamVariable.Full.to_string(var)) {
@@ -456,6 +457,29 @@ let convertDependencies = manifest => {
| "with-dev-setup" => Some(OpamVariable.B(dev))
| "with-doc" => Some(OpamVariable.B(doc))
| "dev" => Some(OpamVariable.B(dev))
+ | "arch" =>
+ switch (arch) {
+ | Some(arch) => Some(OpamVariable.S(System.Arch.show(arch)))
+ | None => None
+ }
+ | "os" =>
+ switch (os) {
+ | Some(os) =>
+ open System.Platform;
+ // We could have avoided the following altogether if the System.Platform implementation
+ // matched opam's. TODO
+ let sys =
+ switch (os) {
+ | Darwin => "macos"
+ | Linux => "linux"
+ | Cygwin => "cygwin"
+ | Unix => "unix"
+ | Windows => "win32"
+ | Unknown => "unknown"
+ };
+ Some(OpamVariable.S(sys));
+ | None => None
+ }
| "version" =>
let version =
OpamPackage.Version.to_string(
@@ -486,8 +510,9 @@ let convertDependencies = manifest => {
};
};
- let filterAndConvertOpamFormula = (~build, ~post, ~test, ~doc, ~dev, f) => {
- let* f = filterOpamFormula(~build, ~post, ~test, ~doc, ~dev, f);
+ let filterAndConvertOpamFormula = (~os, ~build, ~post, ~test, ~doc, ~dev, f) => {
+ let* f =
+ filterOpamFormula(~os, ~arch, ~build, ~post, ~test, ~doc, ~dev, f);
convertOpamFormula(f);
};
@@ -498,6 +523,7 @@ let convertDependencies = manifest => {
let* dependencies = {
let* formula =
filterAndConvertOpamFormula(
+ ~os,
~build=true,
~post=false,
~test=false,
@@ -519,6 +545,7 @@ let convertDependencies = manifest => {
let* devDependencies = {
let* formula =
filterAndConvertOpamFormula(
+ ~os,
~build=false,
~post=false,
~test=true,
@@ -532,6 +559,8 @@ let convertDependencies = manifest => {
let* optDependencies = {
let* formula =
filterOpamFormula(
+ ~os,
+ ~arch,
~build=false,
~post=false,
~test=true,
@@ -575,7 +604,8 @@ let convertDependencies = manifest => {
));
};
-let opamManifestToInstallManifest = (~source=?, ~name, ~version, manifest) => {
+let opamManifestToInstallManifest =
+ (~source=?, ~os, ~arch, ~name, ~version, manifest) => {
open RunAsync.Syntax;
let converted = {
@@ -588,7 +618,7 @@ let opamManifestToInstallManifest = (~source=?, ~name, ~version, manifest) => {
extraSources,
available,
) =
- convertDependencies(manifest);
+ convertDependencies(~os, ~arch, manifest);
return((
source,
dependencies,
@@ -693,6 +723,8 @@ let packageOfSource = (~name, ~overrides, source: Source.t, resolver) => {
},
);
opamManifestToInstallManifest(
+ ~os=resolver.os,
+ ~arch=resolver.arch,
~name,
~version=Version.Source(source),
~source,
@@ -924,6 +956,8 @@ let package = (~resolution: Resolution.t, resolver) => {
| Some(manifest) =>
let* pkg_result =
opamManifestToInstallManifest(
+ ~os=resolver.os,
+ ~arch=resolver.arch,
~name=resolution.name,
~version=Version.Opam(version),
manifest,
--- a/test-e2e/esy-install/opam.test.js
+++ b/test-e2e/esy-install/opam.test.js
@@ -60,17 +60,38 @@ describe('opam available filter tests', () => {
esy: {},
});
- await defineOpamPackageOfFixture(p, 'pkg1', '1.0.0', 'pkg1', undefined, `os = "macos"`);
- await defineOpamPackageOfFixture(p, 'pkg2', '1.0.0', 'pkg2', undefined, `os = "win32"`);
- await defineOpamPackageOfFixture(p, 'pkg3', '1.0.0', 'pkg3', undefined, `os = "linux"`);
+ await defineOpamPackageOfFixture(p, 'dep-macos', '1.0.0', 'depMacos', undefined, `os = "macos"`);
+ await defineOpamPackageOfFixture(p, 'dep-win32', '1.0.0', 'depWin32', undefined, `os = "win32"`);
+ await defineOpamPackageOfFixture(p, 'dep-linux', '1.0.0', 'depLinux', undefined, `os = "linux"`);
+ await p.defineOpamPackageOfFixture(
+ {
+ name: "pkg-main",
+ version: "1.0.0",
+ opam: outdent`
+ opam-version: "2.0"
+ depends: [
+ "dep-macos" {= version & os = "macos"}
+ "dep-win32" {= version & os = "win32"}
+ "dep-linux" {= version & os = "linux"}
+ ]
+ build: [
+ ${helpers.buildCommandInOpam("pkg-main.js")}
+ ["cp" "pkg-main.cmd" "%{bin}%/pkg-main.cmd"]
+ ["cp" "pkg-main.js" "%{bin}%/pkg-main.js"]
+ ]
+ `,
+ },
+ [
+ helpers.dummyExecutable("pkg-main"),
+ ],
+ );
await p.fixture(
helpers.packageJson({
name: 'root',
+ available: [["windows", "x86_64"], ["linux", "x86_64"], ["darwin", "arm64"],["darwin", "x86_64"]],
dependencies: {
- '@opam/pkg1': '*',
- '@opam/pkg2': '*',
- '@opam/pkg3': '*',
+ '@opam/pkg-main': '*',
}
}),
);
@@ -78,35 +99,203 @@ describe('opam available filter tests', () => {
await p.esy();
-
// const solution = await helpers.readSolution(p.projectPath);
// expect(JSON.stringify(solution, null, 2)).toEqual(''); // Just a wait to print lock file
if (process.platform === 'darwin') {
{
- const { stdout } = await p.esy('x pkg1.cmd');
- expect(stdout.trim()).toEqual('__pkg1__');
+ const { stdout } = await p.esy('x depMacos.cmd');
+ expect(stdout.trim()).toEqual('__depMacos__');
+ }
+ await expect(p.esy('x depLinux.cmd')).rejects.toThrow();
+ await expect(p.esy('x depWin32.cmd')).rejects.toThrow();
+ }
+
+ if (process.platform === 'linux') {
+ {
+ const { stdout } = await p.esy('x depLinux.cmd');
+ expect(stdout.trim()).toEqual('__depLinux__');
+ }
+ await expect(p.esy('x depWin32.cmd')).rejects.toThrow();
+ await expect(p.esy('x depMacos.cmd')).rejects.toThrow();
+ }
+
+ if (process.platform === 'win32') {
+ {
+ const { stdout } = await p.esy('x depWin32.cmd');
+ expect(stdout.trim()).toEqual('__depWin32__');
+ }
+ await expect(p.esy('x depMacos.cmd')).rejects.toThrow();
+ await expect(p.esy('x depLinux.cmd')).rejects.toThrow();
+ }
+ });
+
+ it('ensure default solution works on the platforms when only one dependency is platform specific', async () => {
+ const p = await helpers.createTestSandbox();
+
+ await p.defineNpmPackage({
+ name: '@esy-ocaml/substs',
+ version: '0.0.0',
+ esy: {},
+ });
+
+ await defineOpamPackageOfFixture(p, 'dep-macos', '1.0.0', 'depMacos', undefined, `os = "macos"`);
+ await defineOpamPackageOfFixture(p, 'dep-win32', '1.0.0', 'depWin32', undefined, `true`);
+ await defineOpamPackageOfFixture(p, 'dep-linux', '1.0.0', 'depLinux', undefined, `true`);
+
+ await p.defineOpamPackageOfFixture(
+ {
+ name: "pkg-main",
+ version: "1.0.0",
+ opam: outdent`
+ opam-version: "2.0"
+ depends: [
+ "dep-macos" {= version & os = "macos"}
+ "dep-win32"
+ "dep-linux"
+ ]
+ build: [
+ ${helpers.buildCommandInOpam("pkg-main.js")}
+ ["cp" "pkg-main.cmd" "%{bin}%/pkg-main.cmd"]
+ ["cp" "pkg-main.js" "%{bin}%/pkg-main.js"]
+ ]
+ `,
+ },
+ [
+ helpers.dummyExecutable("pkg-main"),
+ ],
+ );
+
+ await p.fixture(
+ helpers.packageJson({
+ name: 'root',
+ dependencies: {
+ '@opam/pkg-main': '*',
+ }
+ }),
+ );
+
+ await p.esy();
+
+ // Since the multiplat solver config only guarantees solutions for windows-x64,
+ // For other platforms, default solution is used.
+ // Which means, unavailable packages could become available there
+ if (process.platform === 'darwin') {
+ {
+ const { stdout } = await p.esy('x depMacos.cmd');
+ expect(stdout.trim()).toEqual('__depMacos__');
+ }
+ {
+ const { stdout } = await p.esy('x depLinux.cmd');
+ expect(stdout.trim()).toEqual('__depLinux__');
+ }
+ {
+ const { stdout } = await p.esy('x depWin32.cmd');
+ expect(stdout.trim()).toEqual('__depWin32__');
+ }
+ }
+
+ if (process.platform === 'linux') {
+ {
+ const { stdout } = await p.esy('x depLinux.cmd');
+ expect(stdout.trim()).toEqual('__depLinux__');
+ }
+ {
+ const { stdout } = await p.esy('x depWin32.cmd');
+ expect(stdout.trim()).toEqual('__depWin32__');
+ }
+ await expect(p.esy('x depMacos.cmd')).rejects.toThrow();
+ }
+
+ if (process.platform === 'win32') {
+ {
+ const { stdout } = await p.esy('x depLinux.cmd');
+ expect(stdout.trim()).toEqual('__depLinux__');
+ }
+ {
+ const { stdout } = await p.esy('x depWin32.cmd');
+ expect(stdout.trim()).toEqual('__depWin32__');
+ }
+ await expect(p.esy('x depMacos.cmd')).rejects.toThrow();
+ }
+ });
+
+ it('ensure package.json/esy.json config is respected', async () => {
+ const p = await helpers.createTestSandbox();
+
+ await p.defineNpmPackage({
+ name: '@esy-ocaml/substs',
+ version: '0.0.0',
+ esy: {},
+ });
+
+ await defineOpamPackageOfFixture(p, 'dep-macos', '1.0.0', 'depMacos', undefined, `os = "macos"`);
+ await defineOpamPackageOfFixture(p, 'dep-win32', '1.0.0', 'depWin32', undefined, `os = "win32"`);
+ await defineOpamPackageOfFixture(p, 'dep-linux', '1.0.0', 'depLinux', undefined, `os = "linux"`);
+
+ await p.defineOpamPackageOfFixture(
+ {
+ name: "pkg-main",
+ version: "1.0.0",
+ opam: outdent`
+ opam-version: "2.0"
+ depends: [
+ "dep-macos" {= version & os = "macos"}
+ "dep-win32" {= version & os = "win32"}
+ "dep-linux" {= version & os = "linux"}
+ ]
+ build: [
+ ${helpers.buildCommandInOpam("pkg-main.js")}
+ ["cp" "pkg-main.cmd" "%{bin}%/pkg-main.cmd"]
+ ["cp" "pkg-main.js" "%{bin}%/pkg-main.js"]
+ ]
+ `,
+ },
+ [
+ helpers.dummyExecutable("pkg-main"),
+ ],
+ );
+
+ await p.fixture(
+ helpers.packageJson({
+ name: 'root',
+ available: [["windows", "x86_64"]],
+ dependencies: {
+ '@opam/pkg-main': '*',
+ }
+ }),
+ );
+
+ await p.esy();
+
+ // Since the multiplat solver config only guarantees solutions for windows-x64,
+ // For other platforms, default solution is used.
+ // Which means, unavailable packages could become available there
+ if (process.platform === 'darwin') {
+ {
+ const { stdout } = await p.esy('x depMacos.cmd');
+ expect(stdout.trim()).toEqual('__depMacos__');
}
- await expect(p.esy('x pkg2.cmd')).rejects.toThrow();
- await expect(p.esy('x pkg3.cmd')).rejects.toThrow();
+ await expect(p.esy('x depLinux.cmd'));
+ await expect(p.esy('x depWin32.cmd'));
}
if (process.platform === 'linux') {
{
- const { stdout } = await p.esy('x pkg3.cmd');
- expect(stdout.trim()).toEqual('__pkg3__');
+ const { stdout } = await p.esy('x depLinux.cmd');
+ expect(stdout.trim()).toEqual('__depLinux__');
}
- await expect(p.esy('x pkg1.cmd')).rejects.toThrow();
- await expect(p.esy('x pkg2.cmd')).rejects.toThrow();
+ await expect(p.esy('x depWin32.cmd'));
+ await expect(p.esy('x depMacos.cmd'));
}
if (process.platform === 'win32') {
{
- const { stdout } = await p.esy('x pkg2.cmd');
- expect(stdout.trim()).toEqual('__pkg2__');
+ const { stdout } = await p.esy('x depWin32.cmd');
+ expect(stdout.trim()).toEqual('__depWin32__');
}
- await expect(p.esy('x pkg1.cmd')).rejects.toThrow();
- await expect(p.esy('x pkg3.cmd')).rejects.toThrow();
+ await expect(p.esy('x depMacos.cmd')).rejects.toThrow();
+ await expect(p.esy('x depLinux.cmd')).rejects.toThrow();
}
});
});
Remove redundant tasks and better progress reporting
diff --git a/esy-fetch/SolutionLock.re b/esy-fetch/SolutionLock.re
index 18d7ffda2..22f494ef4 100644
--- a/esy-fetch/SolutionLock.re
+++ b/esy-fetch/SolutionLock.re
@@ -52,7 +52,7 @@ and node = {
installConfig: InstallConfig.t,
[@default []]
extraSources: list(ExtraSource.t),
- available: [@default None] option(string),
+ available: option(AvailablePlatforms.t),
};
let indexFilename = "index.json";
@@ -232,7 +232,7 @@ let writePackage = (sandbox, pkg: Package.t, gitUsername, gitPassword) => {
devDependencies: pkg.devDependencies,
installConfig: pkg.installConfig,
extraSources: pkg.extraSources,
- available: pkg.available,
+ available: Some(pkg.available),
});
};
@@ -250,6 +250,49 @@ let readPackage = (sandbox, node: node) => {
};
let* overrides = readOverrides(sandbox, node.overrides);
+
+ let* available =
+ switch (node.available) {
+ | Some(available) => RunAsync.return @@ available
+ | None =>
+ switch (node.source) {
+ | Link({path, manifest: Some((Opam, filename)), kind: _}) =>
+ let* opamfile = {
+ let path =
+ DistPath.(path / filename |> toPath(sandbox.Sandbox.spec.path));
+ let* data = Fs.readFile(path);
+ let filename =
+ OpamFile.make(OpamFilename.of_string(Path.show(path)));
+ try(return(OpamFile.OPAM.read_from_string(~filename, data))) {
+ | Failure(msg) =>
+ errorf("error parsing opam metadata %a: %s", Path.pp, path, msg)
+ | _ => error("error parsing opam metadata")
+ };
+ };
+ let availableFilter = OpamFile.OPAM.available(opamfile);
+ RunAsync.return @@
+ AvailablePlatforms.filter(availableFilter, AvailablePlatforms.default);
+
+ | Link(_) => RunAsync.return @@ AvailablePlatforms.default
+ | Install({source: _, opam: None}) =>
+ RunAsync.return @@ AvailablePlatforms.default
+ | Install({source: _, opam: Some(opam)}) =>
+ let* opamfile = {
+ let path = Path.(opam.path / "opam");
+ let* data = Fs.readFile(path);
+ let filename =
+ OpamFile.make(OpamFilename.of_string(Path.show(path)));
+ try(return(OpamFile.OPAM.read_from_string(~filename, data))) {
+ | Failure(msg) =>
+ errorf("error parsing opam metadata %a: %s", Path.pp, path, msg)
+ | _ => error("error parsing opam metadata")
+ };
+ };
+ let availableFilter = OpamFile.OPAM.available(opamfile);
+ RunAsync.return @@
+ AvailablePlatforms.filter(availableFilter, AvailablePlatforms.default);
+ }
+ };
return({
Package.id: node.id,
name: node.name,
@@ -260,7 +303,7 @@ let readPackage = (sandbox, node: node) => {
devDependencies: node.devDependencies,
installConfig: node.installConfig,
extraSources: node.extraSources,
- available: node.available,
+ available,
});
};
I'm @ManasJayanth on x.com.
Email: manas [at] dining-philosophers [dot] in
These slides can be found at slides.manas-jayanth.com