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

fix: patchShebangs in nonexecutable bin files (#106) #107

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

milahu
Copy link
Contributor

@milahu milahu commented Sep 14, 2021

close #106

internal.nix Outdated
@@ -331,6 +331,9 @@ rec {

${preInstallLinkCommands}

cat package.json | jq -r '. | select(.bin != null) | .bin | values[]' | while read binTarget; do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the package.json guranteed to be there?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also pass the package.json as 2nd argument to cat and avoid the cat invocation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the package.json guranteed to be there?

this is called for every npm package, these are required to have a package.json

We could also pass the package.json as 2nd argument to jq

its prettier this way ; ) but yes, lets save some nanoseconds and call only jq

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the package.json guranteed to be there?

package.json *should* always be there
and if its missing, we should throw a fatal error

currently, the script continues and throws later

i managed to hack together a broken node_modules with missing submodules ...
in that case i got

> [email protected] preinstall /build/node_modules
> /build/node_modules/.hooks/preinstall
jq: error: Could not open file package.json: No such file or directory
patching script interpreter paths in .

but package.json should be there

$ tar tf /nix/store/gll14kwxsz9ggfhngafafh64rv4a02fr-semver-5.7.1.tgz
package/LICENSE
package/bin/semver
package/range.bnf
package/semver.js
package/package.json
package/CHANGELOG.md
package/README.md

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also encountered a similar problem, where jq was complaining there was no "package.json" even though it was fine accesing it with cat and ls when I was working on #108. I think using cat <file> | jq ... was mitigating the problem but then I stopped encountering it after fixing a some other part of the bash code. I still have no idea why that happened/happens.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

package.json should always be there
and if its missing, we should throw a fatal error

no. there are packages without a package.json,
for example type definitions (*.d.ts) or header files (*.h)

pnpm/pnpm#3728

internal.nix Outdated
@@ -331,6 +331,9 @@ rec {

${preInstallLinkCommands}

jq -r '. | select(.bin != null) | .bin | values[]' package.json | while read binTarget; do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bin field could also have string as the value if there is a single executable1

{
  "name": "my-program",
  "version": "1.2.5",
  "bin": "./path/to/program"
}

Currently, this jq expression doesn't seem to handle that case. I used something like 'select(has("bin")) | .bin | if type == "string" then . else .[] end' to handle that case. But this is the first time I wrote jq expression, so it might be far from the optimal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not really into maintaining jq expression.. I've done it once or twice but generally that isn't something that i want to maintain beyond a certain level of complexity.

How much effort would it be to rewrite this in a short nodejs script (without dependencies)? The conversaion so far makes me fear about any additional special cases that we will have to add in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jq is a standard tool for json manipulation, node feels like overkill for this

The conversaion so far makes me fear about any additional special cases that we will have to add in the future.

heh, not really ... there are three cases

the bin field ...

  1. does not exist
  2. is a string
  3. is an object

all of these are handled

https://docs.npmjs.com/cli/v7/configuring-npm/package-json#bin

internal.nix Outdated
@@ -331,6 +331,9 @@ rec {

${preInstallLinkCommands}

jq -r 'select(.bin != null) | .bin | if type == "string" then . else values[] end' package.json | while read binTarget; do
chmod +x "$binTarget" # patchShebangs will only patch executable files
Copy link
Contributor Author

@milahu milahu Sep 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this should throw if binTarget is not found

in my case:

npmlock2nix.shell {
  src = /tmp/package;
}

the root package.json (/tmp/package/package.json) has a bin,
but npmlock2nix will copy only the *.json files to /build, so binTarget is not found

or rather, npmlock2nix should ignore bin's for the root package (/tmp/package)

Copy link
Contributor Author

@milahu milahu Sep 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

npmlock2nix should ignore bin's for the root package (/tmp/package)

even more, npmlock2nix.node_modules should ignore install scripts for the root package,
since for the root package, only the *.json files are available,
and install scripts may require other files

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that so? I am not entirely sure that is true. Right now we do an npm install in the node_modules build and that does what we need (most of the time?).

Copy link
Contributor Author

@milahu milahu Sep 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider this package.json

{
  "name": "root-package",
  "version": "1",
  "scripts": {
    "postinstall": "node some/script.js"
  },
  "dependencies": {
    "foo": "1.2.3"
  }
}

npmlock2nix has only two files for the root-package: package.json and package-lock.json

these are symlinked to /build in npmlock2nix.node_modules

npmlock2nix/internal.nix

Lines 365 to 368 in 33eb330

postPatch = ''
ln -sf ${patchedLockfile (sourceHashFunc githubSourceHashMap) packageLockJson} package-lock.json
ln -sf ${patchedPackagefile (sourceHashFunc githubSourceHashMap) packageJson} package.json
'';

so some/script.js is NOT available in /build, and npm install would fail

the job of npmlock2nix.node_modules is only to populate node_modules

internal.nix Outdated Show resolved Hide resolved
internal.nix Outdated
@@ -283,6 +285,22 @@ rec {
else
throw "sourceHashFunc: spec.type '${spec.type}' is not supported. Supported types: 'github'";

runInstallScriptsForRootPackage = ''
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could move this to a bash function, similar to buildPhase in stdenv.mkDerivation

@infinisil
Copy link
Contributor

With NPM 7, hook scripts, which this PR relies on, are not supported anymore, see also #110. However with #151 it will become possible to patch the sources of arbitrary packages, which should cover mostly the same use cases as hook scripts. @milahu Could you try out #151 and see if that can be used for your use cases?

@milahu
Copy link
Contributor Author

milahu commented Mar 19, 2022

Could you try out #151 and see if that can be used for your use cases?

naah, thats too general
my life is too short to add postPatch = "patchShebangs bin"; to every broken package

chmod +x and patchShebangs should run automatically on all the bin entries of a package

@milahu
Copy link
Contributor Author

milahu commented Mar 19, 2022

With NPM 7, hook scripts, which this PR relies on, are not supported anymore

i guess you're confusing ...

Comment on lines +356 to +364
# patchShebangs will only patch executable files
if [[ "$(pwd)" != "/build" ]]; then # ignore the root package. bin entries only make sense for dependencies
jq -r 'select(.bin != null) | .bin | if type == "string" then . else values[] end' package.json | while read binTarget; do
if [[ ! -x "$binTarget" ]]; then
echo "make binary executable: $(pwd)/$binTarget"
chmod +x "$binTarget" || exit 1 # on error, throw ELIFECYCLE
fi
done
fi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@milahu At least this part of this PR relies on hook scripts, see the node_modules buildPhase

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aah yes, thanks
so this is part of the node_modules/.hooks/prepare script
which makes this PR useless for npmv7

which brings me back to #110 (comment)
→ lets implement our own npm install
instead of producing workarounds for a npm-without-hooks

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think being able to patch sources is useful on its own, and it's a relatively easy and reasonable workaround for the hook problem. Patching sources is also very well-known by Nix people already.

Implementing an entirely separate npm install feels a bit over the top to me, requiring maintenance, the potential for bugs, having to synchronize versions, adding a lot of complexity. If such global hook scripts are really needed for use cases like ours, then I think it's better to talk to npm upstream instead, urging them to add a reasonable replacement. And whether that happens or not, patching of sources will work as a workaround in any case.

Copy link
Contributor Author

@milahu milahu Mar 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think being able to patch sources is useful on its own

yes, of course. like patchPhase in stdenv.mkDerivation

for example, npm install puppeteer
will download a full chromium binary
to node_modules/puppeteer/.local-chromium/

in nix, we can say

{
  pname = "puppeteer";
  patchPhase = ''
    <package.json jq '.scripts.install = ""' | sponge package.json
  '';
  postInstall = ''
    d=$out/node_modules/puppeteer/.local-chromium/linux-970485
    mkdir -p $d
    ln -s ${puppeteer-chromium} $d/chrome-linux
  '';

related: patching json files in place

Implementing an entirely separate npm install feels a bit over the top to me, requiring maintenance, the potential for bugs, having to synchronize versions, adding a lot of complexity.

a working prototype is implemented in my pnpm-install-only

the complexity is limited by

  1. focussing only on npm install. its not rocket science:
    • verify tar.gz files
    • unpack tar.gz files
    • run lifecycle scripts: must be "bottom up", so that child-dependencies are installed
    • populate node_modules/.bin
  2. using an external library (snyk-nodejs-lockfile-parser) to parse lockfiles. bonus: this supports different lockfile formats: npm-shrinkwrap.json package-lock.json yarn.lock. pnpm-lock.yaml is supported only via workaround, but it "should" be easy to implement. (seems everybody is a frontend dev these days ...)
  3. using npm run to run lifecycle scripts. no need to reimplement that part of npm

another bonus: this custom npm install allows to use the nix store as a machine-level store for node packages, see my nodejs-hide-symlinks

also that project is a working prototype. its just not popular, because of ... reasons. more human reasons than technical reasons

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

bug: patchShebangs fails when files are not executable
4 participants