• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/python/pants/backend/javascript/subsystems/nodejs.py
1
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

UNCOV
4
from __future__ import annotations
×
5

UNCOV
6
import itertools
×
UNCOV
7
import logging
×
UNCOV
8
import os.path
×
UNCOV
9
from collections.abc import Collection, Iterable, Mapping, Sequence
×
UNCOV
10
from dataclasses import dataclass, field
×
UNCOV
11
from itertools import groupby
×
UNCOV
12
from typing import ClassVar
×
13

UNCOV
14
from nodesemver import min_satisfying
×
15

UNCOV
16
from pants.core.environments.target_types import EnvironmentTarget
×
UNCOV
17
from pants.core.util_rules import asdf, search_paths, system_binaries
×
UNCOV
18
from pants.core.util_rules.asdf import AsdfPathString, AsdfToolPathsResult
×
UNCOV
19
from pants.core.util_rules.env_vars import environment_vars_subset
×
UNCOV
20
from pants.core.util_rules.env_vars import environment_vars_subset as environment_vars_subset_get
×
UNCOV
21
from pants.core.util_rules.external_tool import (
×
22
    DownloadedExternalTool,
23
    ExternalToolRequest,
24
    ExternalToolVersion,
25
    TemplatedExternalToolOptionsMixin,
26
    download_external_tool,
27
)
UNCOV
28
from pants.core.util_rules.external_tool import rules as external_tool_rules
×
UNCOV
29
from pants.core.util_rules.search_paths import (
×
30
    ExecutableSearchPathsOptionMixin,
31
    ValidateSearchPathsRequest,
32
    VersionManagerSearchPathsRequest,
33
    get_un_cachable_version_manager_paths,
34
    validate_search_paths,
35
)
UNCOV
36
from pants.core.util_rules.system_binaries import (
×
37
    BinaryNotFoundError,
38
    BinaryPath,
39
    BinaryPathRequest,
40
    BinaryPathTest,
41
    BinaryShims,
42
    BinaryShimsRequest,
43
    create_binary_shims,
44
    find_binary,
45
)
UNCOV
46
from pants.engine.env_vars import EXTRA_ENV_VARS_USAGE_HELP, EnvironmentVars, EnvironmentVarsRequest
×
UNCOV
47
from pants.engine.fs import EMPTY_DIGEST, CreateDigest, Digest, Directory, DownloadFile
×
UNCOV
48
from pants.engine.internals.native_engine import FileDigest, MergeDigests
×
UNCOV
49
from pants.engine.internals.platform_rules import environment_path_variable
×
UNCOV
50
from pants.engine.internals.selectors import concurrently
×
UNCOV
51
from pants.engine.intrinsics import create_digest, merge_digests
×
UNCOV
52
from pants.engine.platform import Platform
×
UNCOV
53
from pants.engine.process import Process, fallible_to_exec_result_or_raise
×
UNCOV
54
from pants.engine.rules import Rule, collect_rules, implicitly, rule
×
UNCOV
55
from pants.engine.unions import UnionRule
×
UNCOV
56
from pants.option.option_types import DictOption, ShellStrListOption, StrListOption, StrOption
×
UNCOV
57
from pants.option.subsystem import Subsystem
×
UNCOV
58
from pants.util.docutil import bin_name
×
UNCOV
59
from pants.util.frozendict import FrozenDict
×
UNCOV
60
from pants.util.logging import LogLevel
×
UNCOV
61
from pants.util.ordered_set import FrozenOrderedSet
×
UNCOV
62
from pants.util.strutil import help_text, softwrap
×
63

UNCOV
64
_logger = logging.getLogger(__name__)
×
65

66

UNCOV
67
class NodeJS(Subsystem, TemplatedExternalToolOptionsMixin):
×
UNCOV
68
    options_scope = "nodejs"
×
UNCOV
69
    help = "The Node.js Javascript runtime (including Corepack)."
×
70

UNCOV
71
    default_version = "v24.10.0"
×
UNCOV
72
    default_known_versions = [
×
73
        "v24.10.0|macos_arm64|fbc3d6e1e1d962450d058e918214373872cc4c46e08673f31c35932afac4a8c5|51169103",
74
        "v24.10.0|macos_x86_64|627b884f66db0dd35f4b46fb9e994774ce560a7fb60798ba1ab81e867a73687d|52347633",
75
        "v24.10.0|linux_arm64|07f0558316ebb8977dd6fb29b4de8d369a639d3d8cef544293852a6f5eea6af8|31020300",
76
        "v24.10.0|linux_x86_64|2642f4428869aca32443660fd71b3918e2be1277a899bdcaeb64c93b54b5af17|32102040",
77
    ]
78

UNCOV
79
    default_url_template = "https://nodejs.org/dist/{version}/node-{version}-{platform}.tar"
×
UNCOV
80
    default_url_platform_mapping = {
×
81
        "macos_arm64": "darwin-arm64",
82
        "macos_x86_64": "darwin-x64",
83
        "linux_arm64": "linux-arm64",
84
        "linux_x86_64": "linux-x64",
85
    }
86

UNCOV
87
    resolves = DictOption[str](
×
88
        default={},
89
        help=softwrap(
90
            f"""
91
            A mapping of names to lockfile paths used in your project.
92

93
            Specifying a resolve name is optional. If unspecified,
94
            the default resolve name is calculated by taking the path
95
            from the source root to the directory containing the lockfile
96
            and replacing '{os.path.sep}' with '.' in that path.
97

98
            Example:
99
            An npm lockfile located at `src/js/package/package-lock.json`
100
            will result in a resolve named `js.package`, assuming src/
101
            is a source root.
102

103
            Run `{bin_name()} generate-lockfiles` to
104
            generate the lockfile(s).
105
            """
106
        ),
107
        advanced=True,
108
    )
109

UNCOV
110
    def generate_url(self, version: str, plat: Platform) -> str:
×
111
        """NodeJS binaries are compressed as .gz for Mac, .xz for Linux."""
112
        platform = self.url_platform_mapping.get(plat.value, "")
×
113
        url = self.url_template.format(version=version, platform=platform)
×
114
        extension = "gz" if plat.is_macos else "xz"
×
115
        return f"{url}.{extension}"
×
116

UNCOV
117
    def generate_exe(self, version: str, plat: Platform) -> str:
×
118
        assert self.default_url_platform_mapping is not None
×
119
        plat_str = self.default_url_platform_mapping[plat.value]
×
120
        return f"./node-{version}-{plat_str}/bin/node"
×
121

UNCOV
122
    async def download_known_version(
×
123
        self, known_version: ExternalToolVersion, platform: Platform
124
    ) -> DownloadedExternalTool:
125
        exe = self.generate_exe(known_version.version, platform)
×
126
        url = self.generate_url(known_version.version, platform)
×
127
        download_file = DownloadFile(url, FileDigest(known_version.sha256, known_version.filesize))
×
128
        return await download_external_tool(ExternalToolRequest(download_file, exe))
×
129

UNCOV
130
    package_manager = StrOption(
×
131
        default="npm",
132
        help=softwrap(
133
            """
134
            Default Node.js package manager to use.
135

136
            You can either rely on this default together with the [nodejs].package_managers
137
            option, or specify the `package.json#packageManager` tool and version
138
            in the package.json of your project.
139

140
            Specifying conflicting package manager versions within a multi-package
141
            workspace is an error.
142
            """
143
        ),
144
    )
145

UNCOV
146
    package_managers = DictOption[str](
×
147
        default={"npm": "11.6.2", "yarn": "1.22.22", "pnpm": "10.19.0"},
148
        help=help_text(
149
            """
150
            A mapping of package manager versions to semver releases.
151

152
            Many organizations only need a single version of a package manager, which is
153
            a good default and often the simplest thing to do.
154

155
            The version download is managed by Corepack. This mapping corresponds to
156
            the https://github.com/nodejs/corepack#known-good-releases setting, using
157
            the `--activate` flag.
158
            """
159
        ),
160
    )
161

UNCOV
162
    extra_env_vars = StrListOption(
×
163
        help=softwrap(
164
            f"""
165
            Environment variables to set during package manager operations.
166

167
            {EXTRA_ENV_VARS_USAGE_HELP}
168
            """
169
        ),
170
        advanced=True,
171
    )
172

UNCOV
173
    @property
×
UNCOV
174
    def default_package_manager(self) -> str | None:
×
175
        if self.package_manager in self.package_managers:
×
176
            return f"{self.package_manager}@{self.package_managers[self.package_manager]}"
×
177
        return self.package_manager
×
178

UNCOV
179
    _tools = StrListOption(
×
180
        default=[],
181
        help=softwrap(
182
            """
183
            List any additional executable tools required for node processes to work. The paths to
184
            these tools will be included in the PATH used in the execution sandbox, so that
185
            they may be used by nodejs processes execution.
186
            """
187
        ),
188
        advanced=True,
189
    )
190

UNCOV
191
    _optional_tools = StrListOption(
×
192
        default=[],
193
        help=softwrap(
194
            """
195
            List any additional executable which are not mandatory for node processes to work, but
196
            which should be included if available. The paths to these tools will be included in the
197
            PATH used in the execution sandbox, so that they may be used by nodejs processes execution.
198
            """
199
        ),
200
        advanced=True,
201
    )
202

UNCOV
203
    @property
×
UNCOV
204
    def tools(self) -> tuple[str, ...]:
×
UNCOV
205
        return tuple(sorted(set(self._tools)))
×
206

UNCOV
207
    @property
×
UNCOV
208
    def optional_tools(self) -> tuple[str, ...]:
×
UNCOV
209
        return tuple(sorted(set(self._optional_tools)))
×
210

UNCOV
211
    class EnvironmentAware(ExecutableSearchPathsOptionMixin, Subsystem.EnvironmentAware):
×
UNCOV
212
        search_path = StrListOption(
×
213
            default=["<PATH>"],
214
            help=lambda cls: help_text(
215
                f"""
216
                A list of paths to search for Node.js distributions.
217

218
                This option is only used if a templated url download
219
                specified via [{cls.subsystem.options_scope}].known_versions
220
                does not contain a version matching the configured
221
                [{cls.subsystem.options_scope}].version range.
222

223
                You can specify absolute paths to binaries
224
                and/or to directories containing binaries. The order of entries does
225
                not matter.
226

227
                The following special strings are supported:
228

229
                For all runtime environment types:
230

231
                * `<PATH>`, the contents of the PATH env var
232

233
                When the environment is a `local_environment` target:
234

235
                * `{AsdfPathString.STANDARD}`, {AsdfPathString.STANDARD.description("Node.js")}
236
                * `{AsdfPathString.LOCAL}`, {AsdfPathString.LOCAL.description("binaries")}
237
                * `<NVM>`, all NodeJS versions under $NVM_DIR/versions/node
238
                * `<NVM_LOCAL>`, the nvm installation with the version in BUILD_ROOT/.nvmrc
239
                Note that the version in the .nvmrc file has to be on the form "vX.Y.Z".
240
                """
241
            ),
242
            advanced=True,
243
            metavar="<binary-paths>",
244
        )
245

UNCOV
246
        executable_search_paths_help = softwrap(
×
247
            """
248
            The PATH value that will be used to find any tools required to run nodejs processes.
249
            """
250
        )
251

UNCOV
252
        _corepack_env_vars = ShellStrListOption(
×
253
            help=softwrap(
254
                """
255
                Environment variables to set for `corepack` invocations.
256

257
                Entries are either strings in the form `ENV_VAR=value` to set an explicit value;
258
                or just `ENV_VAR` to copy the value from Pants's own environment.
259

260
                Review https://github.com/nodejs/corepack#environment-variables
261
                for available variables.
262
                """
263
            ),
264
            advanced=True,
265
        )
266

UNCOV
267
        @property
×
UNCOV
268
        def corepack_env_vars(self) -> tuple[str, ...]:
×
269
            return tuple(sorted(set(self._corepack_env_vars)))
×
270

271

UNCOV
272
@dataclass(frozen=True)
×
UNCOV
273
class NodeJSToolProcess:
×
274
    """A request for a tool installed with NodeJS."""
275

UNCOV
276
    tool: str
×
UNCOV
277
    tool_version: str | None
×
UNCOV
278
    args: tuple[str, ...]
×
UNCOV
279
    description: str
×
UNCOV
280
    level: LogLevel = LogLevel.INFO
×
UNCOV
281
    input_digest: Digest = EMPTY_DIGEST
×
UNCOV
282
    output_files: tuple[str, ...] = ()
×
UNCOV
283
    output_directories: tuple[str, ...] = ()
×
UNCOV
284
    working_directory: str | None = None
×
UNCOV
285
    append_only_caches: FrozenDict[str, str] = field(default_factory=FrozenDict)
×
UNCOV
286
    timeout_seconds: int | None = None
×
UNCOV
287
    extra_env: Mapping[str, str] = field(default_factory=FrozenDict)
×
UNCOV
288
    project_digest: Digest | None = None
×
289

UNCOV
290
    @classmethod
×
UNCOV
291
    def npm(
×
292
        cls,
293
        args: Iterable[str],
294
        description: str,
295
        level: LogLevel = LogLevel.INFO,
296
        input_digest: Digest = EMPTY_DIGEST,
297
        output_files: tuple[str, ...] = (),
298
        output_directories: tuple[str, ...] = (),
299
        working_directory: str | None = None,
300
        append_only_caches: FrozenDict[str, str] | None = None,
301
        timeout_seconds: int | None = None,
302
        extra_env: Mapping[str, str] | None = None,
303
        tool_version: str | None = None,
304
        project_digest: Digest | None = None,
305
    ) -> NodeJSToolProcess:
UNCOV
306
        return cls(
×
307
            tool="npm",
308
            tool_version=tool_version,
309
            args=tuple(args),
310
            description=description,
311
            level=level,
312
            input_digest=input_digest,
313
            output_files=output_files,
314
            output_directories=output_directories,
315
            working_directory=working_directory,
316
            append_only_caches=append_only_caches or FrozenDict(),
317
            timeout_seconds=timeout_seconds,
318
            extra_env=extra_env or FrozenDict(),
319
            project_digest=project_digest,
320
        )
321

322

UNCOV
323
@dataclass(frozen=True)
×
UNCOV
324
class NodeJSBinaries:
×
UNCOV
325
    binary_dir: str
×
UNCOV
326
    digest: Digest | None = None
×
327

328

UNCOV
329
@dataclass(frozen=True)
×
UNCOV
330
class NodeJSProcessEnvironment:
×
UNCOV
331
    binaries: NodeJSBinaries
×
UNCOV
332
    npm_config_cache: str
×
UNCOV
333
    tool_binaries: BinaryShims
×
UNCOV
334
    corepack_home: str
×
UNCOV
335
    corepack_shims: str
×
UNCOV
336
    corepack_env_vars: EnvironmentVars
×
337

UNCOV
338
    base_bin_dir: ClassVar[str] = "__node"
×
339

UNCOV
340
    def to_env_dict(self, extras: Mapping[str, str] | None = None) -> dict[str, str]:
×
UNCOV
341
        extras = extras or {}
×
UNCOV
342
        extra_path = extras.get("PATH", "")
×
UNCOV
343
        path = [self.tool_binaries.path_component, self.corepack_shims, self.binary_directory]
×
UNCOV
344
        if extra_path:
×
UNCOV
345
            path.append(extra_path)
×
346

UNCOV
347
        return {
×
348
            **extras,
349
            "PATH": os.pathsep.join(path),
350
            "npm_config_cache": self.npm_config_cache,  # Normally stored at ~/.npm,
351
            "COREPACK_HOME": os.path.join("{chroot}", self.corepack_home),
352
            **self.corepack_env_vars,
353
        }
354

UNCOV
355
    @property
×
UNCOV
356
    def append_only_caches(self) -> Mapping[str, str]:
×
357
        return {"npm": self.npm_config_cache}
×
358

UNCOV
359
    @property
×
UNCOV
360
    def binary_directory(self) -> str:
×
UNCOV
361
        return self.binaries.binary_dir
×
362

UNCOV
363
    def immutable_digest(self) -> dict[str, Digest]:
×
364
        return (
×
365
            {self.base_bin_dir: self.binaries.digest, **self.tool_binaries.immutable_input_digests}
366
            if self.binaries.digest
367
            else {**self.tool_binaries.immutable_input_digests}
368
        )
369

370

UNCOV
371
async def add_corepack_shims_to_digest(
×
372
    binaries: NodeJSBinaries, tool_shims: BinaryShims, corepack_env_vars: EnvironmentVars
373
) -> Digest:
UNCOV
374
    directory_digest = await create_digest(CreateDigest([Directory("._corepack")]))
×
UNCOV
375
    binary_digest = binaries.digest if binaries.digest else EMPTY_DIGEST
×
UNCOV
376
    input_digest = await merge_digests(MergeDigests((directory_digest, binary_digest)))
×
377

UNCOV
378
    none_immutable_binary_path = binaries.binary_dir.replace(
×
379
        f"/{NodeJSProcessEnvironment.base_bin_dir}", ""
380
    )
UNCOV
381
    enable_corepack_result = await fallible_to_exec_result_or_raise(
×
382
        **implicitly(
383
            Process(
384
                argv=(
385
                    "corepack",
386
                    "enable",
387
                    "npm",
388
                    "pnpm",
389
                    "yarn",
390
                    "--install-directory",
391
                    "._corepack",
392
                ),
393
                input_digest=input_digest,
394
                immutable_input_digests={**tool_shims.immutable_input_digests},
395
                output_directories=["._corepack"],
396
                description="Enabling corepack shims",
397
                level=LogLevel.DEBUG,
398
                env={
399
                    "PATH": f"{tool_shims.path_component}:{none_immutable_binary_path}",
400
                    "COREPACK_HOME": "._corepack_home",
401
                    **corepack_env_vars,
402
                },
403
            )
404
        )
405
    )
UNCOV
406
    return await merge_digests(MergeDigests((binary_digest, enable_corepack_result.output_digest)))
×
407

408

UNCOV
409
async def get_nodejs_process_tools_shims(
×
410
    *,
411
    tools: Sequence[str],
412
    optional_tools: Sequence[str],
413
    search_path: Sequence[str],
414
    rationale: str,
415
) -> BinaryShims:
UNCOV
416
    requests = [
×
417
        BinaryPathRequest(binary_name=binary_name, search_path=search_path)
418
        for binary_name in (*tools, *optional_tools)
419
    ]
UNCOV
420
    paths = await concurrently(find_binary(request, **implicitly()) for request in requests)
×
UNCOV
421
    required_tools_paths = [
×
422
        path.first_path_or_raise(request, rationale=rationale)
423
        for request, path in zip(requests, paths)
424
        if request.binary_name in tools
425
    ]
UNCOV
426
    optional_tools_paths = [
×
427
        path.first_path
428
        for request, path in zip(requests, paths)
429
        if request.binary_name in optional_tools and path.first_path
430
    ]
431

UNCOV
432
    tools_shims = await create_binary_shims(
×
433
        BinaryShimsRequest.for_paths(
434
            *required_tools_paths,
435
            *optional_tools_paths,
436
            rationale=rationale,
437
        ),
438
        **implicitly(),
439
    )
440

UNCOV
441
    return tools_shims
×
442

443

UNCOV
444
@rule(level=LogLevel.DEBUG)
×
UNCOV
445
async def node_process_environment(
×
446
    binaries: NodeJSBinaries,
447
    nodejs: NodeJS,
448
    nodejs_environment: NodeJS.EnvironmentAware,
449
) -> NodeJSProcessEnvironment:
UNCOV
450
    default_required_tools = ["sh", "bash"]
×
UNCOV
451
    tools_used_by_setup_scripts = ["mkdir", "rm", "touch", "which"]
×
UNCOV
452
    pnpm_shim_tools = ["sed", "dirname", "uname"]
×
453

UNCOV
454
    binary_shims = await get_nodejs_process_tools_shims(
×
455
        tools=[
456
            *default_required_tools,
457
            *tools_used_by_setup_scripts,
458
            *pnpm_shim_tools,
459
            *nodejs.tools,
460
        ],
461
        optional_tools=nodejs.optional_tools,
462
        search_path=nodejs_environment.executable_search_path,
463
        rationale="execute a nodejs process",
464
    )
UNCOV
465
    corepack_env_vars = await environment_vars_subset_get(
×
466
        EnvironmentVarsRequest(nodejs_environment.corepack_env_vars), **implicitly()
467
    )
UNCOV
468
    binary_digest_with_shims = await add_corepack_shims_to_digest(
×
469
        binaries, binary_shims, corepack_env_vars
470
    )
UNCOV
471
    binaries = NodeJSBinaries(binaries.binary_dir, binary_digest_with_shims)
×
472

UNCOV
473
    return NodeJSProcessEnvironment(
×
474
        binaries=binaries,
475
        npm_config_cache="._npm",
476
        tool_binaries=binary_shims,
477
        corepack_home="._corepack_home",
478
        corepack_shims=os.path.join(
479
            "{chroot}", NodeJSProcessEnvironment.base_bin_dir, "._corepack"
480
        ),
481
        corepack_env_vars=corepack_env_vars,
482
    )
483

484

UNCOV
485
@dataclass(frozen=True)
×
UNCOV
486
class NodeJSBootstrap:
×
UNCOV
487
    nodejs_search_paths: tuple[str, ...]
×
488

489

UNCOV
490
async def _get_nvm_root() -> str | None:
×
491
    """See https://github.com/nvm-sh/nvm#installing-and-updating."""
492

UNCOV
493
    env = await environment_vars_subset(
×
494
        EnvironmentVarsRequest(("NVM_DIR", "XDG_CONFIG_HOME", "HOME")), **implicitly()
495
    )
UNCOV
496
    nvm_dir = env.get("NVM_DIR")
×
UNCOV
497
    default_dir = env.get("XDG_CONFIG_HOME", env.get("HOME"))
×
UNCOV
498
    if nvm_dir:
×
UNCOV
499
        return nvm_dir
×
UNCOV
500
    elif default_dir:
×
UNCOV
501
        return os.path.join(default_dir, ".nvm")
×
UNCOV
502
    return None
×
503

504

UNCOV
505
async def _nodejs_search_paths(
×
506
    env_tgt: EnvironmentTarget, paths: Collection[str]
507
) -> tuple[str, ...]:
508
    asdf_result = await AsdfToolPathsResult.get_un_cachable_search_paths(
×
509
        paths,
510
        env_tgt=env_tgt,
511
        tool_name="nodejs",
512
        tool_description="Node.js distribution",
513
        paths_option_name=f"[{NodeJS.options_scope}].search_path",
514
    )
515
    asdf_standard_tool_paths = asdf_result.standard_tool_paths
×
516
    asdf_local_tool_paths = asdf_result.local_tool_paths
×
517
    special_strings: dict[str, Iterable[str]] = {
×
518
        AsdfPathString.STANDARD: asdf_standard_tool_paths,
519
        AsdfPathString.LOCAL: asdf_local_tool_paths,
520
    }
521
    nvm_dir = await _get_nvm_root()
×
522
    expanded: list[str] = []
×
523
    nvm_path_results = await concurrently(
×
524
        get_un_cachable_version_manager_paths(
525
            VersionManagerSearchPathsRequest(
526
                env_tgt,
527
                nvm_dir,
528
                "versions/node",
529
                f"[{NodeJS.options_scope}].search_path",
530
                (".nvmrc",),
531
                s if s == "<NVM_LOCAL>" else None,
532
            ),
533
        )
534
        for s in paths
535
        if s == "<NVM>" or s == "<NVM_LOCAL>"
536
    )
537
    for nvm_path in FrozenOrderedSet(itertools.chain.from_iterable(nvm_path_results)):
×
538
        expanded.append(nvm_path)
×
539
    for s in paths:
×
540
        if s == "<PATH>":
×
541
            expanded.extend(await environment_path_variable(**implicitly()))  # noqa: PNT30: Linear search
×
542
        elif s in special_strings:
×
543
            expanded.extend(special_strings[s])
×
544
        elif s == "<NVM>" or s == "<NVM_LOCAL>":
×
545
            continue
×
546
        else:
547
            expanded.append(s)
×
548
    return tuple(expanded)
×
549

550

UNCOV
551
@rule
×
UNCOV
552
async def nodejs_bootstrap(nodejs_env_aware: NodeJS.EnvironmentAware) -> NodeJSBootstrap:
×
553
    search_paths = await validate_search_paths(
×
554
        ValidateSearchPathsRequest(
555
            env_tgt=nodejs_env_aware.env_tgt,
556
            search_paths=tuple(nodejs_env_aware.search_path),
557
            option_origin=f"[{NodeJS.options_scope}].search_path",
558
            environment_key="nodejs_search_path",
559
            is_default=nodejs_env_aware._is_default("search_path"),
560
            local_only=FrozenOrderedSet(
561
                (AsdfPathString.STANDARD, AsdfPathString.LOCAL, "<NVM>", "<NVM_LOCAL>")
562
            ),
563
        )
564
    )
565

566
    expanded_paths = await _nodejs_search_paths(nodejs_env_aware.env_tgt, search_paths)
×
567

568
    return NodeJSBootstrap(nodejs_search_paths=expanded_paths)
×
569

570

UNCOV
571
class _BinaryPathsPerVersion(FrozenDict[str, Sequence[BinaryPath]]):
×
UNCOV
572
    pass
×
573

574

UNCOV
575
@rule(level=LogLevel.DEBUG, desc="Testing for Node.js binaries.")
×
UNCOV
576
async def get_valid_nodejs_paths_by_version(bootstrap: NodeJSBootstrap) -> _BinaryPathsPerVersion:
×
577
    paths = await find_binary(
×
578
        BinaryPathRequest(
579
            search_path=bootstrap.nodejs_search_paths,
580
            binary_name="node",
581
            test=BinaryPathTest(
582
                ["--version"], fingerprint_stdout=False
583
            ),  # Hack to retain version info
584
        ),
585
        **implicitly(),
586
    )
587

588
    group_by_version = groupby((path for path in paths.paths), key=lambda path: path.fingerprint)
×
589
    return _BinaryPathsPerVersion({version: tuple(paths) for version, paths in group_by_version})
×
590

591

UNCOV
592
@rule(level=LogLevel.DEBUG, desc="Finding Node.js distribution binaries.")
×
UNCOV
593
async def determine_nodejs_binaries(
×
594
    nodejs: NodeJS, platform: Platform, paths_per_version: _BinaryPathsPerVersion
595
) -> NodeJSBinaries:
UNCOV
596
    decoded_versions = groupby(
×
597
        (ExternalToolVersion.decode(unparsed) for unparsed in nodejs.known_versions),
598
        lambda v: v.version,
599
    )
600

UNCOV
601
    decoded_per_version = {
×
602
        version: tuple(
603
            known_version
604
            for known_version in known_versions
605
            if known_version.platform == platform.value
606
        )
607
        for version, known_versions in decoded_versions
608
    }
609

UNCOV
610
    satisfying_version = min_satisfying(decoded_per_version.keys(), nodejs.version)
×
UNCOV
611
    if satisfying_version:
×
UNCOV
612
        known_version = decoded_per_version[satisfying_version][0]
×
UNCOV
613
        downloaded = await nodejs.download_known_version(known_version, platform)
×
UNCOV
614
        nodejs_bin_dir = os.path.join(
×
615
            "{chroot}",
616
            NodeJSProcessEnvironment.base_bin_dir,
617
            os.path.dirname(downloaded.exe),
618
        )
619

UNCOV
620
        return NodeJSBinaries(nodejs_bin_dir, downloaded.digest)
×
621

UNCOV
622
    satisfying_version = min_satisfying(paths_per_version.keys(), nodejs.version)
×
UNCOV
623
    if not satisfying_version:
×
UNCOV
624
        raise BinaryNotFoundError(
×
625
            softwrap(
626
                f"""
627
                Cannot find any `node` binaries satisfying the range '{nodejs.version}'.
628

629
                To fix, either list a `[{NodeJS.options_scope}].known_versions` version that satisfies the range,
630
                or ensure `[{NodeJS.options_scope}].search_path` contains a path to binaries that satisfy the range.
631
                """
632
            )
633
        )
UNCOV
634
    return NodeJSBinaries(os.path.dirname(paths_per_version[satisfying_version][0].path))
×
635

636

UNCOV
637
@dataclass(frozen=True)
×
UNCOV
638
class CorepackToolRequest:
×
UNCOV
639
    tool: str
×
UNCOV
640
    version: str | None = None
×
641

642

UNCOV
643
@dataclass(frozen=True)
×
UNCOV
644
class CorepackToolDigest:
×
UNCOV
645
    digest: Digest
×
646

647

UNCOV
648
@rule(desc="Preparing Corepack managed tool.")
×
UNCOV
649
async def prepare_corepack_tool(
×
650
    request: CorepackToolRequest, environment: NodeJSProcessEnvironment, nodejs: NodeJS
651
) -> CorepackToolDigest:
652
    version = request.version or nodejs.package_managers.get(request.tool)
×
653
    tool_spec = f"{request.tool}@{version}" if version else request.tool
×
654
    tool_description = tool_spec if version else f"default {tool_spec} version"
×
655
    result = await fallible_to_exec_result_or_raise(
×
656
        **implicitly(
657
            Process(
658
                argv=filter(
659
                    None, ("corepack", "prepare", tool_spec if version else "--all", "--activate")
660
                ),
661
                description=f"Preparing configured {tool_description}.",
662
                immutable_input_digests=environment.immutable_digest(),
663
                level=LogLevel.DEBUG,
664
                env=environment.to_env_dict(),
665
                append_only_caches={**environment.append_only_caches},
666
                output_directories=[environment.corepack_home],
667
            )
668
        )
669
    )
670
    return CorepackToolDigest(result.output_digest)
×
671

672

UNCOV
673
@rule(level=LogLevel.DEBUG)
×
UNCOV
674
async def setup_node_tool_process(
×
675
    request: NodeJSToolProcess, environment: NodeJSProcessEnvironment
676
) -> Process:
677
    if request.tool in ("npm", "npx", "pnpm", "yarn"):
×
678
        tool_name = request.tool.replace("npx", "npm")
×
679
        corepack_tool = await prepare_corepack_tool(
×
680
            CorepackToolRequest(tool_name, request.tool_version), **implicitly()
681
        )
682
        input_digest = await merge_digests(
×
683
            MergeDigests([request.input_digest, corepack_tool.digest])
684
        )
685
    else:
686
        input_digest = request.input_digest
×
687
    return Process(
×
688
        argv=list(filter(None, (request.tool, *request.args))),
689
        input_digest=input_digest,
690
        output_files=request.output_files,
691
        immutable_input_digests=environment.immutable_digest(),
692
        output_directories=request.output_directories,
693
        description=request.description,
694
        level=request.level,
695
        env=environment.to_env_dict(request.extra_env),
696
        working_directory=request.working_directory,
697
        append_only_caches={**request.append_only_caches, **environment.append_only_caches},
698
        timeout_seconds=request.timeout_seconds,
699
    )
700

701

UNCOV
702
class UserChosenNodeJSResolveAliases(FrozenDict[str, str]):
×
UNCOV
703
    pass
×
704

705

UNCOV
706
@rule(level=LogLevel.DEBUG)
×
UNCOV
707
async def user_chosen_resolve_aliases(nodejs: NodeJS) -> UserChosenNodeJSResolveAliases:
×
708
    return UserChosenNodeJSResolveAliases((value, key) for key, value in nodejs.resolves.items())
×
709

710

UNCOV
711
def rules() -> Iterable[Rule | UnionRule]:
×
UNCOV
712
    return (
×
713
        *collect_rules(),
714
        *external_tool_rules(),
715
        *asdf.rules(),
716
        *system_binaries.rules(),
717
        *search_paths.rules(),
718
    )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc