Skip to content

Commit d8f8665

Browse files
committed
Free the npm package from third party dependencies
And support Linux ARM and macOS ARM.
1 parent 047d502 commit d8f8665

File tree

18 files changed

+278
-161
lines changed

18 files changed

+278
-161
lines changed

installers/npm/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
node_modules/
2+
packages/*/elm
3+
packages/*/elm.exe

installers/npm/PUBLISHING.md

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,95 @@
22

33
Here's how to update the `npm` installer.
44

5+
## 0. Overview
6+
7+
- There is one _main npm package_ called `elm`.
8+
- Then there is one _binary npm package_ for each platform, called for example `@evancz/elm_darwin_arm64`.
9+
10+
The binary packages declare which OS and CPU they are compatible with. For example:
11+
12+
```json
13+
"os": [ "darwin" ],
14+
"cpu": [ "arm64" ]
15+
```
16+
17+
The main npm package depend on the binary packages via [optional dependencies](https://docs.npmjs.com/cli/v9/configuring-npm/package-json#optionaldependencies):
18+
19+
```json
20+
"@evancz/elm_darwin_arm64": "0.19.1-0",
21+
"@evancz/elm_darwin_x64": "0.19.1-0",
22+
"@evancz/elm_linux_arm64": "0.19.1-0",
23+
...
24+
```
25+
26+
When installing, `npm` fetches the metadata for all the optional dependencies and only installs the one with a matching OS and CPU. If none of them match, `npm` still considers the install successful. However, the main npm package contains an install script that gives a helpful error.
27+
528

629
## 1. GitHub Release
730

831
Create a [GitHub Release](https://github.com/elm/compiler/releases) with the following files:
932

1033
1. `binary-for-mac-64-bit.gz`
11-
2. `binary-for-windows-64-bit.gz`
34+
2. `binary-for-mac-arm-64-bit.gz`
1235
3. `binary-for-linux-64-bit.gz`
36+
4. `binary-for-linux-arm-64-bit.gz`
37+
5. `binary-for-windows-64-bit.gz`
1338

1439
Create each of these by running the `elm` executable for each platform through `gzip elm`.
1540

1641

17-
## 2. Try a beta release
42+
## 2. Put the binaries in place
43+
44+
Put the above files at:
45+
46+
1. `packages/elm_darwin_arm64/elm`
47+
2. `packages/elm_darwin_x64/elm`
48+
3. `packages/elm_linux_x64/elm`
49+
4. `packages/elm_linux_arm64/elm`
50+
5. `packages/elm_win32_x64/elm.exe` (Note the `.exe` file extension!)
51+
52+
(They are ignored by git.)
53+
54+
55+
## 3. Publish the binary packages
56+
57+
Repeat this for all the packages mentioned in the previous section. This uses `packages/elm_darwin_arm64` as an example.
58+
59+
1. Go to the folder: `cd packages/elm_darwin_arm64`
60+
2. Double-check that you put the right binary in the right package: `file elm`
61+
3. Double-check that the file is executable: `ls -l elm`
62+
4. In `package.json` of the binary package, bump the version for example to `"0.19.1-2"`.
63+
5. In `package.json` of the main npm package, update `"optionalDependencies"` to point to the bumped version. For example: `"@evancz/elm_darwin_arm64": "0.19.1-2"`
64+
6. Publish the package: `npm publish --access=public`
65+
66+
`--access=public` is needed because scoped packages are private by default.
67+
68+
<details>
69+
<summary>Notes about the versions of the binary packages</summary>
70+
71+
- End users never have to think about them. They only need to think about the version of the main npm package.
72+
73+
- The binary packages can have different versions. One can have `"0.19.1-0"` while another is at `"0.19.1-1"`. This is useful if you mess up publishing one platform: Then you can bump just that one and re-release, instead of having to re-release _all_ platforms.
74+
75+
- The version of the main npm package is not related to the versions of the binary packages – they’re all independent. So the main npm package can be at `"0.19.1-6"` while the binary packages have suffixes like `-0`, `-1` and `-9`. (They all share the `0.19.1` prefix though to make things more understandable!)
76+
77+
- The main npm package pins the versions of the binary packages _exactly_ – no version ranges.
78+
- This means that installing `[email protected]` installs the exact same bytes in two years as today.
79+
- The `package.json` of each binary package says which OS and CPU it works for. `binary.js` in the main npm package has code that deals with OS and CPU too, so the main npm package needs to install binary packages with known OS and CPU declarations.
80+
81+
- There is no need to use `beta` suffixes for the binary packages. Just bump the number suffix and point to it in a beta release of the main npm package. As mentioned above:
82+
- Already published versions of the main npm package depend on exact versions of the binary packages, so they won’t accidentally start downloading beta versions.
83+
- End users only see the version of the main npm package.
84+
85+
</details>
86+
87+
88+
## 4. Try a beta release
1889

1990
In `package.json`, bump the version to `"0.19.2-beta"`.
2091

92+
Double-check that `"optionalDependencies"` is in sync with the binary packages.
93+
2194
```bash
2295
npm publish --tag beta
2396
```
@@ -34,7 +107,7 @@ The `latest` tag should not be changed, and there should be an additional `beta`
34107
Try this on Windows, Linux, and Mac.
35108

36109

37-
## 3. Publish final release
110+
## 5. Publish final release
38111

39112
Remove the `-beta` suffix from the version in `package.json`. Then run:
40113

@@ -43,7 +116,7 @@ npm publish
43116
```
44117

45118

46-
## 4. Tag the `latest-0.19.1` version
119+
## 6. Tag the `latest-0.19.1` version
47120

48121
Many compiler releases have needed multiple `npm` publications. Maybe something does not work on Windows or some dependency becomes insecure. Normal `npm` problems.
49122

installers/npm/bin/elm

Lines changed: 7 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,28 @@
11
#!/usr/bin/env node
22

33
var child_process = require('child_process');
4-
var path = require('path');
5-
var fs = require('fs');
64

75

86
// Some npm users enable --ignore-scripts (a good security measure) so
97
// they do not run the post-install hook and install.js does not run.
108
// Instead they will run this script.
119
//
12-
// On Mac and Linux, we download the elm executable into the exact same
10+
// On Mac and Linux, we hard link the elm executable into the exact same
1311
// location as this file. Since npm uses symlinks on these platforms,
1412
// that means that the first run will invoke this file and subsequent
1513
// runs will call the elm binary directly.
1614
//
17-
// On Windows, we must download a file named elm.exe for it to run properly.
15+
// On Windows, our binary file must be named elm.exe for it to run properly.
1816
// Instead of symlinks, npm creates two files:
1917
//
2018
// - node_modules/.bin/elm (a bash file)
2119
// - node_modules/.bin/elm.cmd (a batch file)
2220
//
2321
// Both files specifically invoke `node` to run the file listed at package.bin,
24-
// so there is no way around instantiating node for no reason on Windows. So
25-
// the existsSync check is needed so that it is not downloaded more than once.
22+
// so there is no way around instantiating node for no reason on Windows.
2623

2724

28-
// figure out where to put the binary (calls path.resolve() to get path separators right on Windows)
29-
//
30-
var binaryPath = path.resolve(__dirname, 'elm') + (process.platform === 'win32' ? '.exe' : '');
31-
32-
// Run the command directly if possible, otherwise download and then run.
33-
// This check is important for Windows where this file will be run all the time.
34-
//
35-
if (process.platform === 'win32')
36-
{
37-
fs.existsSync(binaryPath)
38-
? runCommand()
39-
: require('../download.js')(runCommand);
40-
}
41-
else
42-
{
43-
require('../download.js')(runCommand);
44-
}
45-
46-
47-
function runCommand()
48-
{
49-
// Need double quotes and { shell: true } when there are spaces in the path on windows:
50-
// https://github.com/nodejs/node/issues/7367#issuecomment-229721296
51-
child_process
52-
.spawn('"' + binaryPath + '"', process.argv.slice(2), { stdio: 'inherit', shell: true })
53-
.on('exit', process.exit);
54-
}
25+
var binaryPath = require('../binary.js')();
26+
child_process
27+
.spawn(binaryPath, process.argv.slice(2), { stdio: 'inherit' })
28+
.on('exit', process.exit);

installers/npm/binary.js

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
var fs = require('fs');
2+
var package = require('./package.json');
3+
var path = require('path');
4+
5+
6+
7+
// MAIN
8+
//
9+
// This function is used by install.js and by the bin/elm backup that gets
10+
// called when --ignore-scripts is enabled.
11+
12+
13+
module.exports = function()
14+
{
15+
// figure out package of binary
16+
var version = package.version.replace(/^(\d+\.\d+\.\d+).*$/, '$1'); // turn '1.2.3-alpha' into '1.2.3'
17+
var subPackageName = '@evancz/elm_' + process.platform + '_' + process.arch;
18+
19+
verifyPlatform(version, subPackageName);
20+
21+
var fileName = process.platform === 'win32' ? 'elm.exe' : 'elm';
22+
23+
try
24+
{
25+
var subBinaryPath = require.resolve(subPackageName + '/' + fileName);
26+
}
27+
catch (error)
28+
{
29+
if (error && error.code === 'MODULE_NOT_FOUND')
30+
{
31+
exitFailure(version, missingSubPackageHelp(subPackageName));
32+
}
33+
else
34+
{
35+
exitFailure(version, 'I had trouble requiring the binary package for your platform (' + subPackageName + '):\n\n' + error);
36+
}
37+
}
38+
39+
// as mentioned in bin/elm we cannot do any optimizations on Windows
40+
if (process.platform === 'win32')
41+
{
42+
return subBinaryPath;
43+
}
44+
45+
// figure out where to put the binary
46+
var binaryPath = path.resolve(__dirname, package.bin.elm);
47+
var tmpPath = binaryPath + '.tmp';
48+
49+
// optimize by replacing the JS bin/elm with the native binary directly
50+
try
51+
{
52+
// atomically replace the file with a hard link to the binary
53+
fs.linkSync(subBinaryPath, tmpPath);
54+
fs.renameSync(tmpPath, binaryPath);
55+
}
56+
catch (error)
57+
{
58+
exitFailure(version, 'I had some trouble writing file to disk. It is saying:\n\n' + error);
59+
}
60+
61+
return binaryPath;
62+
}
63+
64+
65+
66+
// VERIFY PLATFORM
67+
68+
69+
function verifyPlatform(version, subPackageName)
70+
{
71+
if (subPackageName in package.optionalDependencies) return;
72+
73+
var situation = process.platform + '_' + process.arch;
74+
console.error(
75+
'-- ERROR -----------------------------------------------------------------------\n\n'
76+
+ 'I am detecting that your computer (' + situation + ') may not be compatible with any\n'
77+
+ 'of the official pre-built binaries.\n\n'
78+
+ 'I recommend against using the npm installer for your situation. Check out the\n'
79+
+ 'alternative installers at https://github.com/elm/compiler/releases/tag/' + version + '\n'
80+
+ 'to see if there is something that will work better for you.\n\n'
81+
+ 'From there I recommend asking for guidance on Slack or Discourse to find someone\n'
82+
+ 'who can help with your specific situation.\n\n'
83+
+ '--------------------------------------------------------------------------------\n'
84+
);
85+
process.exit(1);
86+
}
87+
88+
89+
90+
// EXIT FAILURE
91+
92+
93+
function exitFailure(version, message)
94+
{
95+
console.error(
96+
'-- ERROR -----------------------------------------------------------------------\n\n'
97+
+ message
98+
+ '\n\nNOTE: You can avoid npm entirely by downloading directly from:\n'
99+
+ 'https://github.com/elm/compiler/releases/tag/' + version + '\n'
100+
+ 'All this package does is distributing a file from there.\n\n'
101+
+ '--------------------------------------------------------------------------------\n'
102+
);
103+
process.exit(1);
104+
}
105+
106+
107+
108+
// MISSING SUB PACKAGE HELP
109+
110+
111+
function missingSubPackageHelp(subPackageName)
112+
{
113+
return (
114+
'I support your platform, but I could not find the binary package (' + subPackageName + ') for it!\n\n'
115+
+ 'This can happen if you use the "--omit=optional" (or "--no-optional") npm flag.\n'
116+
+ 'The "optionalDependencies" package.json feature is used by Elm to install the correct\n'
117+
+ 'binary executable for your current platform. Remove that flag to use Elm.\n\n'
118+
+ 'This can also happen if the "node_modules" folder was copied between two operating systems\n'
119+
+ 'that need different binaries - including "virtual" operating systems like Docker and WSL.\n'
120+
+ 'If so, try installing with npm rather than copying "node_modules".'
121+
);
122+
}

0 commit comments

Comments
 (0)