Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,28 @@ corporate registry. Use `npm ci`, not `npm install`, and do not regenerate the
lockfile during installation. Keep the entire checkout, `.compliance`, and
dependency legal files.

### Networks that block registry.npmjs.org

The installers run a portable Node.js runtime that is unpacked into the
installation root. Because Node.js's portable archives ship no builtin npmrc,
npm would resolve its global configuration inside that throwaway runtime
directory and ignore the registry configured for the machine. The installers
therefore locate the machine's real global npmrc and pass it to the portable npm
so that `replace-registry-host` maps the lockfile's canonical npmjs URLs onto the
configured mirror. Machines with no npm configuration are unaffected and continue
to use `registry.npmjs.org`.

Configure a mirror once with:

```sh
npm config set registry <url> --location=global
```

To point a single installation at a specific registry without changing any npm
configuration, set `SKILL_RECORDER_NPM_REGISTRY` to an HTTPS registry URL before
running the installer. The lockfile's integrity hashes are verified whichever
registry serves the packages, so a mirror cannot substitute different content.

## Licensing boundary

The source channels distribute this repository's MIT-licensed source. The
Expand Down
135 changes: 124 additions & 11 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,76 @@ function Invoke-CheckedCommand {
}
}

function Resolve-MachineNpmConfigPath {
param([string[]]$CandidatePaths)

foreach ($candidate in $CandidatePaths) {
if ([string]::IsNullOrWhiteSpace($candidate)) {
continue
}
$trimmed = $candidate.Trim().Trim('"')
if ($trimmed -in @("undefined", "null")) {
continue
}
# A malformed npm configuration must never block a portable installation.
try {
if (Test-Path -LiteralPath $trimmed -PathType Leaf) {
return [IO.Path]::GetFullPath($trimmed)
}
} catch {
continue
}
}
return $null
}

function Get-SystemNpmGlobalConfigPath {
$npmCommand = @(
Get-Command npm -CommandType Application -ErrorAction SilentlyContinue
) | Select-Object -First 1
if (-not $npmCommand) {
return $null
}

$previousPreference = $ErrorActionPreference
$output = @()
$exitCode = 1
try {
$ErrorActionPreference = "Continue"
$output = @(& $npmCommand.Source "config" "get" "globalconfig" 2>$null)
$exitCode = $LASTEXITCODE
} catch {
$output = @()
$exitCode = 1
} finally {
$ErrorActionPreference = $previousPreference
$global:LASTEXITCODE = 0
}

if ($exitCode -ne 0 -or $output.Count -eq 0) {
return $null
}
return ([string]$output[-1]).Trim()
}

function Get-MachineNpmConfigPath {
# The portable Node.js archive ships no builtin npmrc, so npm resolves its
# global config inside the throwaway runtime directory and silently ignores a
# registry configured for this machine. Point npm back at the real file so
# networks that block registry.npmjs.org still install through their mirror.
$candidates = New-Object System.Collections.Generic.List[string]

$reported = Get-SystemNpmGlobalConfigPath
if (-not [string]::IsNullOrWhiteSpace($reported)) {
$candidates.Add($reported)
}
if (-not [string]::IsNullOrWhiteSpace($env:APPDATA)) {
$candidates.Add((Join-Path $env:APPDATA "npm\etc\npmrc"))
}

return Resolve-MachineNpmConfigPath -CandidatePaths $candidates.ToArray()
}

function Get-WindowsArchitecture {
$architecture = [Environment]::GetEnvironmentVariable(
"PROCESSOR_ARCHITECTURE",
Expand Down Expand Up @@ -606,10 +676,35 @@ if (Test-Path -LiteralPath $sourceDirectory -PathType Container) {
"scripts\run-reviewed-electron.mjs"
)

$registryOverride = $env:SKILL_RECORDER_NPM_REGISTRY
if (-not [string]::IsNullOrWhiteSpace($registryOverride)) {
$registryOverride = $registryOverride.Trim()
$parsedRegistry = $null
if (
-not [Uri]::TryCreate($registryOverride, [UriKind]::Absolute, [ref]$parsedRegistry) -or
$parsedRegistry.Scheme -ne "https"
) {
throw "SKILL_RECORDER_NPM_REGISTRY must be an absolute HTTPS URL: $registryOverride"
}
} else {
$registryOverride = $null
}

$environmentOverrides = [ordered]@{
PATH = "$($runtime.Root);$env:PATH"
NPM_CONFIG_ALLOW_SCRIPTS = $null
}
if ($registryOverride) {
Write-Step "Using the npm registry requested by SKILL_RECORDER_NPM_REGISTRY."
$environmentOverrides["NPM_CONFIG_REGISTRY"] = $registryOverride
} else {
$machineNpmConfig = Get-MachineNpmConfigPath
if ($machineNpmConfig) {
Write-Step "Applying this machine's npm configuration from $machineNpmConfig."
$environmentOverrides["NPM_CONFIG_GLOBALCONFIG"] = $machineNpmConfig
}
}

$originalEnvironment = @{}
foreach ($entry in $environmentOverrides.GetEnumerator()) {
$originalEnvironment[$entry.Key] = [Environment]::GetEnvironmentVariable(
Expand Down Expand Up @@ -642,17 +737,35 @@ if (Test-Path -LiteralPath $sourceDirectory -PathType Container) {
-Description "lockfile portability validation"

Write-Step "Installing lockfile-pinned dependencies through the configured npm registry."
Invoke-CheckedCommand `
-FilePath $runtime.Npm `
-Arguments @(
"ci",
"--no-audit",
"--no-fund",
"--ignore-scripts=false",
"--dangerously-allow-all-scripts=false",
"--strict-allow-scripts"
) `
-Description "npm ci"
$registryOutput = @(& $runtime.Npm config get registry)
$effectiveRegistry = "the configured npm registry"
if ($LASTEXITCODE -eq 0 -and $registryOutput.Count -gt 0) {
$effectiveRegistry = ([string]$registryOutput[-1]).Trim()
Write-Step "Dependencies will be downloaded from $effectiveRegistry."
}
$global:LASTEXITCODE = 0

try {
Invoke-CheckedCommand `
-FilePath $runtime.Npm `
-Arguments @(
"ci",
"--no-audit",
"--no-fund",
"--ignore-scripts=false",
"--dangerously-allow-all-scripts=false",
"--strict-allow-scripts"
) `
-Description "npm ci"
} catch {
throw (
"$($_.Exception.Message) Dependencies were requested from $effectiveRegistry. " +
"If your network blocks that registry, configure a compatible mirror with " +
"'npm config set registry <url> --location=global', or set " +
"SKILL_RECORDER_NPM_REGISTRY=<url> before running the installer again. " +
"The lockfile's integrity hashes are verified whichever registry serves the packages."
)
}

$electronDistribution = Assert-ReviewedElectronDistribution `
-SourceDirectory $buildDirectory `
Expand Down
48 changes: 47 additions & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,22 @@ checksum_from_manifest() {
awk -v name="$file_name" '$2 == name || $2 == "*" name { print tolower($1); exit }' "$manifest"
}

detect_machine_npm_config() {
# The portable Node.js archive ships no builtin npmrc, so npm resolves its
# global config inside the throwaway runtime directory and silently ignores a
# registry configured for this machine. Capture the real path before the
# portable runtime is prepended to PATH so mirrored registries keep working.
local candidate=""
if have npm; then
candidate="$(npm config get globalconfig 2>/dev/null | tail -n 1 | tr -d '\r')" || candidate=""
fi
case "$candidate" in
""|undefined|null) return 0 ;;
esac
[ -f "$candidate" ] || return 0
printf '%s' "$candidate"
}

install_node_runtime() {
local channel="https://nodejs.org/dist/latest-v24.x"
local sums="$WORK_DIR/node-SHASUMS256.txt"
Expand Down Expand Up @@ -266,18 +282,46 @@ build_source_install() {
export NPM_CONFIG_CACHE="$INSTALL_ROOT/npm-cache"
unset NPM_CONFIG_ALLOW_SCRIPTS npm_config_allow_scripts

if [ -n "${SKILL_RECORDER_NPM_REGISTRY:-}" ]; then
case "$SKILL_RECORDER_NPM_REGISTRY" in
https://*) ;;
*)
die "SKILL_RECORDER_NPM_REGISTRY must be an absolute HTTPS URL: $SKILL_RECORDER_NPM_REGISTRY."
;;
esac
info "Using the npm registry requested by SKILL_RECORDER_NPM_REGISTRY."
export NPM_CONFIG_REGISTRY="$SKILL_RECORDER_NPM_REGISTRY"
elif [ -n "${MACHINE_NPM_CONFIG:-}" ]; then
info "Applying this machine's npm configuration from $MACHINE_NPM_CONFIG."
export NPM_CONFIG_GLOBALCONFIG="$MACHINE_NPM_CONFIG"
fi

info "Validating portable dependency policy."
local npm_version
npm_version="$("$NPM" --version)" || die "Could not determine the bundled npm version."
"$NODE" "scripts/check-lockfile-portability.mjs" --npm-version "$npm_version"

info "Installing lockfile-pinned dependencies through the configured npm registry."
local effective_registry
effective_registry="$("$NPM" config get registry 2>/dev/null | tail -n 1 | tr -d '\r')" ||
effective_registry=""
[ -n "$effective_registry" ] || effective_registry="the configured npm registry"
info "Dependencies will be downloaded from $effective_registry."

"$NPM" ci \
--no-audit \
--no-fund \
--ignore-scripts=false \
--dangerously-allow-all-scripts=false \
--strict-allow-scripts
--strict-allow-scripts ||
die "$(
printf '%s' \
"npm ci failed. Dependencies were requested from $effective_registry. " \
"If your network blocks that registry, configure a compatible mirror with " \
"'npm config set registry <url> --location=global', or set " \
"SKILL_RECORDER_NPM_REGISTRY=<url> before running the installer again. " \
"The lockfile's integrity hashes are verified whichever registry serves the packages."
)"

local policy_key="$PLATFORM-$ARCHITECTURE"
local electron_version reviewed_hash
Expand Down Expand Up @@ -470,6 +514,8 @@ write_launcher() {
fi
}

MACHINE_NPM_CONFIG="$(detect_machine_npm_config)"

install_node_runtime

SOURCE_DIR="$VERSIONS_ROOT/$COMMIT"
Expand Down
37 changes: 36 additions & 1 deletion scripts/install-windows.test.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ if ($parseErrors.Count -ne 0) {
$helperNames = @(
"ConvertTo-ExtendedLengthPath",
"Move-DirectoryTree",
"Remove-DirectoryTree"
"Remove-DirectoryTree",
"Resolve-MachineNpmConfigPath"
)
$functionDefinitions = @(
$installerAst.FindAll(
Expand All @@ -50,6 +51,40 @@ if ($uncPath -ne "\\?\UNC\server\share\folder") {
throw "Extended-length UNC conversion returned an unexpected path: $uncPath"
}

$npmConfigRoot = Join-Path (
[IO.Path]::GetTempPath()
) ("skill-recorder-npmrc-" + [guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $npmConfigRoot -Force | Out-Null
try {
$machineNpmrc = Join-Path $npmConfigRoot "npmrc"
Set-Content -LiteralPath $machineNpmrc -Value "registry=https://example.invalid/npm/" -Encoding ASCII
$missingNpmrc = Join-Path $npmConfigRoot "missing\npmrc"

$resolved = Resolve-MachineNpmConfigPath -CandidatePaths @($missingNpmrc, $machineNpmrc)
if ($resolved -ne [IO.Path]::GetFullPath($machineNpmrc)) {
throw "Resolve-MachineNpmConfigPath skipped the existing npmrc: $resolved"
}

# npm prints "undefined" when no global config is configured; it is not a path.
$placeholders = Resolve-MachineNpmConfigPath -CandidatePaths @("undefined", "null", "", $null)
if ($null -ne $placeholders) {
throw "Resolve-MachineNpmConfigPath accepted a placeholder value: $placeholders"
}

# Installs on machines without any npm configuration must stay on the default registry.
$absent = Resolve-MachineNpmConfigPath -CandidatePaths @($missingNpmrc)
if ($null -ne $absent) {
throw "Resolve-MachineNpmConfigPath returned a nonexistent npmrc: $absent"
}

$quoted = Resolve-MachineNpmConfigPath -CandidatePaths @(('"' + $machineNpmrc + '" '))
if ($quoted -ne [IO.Path]::GetFullPath($machineNpmrc)) {
throw "Resolve-MachineNpmConfigPath did not normalize a quoted npm path: $quoted"
}
} finally {
Remove-Item -LiteralPath $npmConfigRoot -Recurse -Force
}

$testRoot = Join-Path (
[IO.Path]::GetTempPath()
) ("skill-recorder-installer-" + [guid]::NewGuid().ToString("N"))
Expand Down
Loading