Support/Package/Execution/Eigenverft.Manifested.Sandbox.Package.Npm.ps1
|
<#
Eigenverft.Manifested.Sandbox.Package.Npm #> function Get-PackageNpmGlobalConfigPath { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult ) $packageRoot = Get-PackageRootFromInventoryPath -PackageAssignmentInventoryFilePath ([string]$PackageResult.PackageConfig.PackageAssignmentInventoryFilePath) return ([System.IO.Path]::GetFullPath((Join-Path $packageRoot 'Configuration\External\npm\npmrc'))) } function New-PackageNpmCacheDirectory { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult ) $packageRoot = Get-PackageRootFromInventoryPath -PackageAssignmentInventoryFilePath ([string]$PackageResult.PackageConfig.PackageAssignmentInventoryFilePath) $segments = @( 'Caches' 'npm' [string]$PackageResult.DefinitionId [string]$PackageResult.Package.releaseTrack [string]$PackageResult.Package.version [string]$PackageResult.Package.artifactDistributionVariant ) | ForEach-Object { ([string]$_).Trim() -replace '[\\/:\*\?"<>\|]', '-' } $cacheDirectory = [System.IO.Path]::GetFullPath((Join-Path $packageRoot ($segments -join '\'))) $null = New-Item -ItemType Directory -Path $cacheDirectory -Force return $cacheDirectory } function Initialize-PackageNpmGlobalConfig { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$GlobalConfigPath ) $resolvedPath = [System.IO.Path]::GetFullPath($GlobalConfigPath) $directoryPath = Split-Path -Parent $resolvedPath if (-not [string]::IsNullOrWhiteSpace($directoryPath)) { $null = New-Item -ItemType Directory -Path $directoryPath -Force } if (-not (Test-Path -LiteralPath $resolvedPath -PathType Leaf)) { Set-Content -LiteralPath $resolvedPath -Value '' -Encoding UTF8 } return $resolvedPath } function Resolve-PackageNpmInstallerCommand { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult ) $install = Get-PackageAssignedInstallOperation -Release $PackageResult.Package if (-not $install) { throw "Package npm global package install for '$($PackageResult.PackageId)' requires packageOperations.assigned.install on the selected release." } if (-not $install.PSObject.Properties['installerCommand'] -or [string]::IsNullOrWhiteSpace([string]$install.installerCommand)) { throw "Package npm global package install for '$($PackageResult.PackageId)' requires packageOperations.assigned.install.installerCommand." } $installerCommand = [string]$install.installerCommand $dependencyInfo = Resolve-PackageDependencyCommandPath -PackageResult $PackageResult -CommandName $installerCommand Write-PackageExecutionMessage -Message ("[STATE] Installer command ready: definition='{0}', command='{1}', path='{2}'." -f $dependencyInfo.DefinitionId, $dependencyInfo.Command, $dependencyInfo.CommandPath) return $dependencyInfo } function Resolve-PackageNpmNodeCommandPath { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult, [Parameter(Mandatory = $true)] [string]$NpmCommandPath ) $npmDirectory = Split-Path -Parent ([System.IO.Path]::GetFullPath($NpmCommandPath)) foreach ($candidateName in @('node.exe', 'node.cmd', 'node.bat')) { $candidatePath = Join-Path $npmDirectory $candidateName if (Test-Path -LiteralPath $candidatePath -PathType Leaf) { return ([System.IO.Path]::GetFullPath($candidatePath)) } } try { $dependencyInfo = Resolve-PackageDependencyCommandPath -PackageResult $PackageResult -CommandName 'node' if ($dependencyInfo -and -not [string]::IsNullOrWhiteSpace([string]$dependencyInfo.CommandPath)) { return ([System.IO.Path]::GetFullPath([string]$dependencyInfo.CommandPath)) } } catch { # Fall through to PATH lookup; npm materialization still reports a clear error below. } $pathCommand = Get-Command -Name 'node.exe' -ErrorAction SilentlyContinue if ($pathCommand -and -not [string]::IsNullOrWhiteSpace([string]$pathCommand.Source)) { return ([System.IO.Path]::GetFullPath([string]$pathCommand.Source)) } throw "Package npm materialization for '$($PackageResult.PackageId)' requires node.exe to parse npm lock metadata." } function Test-PackageNpmMaterializedInstallKind { [CmdletBinding()] param( [AllowNull()] [psobject]$Package ) $install = Get-PackageAssignedInstallOperation -Release $Package return ($install -and $install.PSObject.Properties['kind'] -and [string]::Equals([string]$install.kind, 'npmMaterializedInstallGlobalPackage', [System.StringComparison]::OrdinalIgnoreCase)) } function Get-PackageNpmResolvedPackageSpec { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult ) $install = Get-PackageAssignedInstallOperation -Release $PackageResult.Package if (-not $install -or -not $install.PSObject.Properties['packageSpec'] -or [string]::IsNullOrWhiteSpace([string]$install.packageSpec)) { throw "Package npm materialized install for '$($PackageResult.PackageId)' requires packageOperations.assigned.install.packageSpec." } return (Resolve-PackageTemplateText -Text ([string]$install.packageSpec) -PackageConfig $PackageResult.PackageConfig -Package $PackageResult.Package) } function Get-PackageNpmMaterializationDirectory { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult ) if ([string]::IsNullOrWhiteSpace([string]$PackageResult.PackageFileStagingDirectory)) { throw "Package npm materialization for '$($PackageResult.PackageId)' requires a package file staging directory." } return ([System.IO.Path]::GetFullPath((Join-Path ([string]$PackageResult.PackageFileStagingDirectory) 'npm-materialized'))) } function Get-PackageNpmMaterializationManifestPath { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Directory ) return ([System.IO.Path]::GetFullPath((Join-Path $Directory 'npm-materialization.json'))) } function Get-PackageNpmPlatform { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageConfig ) switch -Regex ([string]$PackageConfig.Platform) { '^(windows|win32)$' { return 'win32' } '^(macos|darwin)$' { return 'darwin' } '^linux$' { return 'linux' } default { return ([string]$PackageConfig.Platform).ToLowerInvariant() } } } function Get-PackageNpmArchitecture { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageConfig ) switch -Regex ([string]$PackageConfig.Architecture) { '^(x64|amd64)$' { return 'x64' } '^(arm64|aarch64)$' { return 'arm64' } '^(x86|ia32)$' { return 'ia32' } default { return ([string]$PackageConfig.Architecture).ToLowerInvariant() } } } function Test-PackageNpmIntegrity { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$Integrity ) if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false } foreach ($token in @(([string]$Integrity) -split '\s+')) { if ([string]::IsNullOrWhiteSpace($token) -or $token -notmatch '^(?<algorithm>sha1|sha256|sha384|sha512)-(?<value>.+)$') { continue } $algorithm = $Matches.algorithm.ToUpperInvariant() $expected = $Matches.value $hashAlgorithm = [System.Security.Cryptography.HashAlgorithm]::Create($algorithm) if (-not $hashAlgorithm) { continue } $stream = [System.IO.File]::OpenRead([System.IO.Path]::GetFullPath($Path)) try { $actual = [Convert]::ToBase64String($hashAlgorithm.ComputeHash($stream)) } finally { $stream.Dispose() $hashAlgorithm.Dispose() } if ([string]::Equals($actual, $expected, [System.StringComparison]::Ordinal)) { return $true } } return $false } function Test-PackageNpmMaterializationDirectory { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Directory, [Parameter(Mandatory = $true)] [string]$PackageSpec, [Parameter(Mandatory = $true)] [string]$NpmPlatform, [Parameter(Mandatory = $true)] [string]$NpmArchitecture ) $manifestPath = Get-PackageNpmMaterializationManifestPath -Directory $Directory if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { return $null } try { $manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json } catch { return $null } if (-not $manifest -or -not [string]::Equals([string]$manifest.packageSpec, $PackageSpec, [System.StringComparison]::OrdinalIgnoreCase) -or -not [string]::Equals([string]$manifest.npmPlatform, $NpmPlatform, [System.StringComparison]::OrdinalIgnoreCase) -or -not [string]::Equals([string]$manifest.npmArchitecture, $NpmArchitecture, [System.StringComparison]::OrdinalIgnoreCase)) { return $null } $tarballPaths = New-Object System.Collections.Generic.List[string] foreach ($package in @($manifest.packages)) { if (-not $package.PSObject.Properties['fileName'] -or [string]::IsNullOrWhiteSpace([string]$package.fileName) -or -not $package.PSObject.Properties['integrity'] -or [string]::IsNullOrWhiteSpace([string]$package.integrity)) { return $null } $tarballPath = [System.IO.Path]::GetFullPath((Join-Path $Directory ([string]$package.fileName))) if (-not (Test-PackageNpmIntegrity -Path $tarballPath -Integrity ([string]$package.integrity))) { return $null } $tarballPaths.Add($tarballPath) | Out-Null } if ($tarballPaths.Count -eq 0) { return $null } return [pscustomobject]@{ ManifestPath = [System.IO.Path]::GetFullPath($manifestPath) Manifest = $manifest TarballPaths = @($tarballPaths.ToArray()) } } function Copy-PackageNpmMaterializationDirectory { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$SourceDirectory, [Parameter(Mandatory = $true)] [string]$TargetDirectory, [Parameter(Mandatory = $true)] [psobject]$Materialization ) $null = New-Item -ItemType Directory -Path $TargetDirectory -Force $sourceManifestPath = Get-PackageNpmMaterializationManifestPath -Directory $SourceDirectory $targetManifestPath = Get-PackageNpmMaterializationManifestPath -Directory $TargetDirectory $null = Copy-FileToPath -SourcePath $sourceManifestPath -TargetPath $targetManifestPath -Overwrite $copiedTarballs = New-Object System.Collections.Generic.List[string] foreach ($package in @($Materialization.Manifest.packages)) { $sourcePath = [System.IO.Path]::GetFullPath((Join-Path $SourceDirectory ([string]$package.fileName))) $targetPath = [System.IO.Path]::GetFullPath((Join-Path $TargetDirectory ([string]$package.fileName))) $null = Copy-FileToPath -SourcePath $sourcePath -TargetPath $targetPath -Overwrite $copiedTarballs.Add($targetPath) | Out-Null } return [pscustomobject]@{ ManifestPath = $targetManifestPath Manifest = $Materialization.Manifest TarballPaths = @($copiedTarballs.ToArray()) } } function Find-PackageNpmMaterializationInDepots { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult, [Parameter(Mandatory = $true)] [string]$PackageSpec, [Parameter(Mandatory = $true)] [string]$NpmPlatform, [Parameter(Mandatory = $true)] [string]$NpmArchitecture ) foreach ($depotSource in @(Get-PackagePackageDepotSources -PackageConfig $PackageResult.PackageConfig)) { if ([string]::IsNullOrWhiteSpace([string]$depotSource.basePath)) { continue } $candidateDirectory = [System.IO.Path]::GetFullPath((Join-Path ([string]$depotSource.basePath) ([string]$PackageResult.PackageDepotRelativeDirectory))) $materialization = Test-PackageNpmMaterializationDirectory -Directory $candidateDirectory -PackageSpec $PackageSpec -NpmPlatform $NpmPlatform -NpmArchitecture $NpmArchitecture if ($materialization) { $materialization | Add-Member -MemberType NoteProperty -Name SourceId -Value ([string]$depotSource.id) -Force $materialization | Add-Member -MemberType NoteProperty -Name SourceDirectory -Value $candidateDirectory -Force return $materialization } } return $null } function New-PackageNpmMaterializationManifest { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult, [Parameter(Mandatory = $true)] [string]$PackageSpec, [Parameter(Mandatory = $true)] [string]$NpmPlatform, [Parameter(Mandatory = $true)] [string]$NpmArchitecture, [Parameter(Mandatory = $true)] [object[]]$Packages ) return [pscustomobject]@{ schemaVersion = '1.0' packageSpec = $PackageSpec definitionId = [string]$PackageResult.DefinitionId packageId = [string]$PackageResult.PackageId packageVersion = [string]$PackageResult.Package.version releaseTrack = [string]$PackageResult.Package.releaseTrack artifactDistributionVariant = [string]$PackageResult.Package.artifactDistributionVariant npmPlatform = $NpmPlatform npmArchitecture = $NpmArchitecture generatedAtUtc = [DateTime]::UtcNow.ToString('o') packages = @($Packages) } } function Write-PackageNpmMaterializationHelperScript { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$HelperScriptPath ) $script = @' const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const childProcess = require('child_process'); const requestPath = process.argv[2]; const resultPath = process.argv[3]; function writeJson(filePath, value) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(value, null, 2), 'utf8'); } function readJson(filePath) { const text = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''); return JSON.parse(text); } function trimOutput(value) { return String(value || '').trim().slice(0, 4000); } function run(command, args, cwd, operation) { const result = childProcess.spawnSync(command, args, { cwd, shell: true, encoding: 'utf8', windowsHide: true }); if (result.error) { throw new Error(`${operation} failed to start: ${result.error.message}`); } const status = typeof result.status === 'number' ? result.status : 0; if (status !== 0) { throw new Error(`${operation} failed with exit code ${status}. stdout=${trimOutput(result.stdout)} stderr=${trimOutput(result.stderr)}`); } return result.stdout || ''; } function parseJsonOutput(output, operation) { const text = String(output || '').trim(); if (!text) { throw new Error(`${operation} did not return JSON output.`); } try { const parsed = JSON.parse(text); return Array.isArray(parsed) ? parsed : [parsed]; } catch (_) { const start = text.indexOf('['); const end = text.lastIndexOf(']'); if (start >= 0 && end > start) { const parsed = JSON.parse(text.substring(start, end + 1)); return Array.isArray(parsed) ? parsed : [parsed]; } throw new Error(`${operation} did not return parseable JSON output.`); } } function allowed(list, value) { if (!Array.isArray(list) || list.length === 0) return true; const positives = list.filter((entry) => !String(entry).startsWith('!')).map(String); const negatives = list.filter((entry) => String(entry).startsWith('!')).map((entry) => String(entry).substring(1)); if (negatives.includes(value)) return false; if (positives.length > 0 && !positives.includes(value)) return false; return true; } function packageName(lockKey) { const pieces = String(lockKey).split('node_modules/'); return pieces[pieces.length - 1].replace(/\/$/, ''); } function fileNameFromResolved(resolved) { try { const url = new URL(resolved); const parts = url.pathname.split('/'); return decodeURIComponent(parts[parts.length - 1]); } catch (_) { return ''; } } function selectedPackages(lock, npmPlatform, npmArchitecture) { const packages = []; for (const [lockKey, entry] of Object.entries(lock.packages || {})) { if (!String(lockKey).includes('node_modules/')) continue; if (!entry || !entry.version || !entry.resolved || !entry.integrity) continue; if (!allowed(entry.os, npmPlatform) || !allowed(entry.cpu, npmArchitecture)) continue; packages.push({ name: packageName(lockKey), version: String(entry.version), resolved: String(entry.resolved), integrity: String(entry.integrity), fileName: fileNameFromResolved(String(entry.resolved)), optional: !!entry.optional, os: Array.isArray(entry.os) ? entry.os.map(String) : [], cpu: Array.isArray(entry.cpu) ? entry.cpu.map(String) : [] }); } return packages; } function verifyIntegrity(filePath, integrity) { if (!filePath || !integrity || !fs.existsSync(filePath)) return false; const content = fs.readFileSync(filePath); for (const token of String(integrity).split(/\s+/)) { const match = /^([a-z0-9]+)-(.+)$/i.exec(token); if (!match) continue; try { const actual = crypto.createHash(match[1].toLowerCase()).update(content).digest('base64'); if (actual === match[2]) return true; } catch (_) { continue; } } return false; } function globalConfigArgs(globalConfigPath) { return globalConfigPath ? ['--globalconfig', globalConfigPath] : []; } function materializePackage(request, packageInfo) { if (packageInfo.fileName && packageInfo.integrity) { const existingPath = path.resolve(request.targetDirectory, packageInfo.fileName); if (verifyIntegrity(existingPath, packageInfo.integrity)) { return packageInfo; } } const packageSpec = `${packageInfo.name}@${packageInfo.version}`; const packOutput = run( request.npmCommandPath, ['pack', packageSpec, '--pack-destination', request.targetDirectory, '--json', '--cache', request.cacheDirectory, ...globalConfigArgs(request.globalConfigPath)], request.targetDirectory, `npm pack for '${packageSpec}'` ); const packItems = parseJsonOutput(packOutput, `npm pack for '${packageSpec}'`); const packItem = packItems.find((item) => String(item.name).toLowerCase() === String(packageInfo.name).toLowerCase() && String(item.version) === String(packageInfo.version)) || packItems[0]; const fileName = String(packItem.filename || packageInfo.fileName || ''); const integrity = String(packItem.integrity || packageInfo.integrity || ''); if (!fileName) throw new Error(`npm pack for '${packageSpec}' did not report a tarball filename.`); if (!integrity) throw new Error(`npm pack for '${packageSpec}' did not report integrity metadata.`); const targetPath = path.resolve(request.targetDirectory, fileName); if (!verifyIntegrity(targetPath, integrity)) { throw new Error(`npm pack output '${fileName}' did not satisfy integrity metadata.`); } return { name: String(packageInfo.name), version: String(packageInfo.version), resolved: String(packageInfo.resolved || ''), integrity, fileName, optional: !!packageInfo.optional, os: Array.isArray(packageInfo.os) ? packageInfo.os.map(String) : [], cpu: Array.isArray(packageInfo.cpu) ? packageInfo.cpu.map(String) : [] }; } function main() { const request = readJson(requestPath); fs.rmSync(request.resolutionDirectory, { recursive: true, force: true }); fs.mkdirSync(request.resolutionDirectory, { recursive: true }); fs.mkdirSync(request.targetDirectory, { recursive: true }); run( request.npmCommandPath, ['install', '--package-lock-only', '--ignore-scripts', '--no-audit', '--no-fund', '--cache', request.cacheDirectory, ...globalConfigArgs(request.globalConfigPath), request.packageSpec], request.resolutionDirectory, `npm metadata resolution for '${request.packageSpec}'` ); const lockFilePath = path.resolve(request.resolutionDirectory, 'package-lock.json'); if (!fs.existsSync(lockFilePath)) { throw new Error(`npm metadata resolution for '${request.packageSpec}' did not produce package-lock.json.`); } const lock = readJson(lockFilePath); const packages = selectedPackages(lock, request.npmPlatform, request.npmArchitecture); if (packages.length === 0) { throw new Error(`npm metadata resolution for '${request.packageSpec}' did not produce any materializable packages.`); } const materializedPackages = packages.map((packageInfo) => materializePackage(request, packageInfo)); const manifest = { schemaVersion: '1.0', packageSpec: request.packageSpec, definitionId: request.definitionId, packageId: request.packageId, packageVersion: request.packageVersion, releaseTrack: request.releaseTrack, artifactDistributionVariant: request.artifactDistributionVariant, npmPlatform: request.npmPlatform, npmArchitecture: request.npmArchitecture, generatedAtUtc: new Date().toISOString(), packages: materializedPackages }; writeJson(request.manifestPath, manifest); writeJson(resultPath, { success: true, status: 'Materialized', packageCount: materializedPackages.length, manifestPath: request.manifestPath, packageNames: materializedPackages.map((item) => item.name) }); } try { main(); } catch (error) { writeJson(resultPath, { success: false, status: 'Failed', errorMessage: error && error.message ? error.message : String(error), stack: error && error.stack ? error.stack : '' }); process.exit(1); } '@ $resolvedPath = [System.IO.Path]::GetFullPath($HelperScriptPath) $directoryPath = Split-Path -Parent $resolvedPath if (-not [string]::IsNullOrWhiteSpace($directoryPath)) { $null = New-Item -ItemType Directory -Path $directoryPath -Force } Set-Content -LiteralPath $resolvedPath -Value $script -Encoding UTF8 return $resolvedPath } function New-PackageNpmMaterializationFromRegistry { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult, [Parameter(Mandatory = $true)] [string]$PackageSpec, [Parameter(Mandatory = $true)] [string]$NpmPlatform, [Parameter(Mandatory = $true)] [string]$NpmArchitecture, [Parameter(Mandatory = $true)] [string]$TargetDirectory ) $installerCommandInfo = Resolve-PackageNpmInstallerCommand -PackageResult $PackageResult $nodeCommandPath = Resolve-PackageNpmNodeCommandPath -PackageResult $PackageResult -NpmCommandPath ([string]$installerCommandInfo.CommandPath) $cacheDirectory = New-PackageNpmCacheDirectory -PackageResult $PackageResult $globalConfigPath = Initialize-PackageNpmGlobalConfig -GlobalConfigPath (Get-PackageNpmGlobalConfigPath -PackageResult $PackageResult) Write-PackageExecutionMessage -Message ("[STATE] Materializing npm package spec '{0}'." -f $PackageSpec) $targetFullPath = [System.IO.Path]::GetFullPath($TargetDirectory) $resolutionDirectory = [System.IO.Path]::GetFullPath((Join-Path $targetFullPath '.npm-resolution')) $helperScriptPath = [System.IO.Path]::GetFullPath((Join-Path $targetFullPath 'materialize-npm-package.js')) $requestPath = [System.IO.Path]::GetFullPath((Join-Path $targetFullPath 'npm-materialization-request.json')) $resultPath = [System.IO.Path]::GetFullPath((Join-Path $targetFullPath 'npm-materialization-result.json')) $manifestPath = Get-PackageNpmMaterializationManifestPath -Directory $targetFullPath $null = New-Item -ItemType Directory -Path $targetFullPath -Force $helperScriptPath = Write-PackageNpmMaterializationHelperScript -HelperScriptPath $helperScriptPath $request = [pscustomobject]@{ packageSpec = $PackageSpec definitionId = [string]$PackageResult.DefinitionId packageId = [string]$PackageResult.PackageId packageVersion = [string]$PackageResult.Package.version releaseTrack = [string]$PackageResult.Package.releaseTrack artifactDistributionVariant = [string]$PackageResult.Package.artifactDistributionVariant npmPlatform = $NpmPlatform npmArchitecture = $NpmArchitecture npmCommandPath = [System.IO.Path]::GetFullPath([string]$installerCommandInfo.CommandPath) cacheDirectory = [System.IO.Path]::GetFullPath($cacheDirectory) globalConfigPath = [System.IO.Path]::GetFullPath($globalConfigPath) targetDirectory = $targetFullPath resolutionDirectory = $resolutionDirectory manifestPath = $manifestPath } $request | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $requestPath -Encoding UTF8 Write-PackageExecutionMessage -Message ("[PATH] npm materialization helper: {0}" -f $helperScriptPath) Write-PackageExecutionMessage -Message ("[PATH] npm materialization request: {0}" -f $requestPath) Write-PackageExecutionMessage -Message ("[PATH] npm materialization result: {0}" -f $resultPath) & $nodeCommandPath $helperScriptPath $requestPath $resultPath $exitCode = $LASTEXITCODE if ($null -eq $exitCode) { $exitCode = 0 } $helperResult = $null if (Test-Path -LiteralPath $resultPath -PathType Leaf) { try { $helperResult = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json } catch { $helperResult = $null } } if ($exitCode -ne 0 -or ($helperResult -and $helperResult.PSObject.Properties['success'] -and -not [bool]$helperResult.success)) { $errorMessage = if ($helperResult -and $helperResult.PSObject.Properties['errorMessage'] -and -not [string]::IsNullOrWhiteSpace([string]$helperResult.errorMessage)) { [string]$helperResult.errorMessage } else { "npm materialization helper failed with exit code $exitCode." } throw $errorMessage } $materialization = Test-PackageNpmMaterializationDirectory -Directory $targetFullPath -PackageSpec $PackageSpec -NpmPlatform $NpmPlatform -NpmArchitecture $NpmArchitecture if (-not $materialization) { throw "npm materialization for '$PackageSpec' could not be validated after download." } return $materialization } function Invoke-PackageNpmMaterializationDepotDistribution { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult, [Parameter(Mandatory = $true)] [psobject]$Materialization ) $mode = if ($PackageResult.PackageConfig.PSObject.Properties['DepotDistributionMode'] -and -not [string]::IsNullOrWhiteSpace([string]$PackageResult.PackageConfig.DepotDistributionMode)) { [string]$PackageResult.PackageConfig.DepotDistributionMode } else { 'packageFocused' } $files = New-Object System.Collections.Generic.List[object] $files.Add([pscustomobject]@{ FileName = 'npm-materialization.json'; SourcePath = [string]$Materialization.ManifestPath }) | Out-Null foreach ($package in @($Materialization.Manifest.packages)) { $sourcePath = [System.IO.Path]::GetFullPath((Join-Path (Split-Path -Parent ([string]$Materialization.ManifestPath)) ([string]$package.fileName))) $files.Add([pscustomobject]@{ FileName = [string]$package.fileName; SourcePath = $sourcePath }) | Out-Null } $actions = New-Object System.Collections.Generic.List[object] if ([string]::Equals($mode, 'disabled', [System.StringComparison]::OrdinalIgnoreCase)) { return [pscustomobject]@{ Mode = $mode; Status = 'Skipped'; Reason = 'DisabledByPolicy'; Actions = @(); CopiedCount = 0; FailedCount = 0; SkippedCount = 0 } } foreach ($mirrorSource in @(Get-PackageDepotDistributionTargets -PackageConfig $PackageResult.PackageConfig)) { foreach ($file in @($files.ToArray())) { if (-not [string]::Equals([string]$mirrorSource.kind, 'filesystem', [System.StringComparison]::OrdinalIgnoreCase)) { $actions.Add([pscustomobject]@{ DepotId = [string]$mirrorSource.id; FileName = [string]$file.FileName; Action = 'Skip'; Status = 'Skipped'; Reason = 'UnsupportedDepotKind'; SourcePath = [string]$file.SourcePath; TargetPath = $null; EnsureExists = [bool]$mirrorSource.ensureExists; ErrorMessage = $null }) | Out-Null continue } if ([string]::IsNullOrWhiteSpace([string]$mirrorSource.basePath)) { $actions.Add([pscustomobject]@{ DepotId = [string]$mirrorSource.id; FileName = [string]$file.FileName; Action = 'Skip'; Status = 'Skipped'; Reason = 'MissingBasePath'; SourcePath = [string]$file.SourcePath; TargetPath = $null; EnsureExists = [bool]$mirrorSource.ensureExists; ErrorMessage = $null }) | Out-Null continue } $targetDirectory = [System.IO.Path]::GetFullPath((Join-Path ([string]$mirrorSource.basePath) ([string]$PackageResult.PackageDepotRelativeDirectory))) $targetPath = [System.IO.Path]::GetFullPath((Join-Path $targetDirectory ([string]$file.FileName))) $sourceFullPath = [System.IO.Path]::GetFullPath([string]$file.SourcePath) if ([string]::Equals($sourceFullPath, $targetPath, [System.StringComparison]::OrdinalIgnoreCase)) { $actions.Add([pscustomobject]@{ DepotId = [string]$mirrorSource.id; FileName = [string]$file.FileName; Action = 'Skip'; Status = 'Skipped'; Reason = 'SourceIsTarget'; SourcePath = $sourceFullPath; TargetPath = $targetPath; EnsureExists = [bool]$mirrorSource.ensureExists; ErrorMessage = $null }) | Out-Null continue } $match = Test-PackageDepotDistributionFileMatches -SourcePath $sourceFullPath -TargetPath $targetPath if ($match.Matches) { $actions.Add([pscustomobject]@{ DepotId = [string]$mirrorSource.id; FileName = [string]$file.FileName; Action = 'Skip'; Status = 'Skipped'; Reason = [string]$match.Reason; SourcePath = $sourceFullPath; TargetPath = $targetPath; EnsureExists = [bool]$mirrorSource.ensureExists; ErrorMessage = $null }) | Out-Null continue } if ([string]::Equals($mode, 'packageFocused', [System.StringComparison]::OrdinalIgnoreCase) -and -not [string]::Equals([string]$match.Reason, 'Missing', [System.StringComparison]::OrdinalIgnoreCase)) { $actions.Add([pscustomobject]@{ DepotId = [string]$mirrorSource.id; FileName = [string]$file.FileName; Action = 'Skip'; Status = 'Skipped'; Reason = 'DifferentTargetPreservedByPackageFocusedPolicy'; SourcePath = $sourceFullPath; TargetPath = $targetPath; EnsureExists = [bool]$mirrorSource.ensureExists; ErrorMessage = [string]$match.Reason }) | Out-Null continue } try { if ($mirrorSource.ensureExists) { $null = New-Item -ItemType Directory -Path $targetDirectory -Force } $null = Copy-FileToPath -SourcePath $sourceFullPath -TargetPath $targetPath -Overwrite $actions.Add([pscustomobject]@{ DepotId = [string]$mirrorSource.id; FileName = [string]$file.FileName; Action = 'Copy'; Status = 'Copied'; Reason = [string]$match.Reason; SourcePath = $sourceFullPath; TargetPath = $targetPath; EnsureExists = [bool]$mirrorSource.ensureExists; ErrorMessage = $null }) | Out-Null } catch { $actions.Add([pscustomobject]@{ DepotId = [string]$mirrorSource.id; FileName = [string]$file.FileName; Action = 'Copy'; Status = 'Failed'; Reason = [string]$match.Reason; SourcePath = $sourceFullPath; TargetPath = $targetPath; EnsureExists = [bool]$mirrorSource.ensureExists; ErrorMessage = $_.Exception.Message }) | Out-Null } } } $copiedCount = @($actions.ToArray() | Where-Object { [string]::Equals([string]$_.Status, 'Copied', [System.StringComparison]::OrdinalIgnoreCase) }).Count $failedCount = @($actions.ToArray() | Where-Object { [string]::Equals([string]$_.Status, 'Failed', [System.StringComparison]::OrdinalIgnoreCase) }).Count $skippedCount = @($actions.ToArray() | Where-Object { [string]::Equals([string]$_.Status, 'Skipped', [System.StringComparison]::OrdinalIgnoreCase) }).Count return [pscustomobject]@{ Mode = $mode Status = if ($actions.Count -eq 0) { 'Skipped' } else { 'Planned' } Reason = if ($actions.Count -eq 0) { 'NoDepotTargets' } else { $null } Actions = @($actions.ToArray()) CopiedCount = $copiedCount FailedCount = $failedCount SkippedCount = $skippedCount } } function Invoke-PackageNpmMaterialization { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult ) if (-not (Test-PackageNpmMaterializedInstallKind -Package $PackageResult.Package)) { return $PackageResult } $packageSpec = Get-PackageNpmResolvedPackageSpec -PackageResult $PackageResult $npmPlatform = Get-PackageNpmPlatform -PackageConfig $PackageResult.PackageConfig $npmArchitecture = Get-PackageNpmArchitecture -PackageConfig $PackageResult.PackageConfig $stageDirectory = Get-PackageNpmMaterializationDirectory -PackageResult $PackageResult Remove-PathIfExists -Path $stageDirectory | Out-Null $depotMaterialization = Find-PackageNpmMaterializationInDepots -PackageResult $PackageResult -PackageSpec $packageSpec -NpmPlatform $npmPlatform -NpmArchitecture $npmArchitecture if ($depotMaterialization) { $copied = Copy-PackageNpmMaterializationDirectory -SourceDirectory ([string]$depotMaterialization.SourceDirectory) -TargetDirectory $stageDirectory -Materialization $depotMaterialization $PackageResult | Add-Member -MemberType NoteProperty -Name NpmMaterialization -Value ([pscustomobject]@{ Success = $true Status = 'HydratedFromDepot' PackageSpec = $packageSpec NpmPlatform = $npmPlatform NpmArchitecture = $npmArchitecture SourceId = [string]$depotMaterialization.SourceId ManifestPath = [string]$copied.ManifestPath Manifest = $copied.Manifest TarballPaths = @($copied.TarballPaths) DepotDistribution = $null }) -Force Write-PackageExecutionMessage -Message ("[ACTION] Hydrated npm materialization from depot '{0}'." -f [string]$depotMaterialization.SourceId) } else { $materialization = New-PackageNpmMaterializationFromRegistry -PackageResult $PackageResult -PackageSpec $packageSpec -NpmPlatform $npmPlatform -NpmArchitecture $npmArchitecture -TargetDirectory $stageDirectory $PackageResult | Add-Member -MemberType NoteProperty -Name NpmMaterialization -Value ([pscustomobject]@{ Success = $true Status = 'MaterializedFromRegistry' PackageSpec = $packageSpec NpmPlatform = $npmPlatform NpmArchitecture = $npmArchitecture SourceId = 'npmRegistry' ManifestPath = [string]$materialization.ManifestPath Manifest = $materialization.Manifest TarballPaths = @($materialization.TarballPaths) DepotDistribution = $null }) -Force Write-PackageExecutionMessage -Message ("[ACTION] Materialized npm package spec '{0}' with {1} tarball(s)." -f $packageSpec, @($materialization.TarballPaths).Count) } $distribution = Invoke-PackageNpmMaterializationDepotDistribution -PackageResult $PackageResult -Materialization $PackageResult.NpmMaterialization $PackageResult.NpmMaterialization.DepotDistribution = $distribution Write-PackageExecutionMessage -Message ("[STATE] npm materialization depot distribution completed: mode='{0}', copied={1}, skipped={2}, failed={3}." -f [string]$distribution.Mode, [int]$distribution.CopiedCount, [int]$distribution.SkippedCount, [int]$distribution.FailedCount) return $PackageResult } function Install-PackageNpmPackage { <# .SYNOPSIS Installs an exact npm package spec into a staged Package-owned prefix. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult ) $install = Get-PackageAssignedInstallOperation -Release $PackageResult.Package if (-not $install) { throw "Package npm global package install for '$($PackageResult.PackageId)' requires packageOperations.assigned.install on the selected release." } if (-not $install.PSObject.Properties['packageSpec'] -or [string]::IsNullOrWhiteSpace([string]$install.packageSpec)) { throw "Package npm global package install for '$($PackageResult.PackageId)' requires packageOperations.assigned.install.packageSpec." } $packageSpec = Resolve-PackageTemplateText -Text ([string]$install.packageSpec) -PackageConfig $PackageResult.PackageConfig -Package $PackageResult.Package $installerCommandInfo = Resolve-PackageNpmInstallerCommand -PackageResult $PackageResult $cacheDirectory = New-PackageNpmCacheDirectory -PackageResult $PackageResult $globalConfigPath = Initialize-PackageNpmGlobalConfig -GlobalConfigPath (Get-PackageNpmGlobalConfigPath -PackageResult $PackageResult) if ([string]::IsNullOrWhiteSpace([string]$PackageResult.PackageInstallStageDirectory)) { throw "Package npm global package install for '$($PackageResult.PackageId)' requires a package install stage directory." } $stagePath = [System.IO.Path]::GetFullPath([string]$PackageResult.PackageInstallStageDirectory) Remove-PathIfExists -Path $stagePath | Out-Null $null = New-Item -ItemType Directory -Path $stagePath -Force $stagePromoted = $false $commandArguments = @('install', '-g', '--prefix', $stagePath, '--cache', $cacheDirectory) $commandArguments += @(Get-NpmGlobalConfigArguments -GlobalConfigPath $globalConfigPath) $commandArguments += $packageSpec Write-PackageExecutionMessage -Message ("[STATE] npm global package install:") Write-PackageExecutionMessage -Message ("[PATH] npm command: {0}" -f $installerCommandInfo.CommandPath) Write-PackageExecutionMessage -Message ("[PATH] npm stage: {0}" -f $stagePath) Write-PackageExecutionMessage -Message ("[PATH] npm cache: {0}" -f $cacheDirectory) Write-PackageExecutionMessage -Message ("[PATH] npm global config: {0}" -f $globalConfigPath) Write-PackageExecutionMessage -Message ("[STATE] npm package spec: {0}" -f $packageSpec) try { Push-Location $stagePath try { & $installerCommandInfo.CommandPath @commandArguments $exitCode = $LASTEXITCODE if ($null -eq $exitCode) { $exitCode = 0 } } finally { Pop-Location } if ($exitCode -ne 0) { throw "Package npm global package install for '$($PackageResult.PackageId)' failed with exit code $exitCode." } $installParent = Split-Path -Parent $PackageResult.InstallDirectory if (-not [string]::IsNullOrWhiteSpace($installParent)) { $null = New-Item -ItemType Directory -Path $installParent -Force } Remove-PathIfExists -Path $PackageResult.InstallDirectory | Out-Null Move-Item -LiteralPath $stagePath -Destination $PackageResult.InstallDirectory -Force $stagePromoted = $true } finally { if (-not $stagePromoted) { Write-PackageExecutionMessage -Level 'WRN' -Message ("[WARN] Preserving failed npm package install stage '{0}' for inspection." -f $stagePath) } } return [pscustomobject]@{ Status = Get-PackageOwnedInstallStatus -PackageResult $PackageResult InstallKind = 'npmGlobalPackage' InstallDirectory = $PackageResult.InstallDirectory ReusedExisting = $false InstallerCommand = $installerCommandInfo.Command InstallerCommandPath = $installerCommandInfo.CommandPath PackageSpec = $packageSpec CommandArguments = @($commandArguments) CacheDirectory = $cacheDirectory GlobalConfigPath = $globalConfigPath StagePath = $stagePath ExitCode = $exitCode } } function Install-PackageNpmMaterializedInstallGlobalPackage { <# .SYNOPSIS Installs a materialized npm package spec from local tarballs into a staged Package-owned prefix. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult ) $install = Get-PackageAssignedInstallOperation -Release $PackageResult.Package if (-not $install) { throw "Package npm materialized install for '$($PackageResult.PackageId)' requires packageOperations.assigned.install on the selected release." } if (-not $PackageResult.PSObject.Properties['NpmMaterialization'] -or -not $PackageResult.NpmMaterialization -or -not $PackageResult.NpmMaterialization.Success) { throw "Package npm materialized install for '$($PackageResult.PackageId)' requires prepared npm materialization." } $packageSpec = Get-PackageNpmResolvedPackageSpec -PackageResult $PackageResult $tarballPaths = @($PackageResult.NpmMaterialization.TarballPaths) if ($tarballPaths.Count -eq 0) { throw "Package npm materialized install for '$($PackageResult.PackageId)' has no local materialized tarballs." } foreach ($tarballPath in $tarballPaths) { if ([string]::IsNullOrWhiteSpace([string]$tarballPath) -or -not (Test-Path -LiteralPath ([string]$tarballPath) -PathType Leaf)) { throw "Package npm materialized install for '$($PackageResult.PackageId)' is missing materialized tarball '$tarballPath'." } } $installerCommandInfo = Resolve-PackageNpmInstallerCommand -PackageResult $PackageResult $cacheDirectory = New-PackageNpmCacheDirectory -PackageResult $PackageResult $globalConfigPath = Initialize-PackageNpmGlobalConfig -GlobalConfigPath (Get-PackageNpmGlobalConfigPath -PackageResult $PackageResult) if ([string]::IsNullOrWhiteSpace([string]$PackageResult.PackageInstallStageDirectory)) { throw "Package npm materialized install for '$($PackageResult.PackageId)' requires a package install stage directory." } $stagePath = [System.IO.Path]::GetFullPath([string]$PackageResult.PackageInstallStageDirectory) Remove-PathIfExists -Path $stagePath | Out-Null $null = New-Item -ItemType Directory -Path $stagePath -Force $stagePromoted = $false $cacheAddArguments = @('cache', 'add') $cacheAddArguments += @($tarballPaths) $cacheAddArguments += @('--cache', $cacheDirectory) $cacheAddArguments += @(Get-NpmGlobalConfigArguments -GlobalConfigPath $globalConfigPath) $commandArguments = @('install', '-g', '--prefix', $stagePath, '--cache', $cacheDirectory, '--offline') $commandArguments += @(Get-NpmGlobalConfigArguments -GlobalConfigPath $globalConfigPath) $commandArguments += $packageSpec Write-PackageExecutionMessage -Message ("[STATE] npm materialized package install:") Write-PackageExecutionMessage -Message ("[PATH] npm command: {0}" -f $installerCommandInfo.CommandPath) Write-PackageExecutionMessage -Message ("[PATH] npm stage: {0}" -f $stagePath) Write-PackageExecutionMessage -Message ("[PATH] npm cache: {0}" -f $cacheDirectory) Write-PackageExecutionMessage -Message ("[PATH] npm materialization manifest: {0}" -f [string]$PackageResult.NpmMaterialization.ManifestPath) Write-PackageExecutionMessage -Message ("[STATE] npm package spec: {0}" -f $packageSpec) try { & ([string]$installerCommandInfo.CommandPath) @cacheAddArguments $cacheAddExitCode = $LASTEXITCODE if ($null -eq $cacheAddExitCode) { $cacheAddExitCode = 0 } if ($cacheAddExitCode -ne 0) { throw "npm cache add for materialized package '$($PackageResult.PackageId)' failed with exit code $cacheAddExitCode." } Push-Location $stagePath try { & ([string]$installerCommandInfo.CommandPath) @commandArguments $exitCode = $LASTEXITCODE if ($null -eq $exitCode) { $exitCode = 0 } } finally { Pop-Location } if ($exitCode -ne 0) { throw "Package npm materialized install for '$($PackageResult.PackageId)' failed with exit code $exitCode." } $installParent = Split-Path -Parent $PackageResult.InstallDirectory if (-not [string]::IsNullOrWhiteSpace($installParent)) { $null = New-Item -ItemType Directory -Path $installParent -Force } Remove-PathIfExists -Path $PackageResult.InstallDirectory | Out-Null Move-Item -LiteralPath $stagePath -Destination $PackageResult.InstallDirectory -Force $stagePromoted = $true } finally { if (-not $stagePromoted) { Write-PackageExecutionMessage -Level 'WRN' -Message ("[WARN] Preserving failed npm materialized install stage '{0}' for inspection." -f $stagePath) } } return [pscustomobject]@{ Status = Get-PackageOwnedInstallStatus -PackageResult $PackageResult InstallKind = 'npmMaterializedInstallGlobalPackage' InstallDirectory = $PackageResult.InstallDirectory ReusedExisting = $false InstallerCommand = $installerCommandInfo.Command InstallerCommandPath = $installerCommandInfo.CommandPath PackageSpec = $packageSpec MaterializationManifestPath = [string]$PackageResult.NpmMaterialization.ManifestPath MaterializedTarballPaths = @($tarballPaths) CacheAddArguments = @($cacheAddArguments) CommandArguments = @($commandArguments) CacheDirectory = $cacheDirectory GlobalConfigPath = $globalConfigPath StagePath = $stagePath ExitCode = $exitCode } } |