Skip to content

Commit

Permalink
fix: add option to use symlinks with the npm plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
bepri committed Feb 4, 2025
1 parent a04f571 commit ad8fa4c
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 58 deletions.
18 changes: 17 additions & 1 deletion craft_parts/plugins/npm_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class NpmPluginProperties(PluginProperties, frozen=True):

# part properties required by the plugin
npm_include_node: bool = False
npm_set_symlinks: bool = False
npm_node_version: str | None = None
source: str # pyright: ignore[reportGeneralTypeIssues]

Expand Down Expand Up @@ -273,6 +274,18 @@ def get_build_commands(self) -> list[str]:
"""Return a list of commands to run during the build step."""
cmd = []
options = cast(NpmPluginProperties, self._options)

if options.npm_set_symlinks:
cmd += [
dedent(
"""\
for dir in lib bin; do
mkdir -p ${CRAFT_PART_INSTALL}/usr/${dir}
ln -s usr/${dir} ${CRAFT_PART_INSTALL}/${dir}
done
"""
)
]
if options.npm_include_node:
arch = self._get_architecture()
version = options.npm_node_version
Expand Down Expand Up @@ -301,11 +314,12 @@ def get_build_commands(self) -> list[str]:
cmd += [
dedent(
f"""\
tar -xzf "{self._node_binary_path}" -C "${{CRAFT_PART_INSTALL}}/" \
tar -hxzf "{self._node_binary_path}" -C "${{CRAFT_PART_INSTALL}}/" \
--no-same-owner --strip-components=1
"""
),
]

cmd += [
dedent(
"""\
Expand All @@ -319,4 +333,6 @@ def get_build_commands(self) -> list[str]:
"""
)
]


return cmd
9 changes: 9 additions & 0 deletions docs/common/craft-parts/reference/plugins/npm_plugin.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ for the target architecture.
from nearly a decade ago, consider
migrating to the modern Node.js runtime.

npm-set-symlinks
~~~~~~~~~~~~~~~~
**Type:** boolean
**Default:** False

When set to ``true``, the plugin will create symbolic links for the install
directories used by npm. This behavior can avoid "file permission" conflicts
that may arise when using the npm plugin alongside certain packages.

Examples
--------

Expand Down
9 changes: 9 additions & 0 deletions docs/reference/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
Changelog
*********

X.X.X (2025-MM-DD)
------------------

Bug fixes:

- Adds the ``npm-set-symlinks`` option to the npm plugin. When set to ``true``,
this option fixes an error regarding filename conflicts when using the
:ref:`npm plugin<craft_parts_npm_plugin>`.

2.5.0 (2025-01-30)
------------------

Expand Down
108 changes: 51 additions & 57 deletions tests/integration/plugins/test_npm.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,11 @@
import yaml
from craft_parts import LifecycleManager, Step

import pytest

def test_npm_plugin(new_dir, partitions):
parts_yaml = textwrap.dedent(
"""\
parts:
foo:
plugin: npm
source: .
"""
)
parts = yaml.safe_load(parts_yaml)

Path("hello.js").write_text(
@pytest.fixture
def node_package(tmp_path) -> None:
(tmp_path / "hello.js").write_text(
textwrap.dedent(
"""\
#!/usr/bin/env node
Expand All @@ -43,7 +35,7 @@ def test_npm_plugin(new_dir, partitions):
)
)

Path("package.json").write_text(
(tmp_path / "package.json").write_text(
textwrap.dedent(
"""\
{
Expand All @@ -63,7 +55,7 @@ def test_npm_plugin(new_dir, partitions):
)
)

Path("package-lock.json").write_text(
(tmp_path / "package-lock.json").write_text(
textwrap.dedent(
"""\
{
Expand All @@ -75,6 +67,18 @@ def test_npm_plugin(new_dir, partitions):
)
)

@pytest.mark.usefixtures("node_package")
def test_npm_plugin(new_dir, partitions):
parts_yaml = textwrap.dedent(
"""\
parts:
foo:
plugin: npm
source: .
"""
)
parts = yaml.safe_load(parts_yaml)

lifecycle = LifecycleManager(
parts,
application_name="test_npm_plugin",
Expand All @@ -93,7 +97,7 @@ def test_npm_plugin(new_dir, partitions):

assert Path(lifecycle.project_info.prime_dir, "bin", "node").exists() is False


@pytest.mark.usefixtures("node_package")
def test_npm_plugin_include_node(new_dir, partitions):
parts_yaml = textwrap.dedent(
"""\
Expand All @@ -107,48 +111,6 @@ def test_npm_plugin_include_node(new_dir, partitions):
)
parts = yaml.safe_load(parts_yaml)

Path("hello.js").write_text(
textwrap.dedent(
"""\
#!/usr/bin/env node
console.log('hello world');
"""
)
)

Path("package.json").write_text(
textwrap.dedent(
"""\
{
"name": "npm-hello",
"version": "1.0.0",
"description": "Testing grounds for snapcraft integration tests",
"bin": {
"npm-hello": "hello.js"
},
"scripts": {
"npm-hello": "echo 'Error: no test specified' && exit 1"
},
"author": "",
"license": "GPL-3.0"
}
"""
)
)

Path("package-lock.json").write_text(
textwrap.dedent(
"""\
{
"name": "npm-hello",
"version": "1.0.0",
"lockfileVersion": 1
}
"""
)
)

lifecycle = LifecycleManager(
parts,
application_name="test_npm_plugin",
Expand All @@ -166,3 +128,35 @@ def test_npm_plugin_include_node(new_dir, partitions):
# try to use bundled Node.js to execute the script
output = subprocess.check_output([str(node_path), str(binary)], text=True)
assert output == "hello world\n"

@pytest.mark.usefixtures("node_package")
def test_npm_plugin_set_symlinks(new_dir, partitions) -> None:
parts_yaml = textwrap.dedent(
"""\
parts:
foo:
plugin: npm
npm-include-node: true
npm-node-version: node
npm-set-symlinks: true
source: .
bar:
plugin: nil
stage-packages:
- base-files
"""
)

parts = yaml.safe_load(parts_yaml)

lifecycle = LifecycleManager(
parts,
application_name ="test_npm_plugin",
cache_dir=new_dir,
partitions=partitions
)
actions = lifecycle.plan(Step.PRIME)

with lifecycle.action_executor() as ctx:
ctx.execute(actions)

0 comments on commit ad8fa4c

Please sign in to comment.