fix(bootstrap): harden pinned checkout promotion
This commit is contained in:
@@ -23,6 +23,16 @@
|
||||
"metasploitmcp — Metasploit MCP Server (apt install metasploitmcp, port 8085)",
|
||||
"hexstrike-ai — 150+ 安全工具 MCP 自动化 (apt install hexstrike-ai)"
|
||||
],
|
||||
"bootstrapDependencies": {
|
||||
"pipx": {
|
||||
"package": "pipx==1.16.5",
|
||||
"version": "1.16.5"
|
||||
},
|
||||
"pnpm": {
|
||||
"package": "pnpm@10.24.0",
|
||||
"version": "10.24.0"
|
||||
}
|
||||
},
|
||||
"capabilities": [
|
||||
{
|
||||
"name": "jadx",
|
||||
|
||||
@@ -132,13 +132,24 @@ install_git_commit() {
|
||||
local install_dir="$3"
|
||||
|
||||
if [[ -d "$install_dir/.git" ]]; then
|
||||
local current
|
||||
current=$(git -C "$install_dir" rev-parse HEAD 2>/dev/null || true)
|
||||
local current status
|
||||
if ! current=$(git -C "$install_dir" rev-parse HEAD 2>/dev/null); then
|
||||
log_err "无法解析现有 checkout HEAD: $install_dir"
|
||||
return 1
|
||||
fi
|
||||
if [[ "$current" != "$commit" ]]; then
|
||||
log_err "Existing checkout is not at pinned commit $commit: $install_dir"
|
||||
log_err "Move it aside explicitly, then retry; bootstrap will not overwrite local changes."
|
||||
return 1
|
||||
fi
|
||||
if ! status=$(git -C "$install_dir" status --porcelain --untracked-files=all); then
|
||||
log_err "无法检查 checkout 状态: $install_dir"
|
||||
return 1
|
||||
fi
|
||||
if [[ -n "$status" ]]; then
|
||||
log_err "现有 checkout 含本地修改,拒绝执行: $install_dir"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
if [[ -e "$install_dir" ]]; then
|
||||
@@ -146,15 +157,33 @@ install_git_commit() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$install_dir")"
|
||||
git init -q "$install_dir"
|
||||
git -C "$install_dir" remote add origin "$repo"
|
||||
git -C "$install_dir" fetch --depth 1 origin "$commit"
|
||||
git -C "$install_dir" checkout -q --detach FETCH_HEAD
|
||||
local resolved
|
||||
resolved=$(git -C "$install_dir" rev-parse HEAD)
|
||||
local parent stage resolved status
|
||||
parent=$(dirname "$install_dir")
|
||||
mkdir -p "$parent"
|
||||
stage=$(mktemp -d "$parent/.reverse-bootstrap-XXXXXX") || return 1
|
||||
if ! git init -q "$stage" ||
|
||||
! git -C "$stage" remote add origin "$repo" ||
|
||||
! git -C "$stage" fetch --depth 1 origin "$commit" ||
|
||||
! git -C "$stage" checkout -q --detach FETCH_HEAD; then
|
||||
rm -rf "$stage"
|
||||
return 1
|
||||
fi
|
||||
if ! resolved=$(git -C "$stage" rev-parse HEAD); then
|
||||
rm -rf "$stage"
|
||||
return 1
|
||||
fi
|
||||
if [[ "$resolved" != "$commit" ]]; then
|
||||
log_err "Pinned checkout verification failed (expected $commit, got $resolved)"
|
||||
rm -rf "$stage"
|
||||
return 1
|
||||
fi
|
||||
if ! status=$(git -C "$stage" status --porcelain --untracked-files=all) || [[ -n "$status" ]]; then
|
||||
log_err "Staged checkout is not clean: $stage"
|
||||
rm -rf "$stage"
|
||||
return 1
|
||||
fi
|
||||
if ! mv -T "$stage" "$install_dir"; then
|
||||
rm -rf "$stage"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -331,6 +360,13 @@ manifest_field() {
|
||||
'.capabilities[] | select(.name == $name) | .[$field] // empty' "$KALI_MANIFEST"
|
||||
}
|
||||
|
||||
manifest_dependency() {
|
||||
local name="$1"
|
||||
local field="$2"
|
||||
jq -er --arg name "$name" --arg field "$field" \
|
||||
'.bootstrapDependencies[$name][$field] // empty' "$KALI_MANIFEST"
|
||||
}
|
||||
|
||||
install_manifest_release() {
|
||||
local capability="$1"
|
||||
local repo asset_regex install_dir release_tag asset_sha256
|
||||
@@ -636,11 +672,19 @@ start_anything_analyzer() {
|
||||
commit=$(manifest_field anything-analyzer pinnedCommit)
|
||||
install_git_commit "$repo" "$commit" "$repo_dir" || return 1
|
||||
|
||||
if ! command -v pnpm &>/dev/null; then
|
||||
npm install -g pnpm
|
||||
local pnpm_package pnpm_version current_pnpm_version=''
|
||||
pnpm_package=$(manifest_dependency pnpm package) || return 1
|
||||
pnpm_version=$(manifest_dependency pnpm version) || return 1
|
||||
if command -v pnpm &>/dev/null; then
|
||||
current_pnpm_version=$(pnpm --version 2>/dev/null | head -n1 | tr -d '[:space:]')
|
||||
fi
|
||||
if [[ "$current_pnpm_version" != "$pnpm_version" ]]; then
|
||||
npm install -g "$pnpm_package" || return 1
|
||||
fi
|
||||
|
||||
(cd "$repo_dir" && pnpm install && nohup pnpm dev > /tmp/anything-analyzer.log 2>&1 &)
|
||||
(cd "$repo_dir" && pnpm install --frozen-lockfile) || return 1
|
||||
install_git_commit "$repo" "$commit" "$repo_dir" || return 1
|
||||
(cd "$repo_dir" && nohup pnpm dev > /tmp/anything-analyzer.log 2>&1 &)
|
||||
|
||||
log_info "等待 anything-analyzer 启动 (port 23816) ..."
|
||||
if wait_for_port 23816 120; then
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
{
|
||||
"bootstrapDependencies": {
|
||||
"pipx": {
|
||||
"package": "pipx==1.16.5",
|
||||
"version": "1.16.5"
|
||||
},
|
||||
"pnpm": {
|
||||
"package": "pnpm@10.24.0",
|
||||
"version": "10.24.0"
|
||||
}
|
||||
},
|
||||
"capabilities": [
|
||||
{
|
||||
"name": "jadx",
|
||||
|
||||
@@ -24,6 +24,17 @@ $OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||
|
||||
. (Join-Path $PSScriptRoot 'lib\ToolDiscovery.ps1')
|
||||
|
||||
function Get-BootstrapDependency {
|
||||
param([Parameter(Mandatory = $true)][string]$Name)
|
||||
|
||||
$manifest = Get-Content -LiteralPath (Get-ReverseBootstrapManifestPath) -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
$dependency = $manifest.bootstrapDependencies.PSObject.Properties[$Name].Value
|
||||
if ($null -eq $dependency -or [string]::IsNullOrWhiteSpace([string]$dependency.package) -or [string]::IsNullOrWhiteSpace([string]$dependency.version)) {
|
||||
throw "bootstrapDependencies.$Name must define package and version."
|
||||
}
|
||||
return $dependency
|
||||
}
|
||||
|
||||
$Capability = @(
|
||||
foreach ($item in @($Capability)) {
|
||||
if ([string]::IsNullOrWhiteSpace($item)) {
|
||||
@@ -155,14 +166,23 @@ function Ensure-JavaRuntime {
|
||||
|
||||
function Ensure-Pnpm {
|
||||
Ensure-NodeRuntime
|
||||
if (-not (Get-NodeCommandPath -Name 'pnpm')) {
|
||||
$dependency = Get-BootstrapDependency -Name 'pnpm'
|
||||
$pnpm = Get-NodeCommandPath -Name 'pnpm'
|
||||
$currentVersion = ''
|
||||
if ($pnpm) {
|
||||
$versionLine = & $pnpm --version 2>$null | Select-Object -First 1
|
||||
if ($LASTEXITCODE -eq 0 -and $null -ne $versionLine) {
|
||||
$currentVersion = ([string]$versionLine).Trim()
|
||||
}
|
||||
}
|
||||
if ($currentVersion -ne [string]$dependency.version) {
|
||||
$npm = Get-NodeCommandPath -Name 'npm'
|
||||
if ([string]::IsNullOrWhiteSpace($npm)) {
|
||||
throw 'npm is not available after Node.js installation.'
|
||||
}
|
||||
& $npm install -g pnpm
|
||||
& $npm install -g ([string]$dependency.package)
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'Failed to install pnpm globally.'
|
||||
throw "Failed to install pinned pnpm dependency $($dependency.package)."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -341,34 +361,11 @@ function Set-AnythingAnalyzerPnpmBuildApprovals {
|
||||
|
||||
function Approve-AnythingAnalyzerBuildScripts {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$RepoDir,
|
||||
[Parameter(Mandatory = $true)][string]$PnpmPath
|
||||
[Parameter(Mandatory = $true)][string]$RepoDir
|
||||
)
|
||||
|
||||
$buildPackages = @('electron', 'esbuild', 'better-sqlite3')
|
||||
|
||||
Push-Location $RepoDir
|
||||
try {
|
||||
$approveExitCode = 1
|
||||
try {
|
||||
$approveOutput = & $PnpmPath approve-builds --all 2>&1
|
||||
$approveExitCode = $LASTEXITCODE
|
||||
}
|
||||
catch {
|
||||
$approveOutput = $_.Exception.Message
|
||||
$approveExitCode = 1
|
||||
}
|
||||
|
||||
if ($approveExitCode -eq 0) {
|
||||
return
|
||||
}
|
||||
|
||||
Write-Warning 'pnpm approve-builds --all is unavailable or failed; writing pnpm-workspace.yaml build approvals directly.'
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Set-AnythingAnalyzerPnpmBuildApprovals -RepoDir $RepoDir -Packages $buildPackages
|
||||
}
|
||||
|
||||
@@ -805,9 +802,13 @@ if (Test-ReverseIsWindows) {
|
||||
throw 'pnpm is not available after installation.'
|
||||
}
|
||||
|
||||
$workspacePath = Join-Path $repoDir 'pnpm-workspace.yaml'
|
||||
$workspaceExisted = Test-Path -LiteralPath $workspacePath -PathType Leaf
|
||||
$workspaceBytes = if ($workspaceExisted) { [IO.File]::ReadAllBytes($workspacePath) } else { $null }
|
||||
|
||||
Push-Location $repoDir
|
||||
try {
|
||||
Approve-AnythingAnalyzerBuildScripts -RepoDir $repoDir -PnpmPath $pnpm
|
||||
Approve-AnythingAnalyzerBuildScripts -RepoDir $repoDir
|
||||
|
||||
if (-not (Test-AnythingAnalyzerElectronHealthy -RepoDir $repoDir -PnpmPath $pnpm)) {
|
||||
$nodeModules = Join-Path $repoDir 'node_modules'
|
||||
@@ -816,7 +817,7 @@ if (Test-ReverseIsWindows) {
|
||||
}
|
||||
}
|
||||
|
||||
& $pnpm install
|
||||
& $pnpm install --frozen-lockfile
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($vsBuildToolsError)) {
|
||||
throw "pnpm install failed for anything-analyzer. Visual Studio Build Tools auto-install also failed earlier: $vsBuildToolsError"
|
||||
@@ -838,8 +839,17 @@ if (Test-ReverseIsWindows) {
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
if ($workspaceExisted) {
|
||||
[IO.File]::WriteAllBytes($workspacePath, $workspaceBytes)
|
||||
}
|
||||
elseif (Test-Path -LiteralPath $workspacePath) {
|
||||
Remove-Item -LiteralPath $workspacePath -Force
|
||||
}
|
||||
}
|
||||
|
||||
$git = Get-FirstCommandPath -Names @('git')
|
||||
Assert-GitCheckoutState -GitPath $git -CheckoutPath $repoDir -PinnedCommit ([string]$Definition.pinnedCommit)
|
||||
|
||||
$stdoutLog = Join-Path $repoDir 'anything-analyzer-dev.log'
|
||||
$stderrLog = Join-Path $repoDir 'anything-analyzer-dev.err.log'
|
||||
Remove-Item -LiteralPath $stdoutLog, $stderrLog -Force -ErrorAction SilentlyContinue
|
||||
@@ -879,6 +889,27 @@ function Ensure-AndroidPlatformTools {
|
||||
return (Resolve-ReverseToolSpec -Name 'adb')
|
||||
}
|
||||
|
||||
function Assert-GitCheckoutState {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$GitPath,
|
||||
[Parameter(Mandatory = $true)][string]$CheckoutPath,
|
||||
[Parameter(Mandatory = $true)][string]$PinnedCommit
|
||||
)
|
||||
|
||||
$resolvedLine = & $GitPath -C $CheckoutPath rev-parse HEAD 2>$null | Select-Object -First 1
|
||||
$resolvedCommit = if ($null -eq $resolvedLine) { '' } else { ([string]$resolvedLine).Trim() }
|
||||
if ($LASTEXITCODE -ne 0 -or $resolvedCommit -ne $PinnedCommit) {
|
||||
throw "Checkout verification failed: expected $PinnedCommit, got $resolvedCommit ($CheckoutPath)"
|
||||
}
|
||||
$status = @(& $GitPath -C $CheckoutPath status --porcelain --untracked-files=all 2>&1)
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Cannot inspect checkout state: $CheckoutPath"
|
||||
}
|
||||
if ($status.Count -gt 0) {
|
||||
throw "Checkout has local changes; refusing to execute it: $CheckoutPath"
|
||||
}
|
||||
}
|
||||
|
||||
function Ensure-GitCloneInstall {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]$Definition,
|
||||
@@ -892,36 +923,42 @@ function Ensure-GitCloneInstall {
|
||||
}
|
||||
|
||||
if ((Test-Path -LiteralPath $TargetPath -PathType Container) -and (Test-Path -LiteralPath (Join-Path $TargetPath '.git'))) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($pinnedCommit)) {
|
||||
$currentCommit = (& $git -C $TargetPath rev-parse HEAD).Trim()
|
||||
if ($LASTEXITCODE -ne 0 -or $currentCommit -ne $pinnedCommit) {
|
||||
throw "Existing checkout is not at pinned commit $pinnedCommit. Move it aside explicitly, then retry: $TargetPath"
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($pinnedCommit)) {
|
||||
throw "Git capability $($Definition.repo) must define pinnedCommit before an existing checkout can be used."
|
||||
}
|
||||
Assert-GitCheckoutState -GitPath $git -CheckoutPath $TargetPath -PinnedCommit $pinnedCommit
|
||||
return $true
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $TargetPath) {
|
||||
$backupPath = "$TargetPath.bak-$([DateTime]::UtcNow.ToString('yyyyMMddHHmmss'))"
|
||||
Move-Item -LiteralPath $TargetPath -Destination $backupPath -Force
|
||||
throw "Install path exists but is not a git checkout: $TargetPath"
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($pinnedCommit)) {
|
||||
throw "Git capability $($Definition.repo) must define pinnedCommit."
|
||||
}
|
||||
|
||||
Ensure-DownloadDirectory -Path (Split-Path -Path $TargetPath -Parent)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($pinnedCommit)) {
|
||||
& $git clone --depth 1 $Definition.repo $TargetPath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "git clone failed for $($Definition.repo)"
|
||||
$parent = Split-Path -Path $TargetPath -Parent
|
||||
Ensure-DownloadDirectory -Path $parent
|
||||
$stagePath = Join-Path $parent ('.reverse-bootstrap-{0}' -f [Guid]::NewGuid().ToString('N'))
|
||||
New-Item -ItemType Directory -Path $stagePath | Out-Null
|
||||
try {
|
||||
& $git init --quiet $stagePath
|
||||
if ($LASTEXITCODE -ne 0) { throw 'git init failed' }
|
||||
& $git -C $stagePath remote add origin $Definition.repo
|
||||
if ($LASTEXITCODE -ne 0) { throw 'git remote add failed' }
|
||||
& $git -C $stagePath fetch --depth 1 origin $pinnedCommit
|
||||
if ($LASTEXITCODE -ne 0) { throw 'git fetch failed' }
|
||||
& $git -C $stagePath checkout --quiet --detach FETCH_HEAD
|
||||
if ($LASTEXITCODE -ne 0) { throw 'git checkout failed' }
|
||||
Assert-GitCheckoutState -GitPath $git -CheckoutPath $stagePath -PinnedCommit $pinnedCommit
|
||||
Move-Item -LiteralPath $stagePath -Destination $TargetPath
|
||||
if ((Test-Path -LiteralPath $stagePath) -or -not (Test-Path -LiteralPath (Join-Path $TargetPath '.git') -PathType Container)) {
|
||||
throw "Failed to promote staged checkout to $TargetPath"
|
||||
}
|
||||
}
|
||||
else {
|
||||
& $git init --quiet $TargetPath
|
||||
& $git -C $TargetPath remote add origin $Definition.repo
|
||||
& $git -C $TargetPath fetch --depth 1 origin $pinnedCommit
|
||||
& $git -C $TargetPath checkout --quiet --detach FETCH_HEAD
|
||||
$resolvedCommit = (& $git -C $TargetPath rev-parse HEAD).Trim()
|
||||
if ($LASTEXITCODE -ne 0 -or $resolvedCommit -ne $pinnedCommit) {
|
||||
throw "Pinned checkout verification failed for $($Definition.repo): expected $pinnedCommit, got $resolvedCommit"
|
||||
finally {
|
||||
if (Test-Path -LiteralPath $stagePath) {
|
||||
Remove-Item -LiteralPath $stagePath -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,19 @@ raise SystemExit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
manifest_dependency() {
|
||||
local name="$1"
|
||||
local field="$2"
|
||||
python3 - "$MANIFEST_PATH" "$name" "$field" <<'PY'
|
||||
import json, pathlib, sys
|
||||
manifest = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8'))
|
||||
value = manifest.get('bootstrapDependencies', {}).get(sys.argv[2], {}).get(sys.argv[3])
|
||||
if value is None or value == '':
|
||||
raise SystemExit(1)
|
||||
print(value)
|
||||
PY
|
||||
}
|
||||
|
||||
safe_remove_install_dir() {
|
||||
local target="$1"
|
||||
local tmp_target="${2:-}"
|
||||
@@ -218,15 +231,15 @@ ensure_python_runtime() {
|
||||
*) log_err "Install Python 3 manually. See $(platform_doc)"; return 1 ;;
|
||||
esac
|
||||
fi
|
||||
if ! has_cmd pipx; then
|
||||
case "$PLATFORM" in
|
||||
macos)
|
||||
python3 -m pip install --user pipx || install_brew pipx
|
||||
;;
|
||||
linux)
|
||||
install_apt pipx || python3 -m pip install --user pipx
|
||||
;;
|
||||
esac
|
||||
local pipx_package pipx_version current_version
|
||||
pipx_package=$(manifest_dependency pipx package) || return 1
|
||||
pipx_version=$(manifest_dependency pipx version) || return 1
|
||||
current_version=""
|
||||
if has_cmd pipx; then
|
||||
current_version=$(pipx --version 2>/dev/null | head -n1 | tr -d '[:space:]')
|
||||
fi
|
||||
if [[ "$current_version" != "$pipx_version" ]]; then
|
||||
python3 -m pip install --user --upgrade "$pipx_package" || return 1
|
||||
fi
|
||||
python3 -m pipx ensurepath >/dev/null 2>&1 || true
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
@@ -251,10 +264,17 @@ ensure_java_runtime() {
|
||||
}
|
||||
|
||||
ensure_pnpm() {
|
||||
ensure_node_runtime
|
||||
if has_cmd pnpm; then return 0; fi
|
||||
if has_cmd corepack; then corepack enable || true; fi
|
||||
if ! has_cmd pnpm; then npm install -g pnpm; fi
|
||||
ensure_node_runtime || return 1
|
||||
local package version current_version
|
||||
package=$(manifest_dependency pnpm package) || return 1
|
||||
version=$(manifest_dependency pnpm version) || return 1
|
||||
current_version=""
|
||||
if has_cmd pnpm; then
|
||||
current_version=$(pnpm --version 2>/dev/null | head -n1 | tr -d '[:space:]')
|
||||
fi
|
||||
if [[ "$current_version" != "$version" ]]; then
|
||||
npm install -g "$package" || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Args: repo regex [release_tag]
|
||||
@@ -378,14 +398,40 @@ install_git_commit() {
|
||||
local commit="$2"
|
||||
local install_dir="$3"
|
||||
|
||||
git_checkout_is_clean() {
|
||||
local checkout="$1"
|
||||
local status
|
||||
if ! status=$(git -C "$checkout" status --porcelain --untracked-files=all); then
|
||||
log_err "Cannot inspect checkout state: $checkout"
|
||||
return 1
|
||||
fi
|
||||
if [[ -n "$status" ]]; then
|
||||
log_err "Existing checkout has local changes; refusing to execute it: $checkout"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup_git_stage() {
|
||||
local stage="$1"
|
||||
local parent="$2"
|
||||
case "$stage" in
|
||||
"$parent"/.reverse-bootstrap-*) rm -rf "$stage" ;;
|
||||
*) log_err "Refusing to clean unexpected staging path: $stage" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [[ -d "$install_dir/.git" ]]; then
|
||||
local current
|
||||
current=$(git -C "$install_dir" rev-parse HEAD 2>/dev/null || true)
|
||||
if ! current=$(git -C "$install_dir" rev-parse HEAD 2>/dev/null); then
|
||||
log_err "Cannot resolve existing checkout HEAD: $install_dir"
|
||||
return 1
|
||||
fi
|
||||
if [[ "$current" != "$commit" ]]; then
|
||||
log_err "Existing checkout is not at pinned commit $commit: $install_dir"
|
||||
log_err "Move it aside explicitly, then retry; bootstrap will not overwrite local changes."
|
||||
return 1
|
||||
fi
|
||||
git_checkout_is_clean "$install_dir" || return 1
|
||||
return 0
|
||||
fi
|
||||
if [[ -e "$install_dir" ]]; then
|
||||
@@ -393,15 +439,36 @@ install_git_commit() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
ensure_dir "$(dirname "$install_dir")"
|
||||
git init --quiet "$install_dir"
|
||||
git -C "$install_dir" remote add origin "$repo"
|
||||
git -C "$install_dir" fetch --depth 1 origin "$commit"
|
||||
git -C "$install_dir" checkout --quiet --detach FETCH_HEAD
|
||||
local resolved
|
||||
resolved=$(git -C "$install_dir" rev-parse HEAD)
|
||||
local parent stage resolved
|
||||
parent=$(dirname "$install_dir")
|
||||
ensure_dir "$parent"
|
||||
stage=$(mktemp -d "$parent/.reverse-bootstrap-XXXXXX") || return 1
|
||||
if ! git init --quiet "$stage" ||
|
||||
! git -C "$stage" remote add origin "$repo" ||
|
||||
! git -C "$stage" fetch --depth 1 origin "$commit" ||
|
||||
! git -C "$stage" checkout --quiet --detach FETCH_HEAD; then
|
||||
cleanup_git_stage "$stage" "$parent"
|
||||
return 1
|
||||
fi
|
||||
if ! resolved=$(git -C "$stage" rev-parse HEAD); then
|
||||
cleanup_git_stage "$stage" "$parent"
|
||||
return 1
|
||||
fi
|
||||
if [[ "$resolved" != "$commit" ]]; then
|
||||
log_err "Pinned checkout verification failed for $repo: expected $commit, got $resolved"
|
||||
cleanup_git_stage "$stage" "$parent"
|
||||
return 1
|
||||
fi
|
||||
if ! git_checkout_is_clean "$stage"; then
|
||||
cleanup_git_stage "$stage" "$parent"
|
||||
return 1
|
||||
fi
|
||||
if ! python3 - "$stage" "$install_dir" <<'PY'
|
||||
import os, sys
|
||||
os.rename(sys.argv[1], sys.argv[2])
|
||||
PY
|
||||
then
|
||||
cleanup_git_stage "$stage" "$parent"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -539,7 +606,7 @@ ensure_apktool() {
|
||||
}
|
||||
|
||||
ensure_frida_tools() {
|
||||
ensure_python_runtime
|
||||
ensure_python_runtime || return 1
|
||||
if has_cmd frida && has_cmd frida-ps; then log_ok "frida-tools ready"; return 0; fi
|
||||
local package
|
||||
package=$(manifest_field frida pipPackage)
|
||||
@@ -548,7 +615,7 @@ ensure_frida_tools() {
|
||||
}
|
||||
|
||||
ensure_idalib_mcp() {
|
||||
ensure_python_runtime
|
||||
ensure_python_runtime || return 1
|
||||
if has_cmd ida-pro-mcp; then log_ok "ida-pro-mcp ready: $(cmd_path ida-pro-mcp)"; return 0; fi
|
||||
local source
|
||||
source=$(manifest_field idalib-mcp pipSource)
|
||||
@@ -558,7 +625,7 @@ ensure_idalib_mcp() {
|
||||
}
|
||||
|
||||
ensure_jshookmcp() {
|
||||
ensure_node_runtime
|
||||
ensure_node_runtime || return 1
|
||||
local package
|
||||
package=$(manifest_field jshookmcp npmPackage)
|
||||
write_mcp_server "jshook" "$(python3 - "$package" <<'PY'
|
||||
@@ -569,7 +636,7 @@ PY
|
||||
}
|
||||
|
||||
ensure_reqable_mcp() {
|
||||
ensure_node_runtime
|
||||
ensure_node_runtime || return 1
|
||||
local package
|
||||
package=$(manifest_field reqable-mcp npmPackage)
|
||||
write_mcp_server "reqable-mcp" "$(python3 - "$package" <<'PY'
|
||||
@@ -589,11 +656,13 @@ ensure_anything_analyzer() {
|
||||
case "$PLATFORM" in macos) install_brew git ;; linux) install_apt git ;; esac
|
||||
fi
|
||||
install_git_commit "$repo" "$commit" "$dir" || return 1
|
||||
ensure_node_runtime
|
||||
ensure_pnpm
|
||||
ensure_node_runtime || return 1
|
||||
ensure_pnpm || return 1
|
||||
write_mcp_server "anything-analyzer" '{"url":"http://localhost:23816/mcp"}'
|
||||
if $START_SERVICES; then
|
||||
(cd "$dir" && pnpm install && nohup pnpm dev >/tmp/anything-analyzer.log 2>&1 &)
|
||||
(cd "$dir" && pnpm install --frozen-lockfile) || return 1
|
||||
install_git_commit "$repo" "$commit" "$dir" || return 1
|
||||
(cd "$dir" && nohup pnpm dev >/tmp/anything-analyzer.log 2>&1 &)
|
||||
if wait_for_port 23816 120; then
|
||||
if test_mcp_http 23816; then
|
||||
log_ok "anything-analyzer MCP server ready on port 23816 (HTTP verified)"
|
||||
@@ -647,7 +716,7 @@ ensure_adb() {
|
||||
}
|
||||
|
||||
ensure_agent_browser() {
|
||||
ensure_node_runtime
|
||||
ensure_node_runtime || return 1
|
||||
if has_cmd agent-browser; then log_ok "agent-browser ready"; return 0; fi
|
||||
local package
|
||||
package=$(manifest_field agent-browser npmPackage)
|
||||
@@ -658,7 +727,7 @@ ensure_agent_browser() {
|
||||
}
|
||||
|
||||
ensure_ghidra_mcp() {
|
||||
ensure_java_runtime
|
||||
ensure_java_runtime || return 1
|
||||
local repo regex
|
||||
repo=$(manifest_field ghidra-mcp repo)
|
||||
regex=$(manifest_field ghidra-mcp assetRegex)
|
||||
@@ -689,7 +758,7 @@ ensure_seclists() {
|
||||
}
|
||||
|
||||
ensure_proxycat() {
|
||||
ensure_python_runtime
|
||||
ensure_python_runtime || return 1
|
||||
if has_cmd proxycat; then log_ok "proxycat ready"; return 0; fi
|
||||
local repo commit
|
||||
repo=$(manifest_field proxycat repo)
|
||||
@@ -785,7 +854,7 @@ ensure_yara() {
|
||||
}
|
||||
|
||||
ensure_pwntools() {
|
||||
ensure_python_runtime
|
||||
ensure_python_runtime || return 1
|
||||
if python3 -c "import pwn" 2>/dev/null; then log_ok "pwntools ready"; return 0; fi
|
||||
local package
|
||||
package=$(manifest_field pwntools pipPackage)
|
||||
|
||||
@@ -3,12 +3,11 @@ set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BOOTSTRAP="$SCRIPT_DIR/bootstrap-reverse.sh"
|
||||
MANIFEST="$SCRIPT_DIR/bootstrap-manifest.json"
|
||||
KALI_BOOTSTRAP="$SCRIPT_DIR/../../kali/scripts/bootstrap-reverse.sh"
|
||||
MANIFEST="$SCRIPT_DIR/bootstrap-manifest.json"
|
||||
REAL_PYTHON="$(command -v python3)"
|
||||
SCRATCH="$(mktemp -d /tmp/reverse-bootstrap-test-XXXXXX)"
|
||||
trap 'rm -rf "$SCRATCH"' EXIT
|
||||
|
||||
STUB_BIN="$SCRATCH/bin"
|
||||
CALL_LOG="$SCRATCH/calls.log"
|
||||
mkdir -p "$STUB_BIN" "$SCRATCH/home" "$SCRATCH/tools"
|
||||
@@ -16,223 +15,152 @@ mkdir -p "$STUB_BIN" "$SCRATCH/home" "$SCRATCH/tools"
|
||||
cat > "$STUB_BIN/command-stub" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
name="$(basename "$0")"
|
||||
{
|
||||
printf '%s' "$name"
|
||||
for arg in "$@"; do printf '|%s' "$arg"; done
|
||||
printf '\n'
|
||||
} >> "$CALL_LOG"
|
||||
|
||||
if [[ "${STUB_FAIL_COMMAND:-}" == "$name" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$name" == "git" ]]; then
|
||||
if [[ "${1:-}" == "init" ]]; then
|
||||
target="${!#}"
|
||||
mkdir -p "$target/.git"
|
||||
printf '%s\n' 'unpinned-head' > "$target/.stub-head"
|
||||
elif [[ "${1:-}" == "-C" && "${3:-}" == "fetch" ]]; then
|
||||
printf '%s\n' "${7:-}" > "$2/.stub-fetch"
|
||||
elif [[ "${1:-}" == "-C" && "${3:-}" == "checkout" ]]; then
|
||||
cat "$2/.stub-fetch" > "$2/.stub-head"
|
||||
elif [[ "${1:-}" == "-C" && "${3:-}" == "rev-parse" ]]; then
|
||||
cat "$2/.stub-head"
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
{ printf '%s' "$name"; for arg in "$@"; do printf '|%s' "$arg"; done; printf '\n'; } >> "$CALL_LOG"
|
||||
case "$name:${1:-}" in
|
||||
pipx:--version) printf '%s\n' "${STUB_PIPX_VERSION:-0}" ;;
|
||||
pnpm:--version) printf '%s\n' "${STUB_PNPM_VERSION:-0}" ;;
|
||||
git:init)
|
||||
target="${!#}"; mkdir -p "$target/.git"; printf '%s\n' unpinned-head > "$target/.stub-head"
|
||||
;;
|
||||
git:-C)
|
||||
case "${3:-}" in
|
||||
fetch)
|
||||
[[ "${STUB_FAIL_FETCH:-0}" != 1 ]] || exit 1
|
||||
printf '%s\n' "${7:-}" > "$2/.stub-fetch"
|
||||
;;
|
||||
checkout) cp "$2/.stub-fetch" "$2/.stub-head" ;;
|
||||
rev-parse) cat "$2/.stub-head" ;;
|
||||
status) [[ ! -e "$2/.stub-dirty" ]] || printf '%s\n' '?? .npmrc' ;;
|
||||
esac
|
||||
;;
|
||||
nc:-z)
|
||||
count=0; [[ ! -f "$STUB_NC_STATE" ]] || count="$(cat "$STUB_NC_STATE")"
|
||||
printf '%s\n' "$((count + 1))" > "$STUB_NC_STATE"
|
||||
(( count > 0 )) && exit 0 || exit 1
|
||||
;;
|
||||
esac
|
||||
[[ "${STUB_FAIL_COMMAND:-}" != "$name" ]]
|
||||
STUB
|
||||
chmod +x "$STUB_BIN/command-stub"
|
||||
for command_name in git node npm npx pipx pnpm sleep; do
|
||||
ln -s command-stub "$STUB_BIN/$command_name"
|
||||
done
|
||||
for name in git node npm npx pipx pnpm sleep nc; do ln -s command-stub "$STUB_BIN/$name"; done
|
||||
|
||||
cat > "$STUB_BIN/python3" <<STUB
|
||||
#!/usr/bin/env bash
|
||||
if [[ "\${1:-}" == "-" && "\${2:-}" == "23816" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
if [[ "\${1:-}" == "-c" && "\${2:-}" == "import pwn" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
{ printf 'python3'; for arg in "\$@"; do printf '|%s' "\$arg"; done; printf '\n'; } >> "\$CALL_LOG"
|
||||
if [[ "\${1:-}" == '-m' && "\${2:-}" == pip ]]; then [[ "\${STUB_FAIL_PIP_INSTALL:-0}" != 1 ]]; exit; fi
|
||||
if [[ "\${1:-}" == '-m' && "\${2:-}" == pipx ]]; then exit 0; fi
|
||||
if [[ "\${1:-}" == '-c' && "\${2:-}" == 'import pwn' ]]; then exit 1; fi
|
||||
if [[ "\${1:-}" == '-' && "\${2:-}" == 23816 ]]; then exit 0; fi
|
||||
exec "$REAL_PYTHON" "\$@"
|
||||
STUB
|
||||
chmod +x "$STUB_BIN/python3"
|
||||
|
||||
manifest_value() {
|
||||
json_value() {
|
||||
"$REAL_PYTHON" - "$MANIFEST" "$1" "$2" <<'PY'
|
||||
import json, pathlib, sys
|
||||
manifest = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8'))
|
||||
capability = next(item for item in manifest['capabilities'] if item['name'] == sys.argv[2])
|
||||
value = capability.get(sys.argv[3], '')
|
||||
print(value if isinstance(value, str) else json.dumps(value, separators=(',', ':')))
|
||||
d=json.loads(pathlib.Path(sys.argv[1]).read_text())
|
||||
if sys.argv[2] == 'dependency': v=d['bootstrapDependencies'][sys.argv[3]]['package']
|
||||
else: v=next(x for x in d['capabilities'] if x['name']==sys.argv[2])[sys.argv[3]]
|
||||
print(v)
|
||||
PY
|
||||
}
|
||||
|
||||
run_bootstrap() {
|
||||
env \
|
||||
PATH="$STUB_BIN:/usr/bin:/bin" \
|
||||
HOME="$SCRATCH/home" \
|
||||
CALL_LOG="$CALL_LOG" \
|
||||
REVERSE_SKILL_TOOLS_DIR="$SCRATCH/tools" \
|
||||
CLAUDE_MCP_CONFIG="$SCRATCH/home/mcp.json" \
|
||||
bash "$BOOTSTRAP" "$@"
|
||||
run_generic() {
|
||||
env PATH="$STUB_BIN:/usr/bin:/bin" HOME="$SCRATCH/home" CALL_LOG="$CALL_LOG" \
|
||||
STUB_PIPX_VERSION="${STUB_PIPX_VERSION:-}" STUB_PNPM_VERSION="${STUB_PNPM_VERSION:-}" \
|
||||
STUB_FAIL_PIP_INSTALL="${STUB_FAIL_PIP_INSTALL:-0}" STUB_FAIL_FETCH="${STUB_FAIL_FETCH:-0}" \
|
||||
REVERSE_SKILL_TOOLS_DIR="${TEST_TOOLS_ROOT:-$SCRATCH/tools}" \
|
||||
CLAUDE_MCP_CONFIG="$SCRATCH/home/mcp.json" bash "$BOOTSTRAP" "$@"
|
||||
}
|
||||
run_kali() {
|
||||
rm -f "$SCRATCH/nc-count"
|
||||
env PATH="$STUB_BIN:/opt/homebrew/bin:/usr/bin:/bin" HOME="$SCRATCH/home" \
|
||||
CALL_LOG="$CALL_LOG" STUB_NC_STATE="$SCRATCH/nc-count" STUB_PNPM_VERSION="${STUB_PNPM_VERSION:-}" \
|
||||
STUB_FAIL_FETCH="${STUB_FAIL_FETCH:-0}" bash "$KALI_BOOTSTRAP" "$@"
|
||||
}
|
||||
expect_line() { grep -Fqx "$1" "$CALL_LOG" || { echo "missing argv: $1" >&2; cat "$CALL_LOG" >&2; return 1; }; }
|
||||
expect_fragment() { grep -Fq "$1" "$CALL_LOG" || { echo "missing argv fragment: $1" >&2; cat "$CALL_LOG" >&2; return 1; }; }
|
||||
rejects_without_pnpm() {
|
||||
local runner="$1"; shift
|
||||
: > "$CALL_LOG"; set +e; "$runner" "$@" >/dev/null 2>&1; local rc=$?; set -e
|
||||
[[ $rc -ne 0 ]] && ! grep -Eq '^pnpm\|(install|dev)' "$CALL_LOG"
|
||||
}
|
||||
|
||||
run_bootstrap_with_failing_pipx() {
|
||||
env \
|
||||
PATH="$STUB_BIN:/usr/bin:/bin" \
|
||||
HOME="$SCRATCH/home" \
|
||||
CALL_LOG="$CALL_LOG" \
|
||||
STUB_FAIL_COMMAND=pipx \
|
||||
REVERSE_SKILL_TOOLS_DIR="$SCRATCH/tools" \
|
||||
CLAUDE_MCP_CONFIG="$SCRATCH/home/mcp.json" \
|
||||
bash "$BOOTSTRAP" "$@"
|
||||
}
|
||||
pipx_package=$(json_value dependency pipx)
|
||||
pnpm_package=$(json_value dependency pnpm)
|
||||
anything_repo=$(json_value anything-analyzer repoUrl)
|
||||
anything_pin=$(json_value anything-analyzer pinnedCommit)
|
||||
|
||||
run_kali_bootstrap() {
|
||||
env \
|
||||
PATH="$STUB_BIN:/opt/homebrew/bin:/usr/bin:/bin" \
|
||||
HOME="$SCRATCH/home" \
|
||||
CALL_LOG="$CALL_LOG" \
|
||||
bash "$KALI_BOOTSTRAP" "$@"
|
||||
}
|
||||
|
||||
failures=0
|
||||
check_log_line() {
|
||||
if ! grep -Fqx "$1" "$CALL_LOG"; then
|
||||
printf 'missing argv: %s\n' "$1" >&2
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
: > "$CALL_LOG"
|
||||
run_bootstrap frida --skip-refresh >/dev/null
|
||||
frida_package="$(manifest_value frida pipPackage)"
|
||||
check_log_line "pipx|install|--force|$frida_package"
|
||||
|
||||
: > "$CALL_LOG"
|
||||
run_bootstrap idalib-mcp --skip-refresh >/dev/null
|
||||
idalib_source="$(manifest_value idalib-mcp pipSource)"
|
||||
check_log_line "pipx|install|--force|$idalib_source"
|
||||
|
||||
: > "$CALL_LOG"
|
||||
set +e
|
||||
run_bootstrap_with_failing_pipx idalib-mcp --skip-refresh >/dev/null 2>&1
|
||||
idalib_fail_exit=$?
|
||||
set -e
|
||||
if [[ "$idalib_fail_exit" -eq 0 ]]; then
|
||||
printf 'idalib-mcp reported success after its pinned install failed\n' >&2
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
check_log_line "pipx|install|--force|$idalib_source"
|
||||
if [[ "$(wc -l < "$CALL_LOG" | tr -d ' ')" -ne 1 ]]; then
|
||||
printf 'idalib-mcp attempted an unpinned fallback after pinned install failure\n' >&2
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
|
||||
: > "$CALL_LOG"
|
||||
run_bootstrap agent-browser --skip-refresh >/dev/null
|
||||
agent_package="$(manifest_value agent-browser npmPackage)"
|
||||
check_log_line "npm|install|-g|$agent_package"
|
||||
|
||||
: > "$CALL_LOG"
|
||||
run_bootstrap seclists --skip-refresh >/dev/null
|
||||
seclists_repo="$(manifest_value seclists repo)"
|
||||
seclists_pin="$(manifest_value seclists pinnedCommit)"
|
||||
seclists_dir="$SCRATCH/tools/SecLists"
|
||||
check_log_line "git|-C|$seclists_dir|remote|add|origin|$seclists_repo"
|
||||
check_log_line "git|-C|$seclists_dir|fetch|--depth|1|origin|$seclists_pin"
|
||||
|
||||
: > "$CALL_LOG"
|
||||
run_bootstrap proxycat --skip-refresh >/dev/null
|
||||
proxycat_repo="$(manifest_value proxycat repo)"
|
||||
proxycat_pin="$(manifest_value proxycat pinnedCommit)"
|
||||
check_log_line "pipx|install|git+${proxycat_repo}@${proxycat_pin}"
|
||||
|
||||
: > "$CALL_LOG"
|
||||
run_bootstrap pwntools --skip-refresh >/dev/null
|
||||
pwntools_package="$(manifest_value pwntools pipPackage)"
|
||||
check_log_line "pipx|install|$pwntools_package"
|
||||
|
||||
: > "$CALL_LOG"
|
||||
run_bootstrap anything-analyzer --start-services --skip-refresh >/dev/null
|
||||
anything_repo="$(manifest_value anything-analyzer repoUrl)"
|
||||
anything_pin="$(manifest_value anything-analyzer pinnedCommit)"
|
||||
anything_dir="$SCRATCH/tools/anything-analyzer"
|
||||
if [[ -z "$anything_pin" ]]; then
|
||||
printf 'anything-analyzer is missing pinnedCommit in bootstrap-manifest.json\n' >&2
|
||||
failures=$((failures + 1))
|
||||
else
|
||||
check_log_line "git|init|--quiet|$anything_dir"
|
||||
check_log_line "git|-C|$anything_dir|remote|add|origin|$anything_repo"
|
||||
check_log_line "git|-C|$anything_dir|fetch|--depth|1|origin|$anything_pin"
|
||||
check_log_line "git|-C|$anything_dir|checkout|--quiet|--detach|FETCH_HEAD"
|
||||
check_log_line "git|-C|$anything_dir|rev-parse|HEAD"
|
||||
fi
|
||||
check_log_line 'pnpm|install'
|
||||
check_log_line 'pnpm|dev'
|
||||
|
||||
if [[ -n "$anything_pin" ]]; then
|
||||
checkout_line="$(grep -nF "git|-C|$anything_dir|checkout|--quiet|--detach|FETCH_HEAD" "$CALL_LOG" | cut -d: -f1 | head -n1)"
|
||||
install_line="$(grep -nF 'pnpm|install' "$CALL_LOG" | cut -d: -f1 | head -n1)"
|
||||
if [[ -z "$checkout_line" || -z "$install_line" || "$checkout_line" -ge "$install_line" ]]; then
|
||||
printf 'anything-analyzer dependencies ran before the pinned checkout\n' >&2
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
: > "$CALL_LOG"
|
||||
mkdir -p "$anything_dir/.git"
|
||||
printf '%s\n' 'different-commit' > "$anything_dir/.stub-head"
|
||||
set +e
|
||||
run_bootstrap anything-analyzer --start-services --skip-refresh >/dev/null 2>&1
|
||||
mismatch_exit=$?
|
||||
set -e
|
||||
if [[ "$mismatch_exit" -eq 0 ]]; then
|
||||
printf 'anything-analyzer accepted an existing checkout at a different commit\n' >&2
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
if grep -Eq '^pnpm\|(install|dev)$' "$CALL_LOG"; then
|
||||
printf 'anything-analyzer ran dependencies from an unpinned existing checkout\n' >&2
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
|
||||
if (( BASH_VERSINFO[0] >= 4 )); then
|
||||
# Table: each generic package-manager sink receives its canonical manifest value.
|
||||
while IFS='|' read -r capability field expected; do
|
||||
: > "$CALL_LOG"
|
||||
STUB_PIPX_VERSION=1.16.5 run_generic "$capability" --skip-refresh >/dev/null
|
||||
expect_line "$expected"
|
||||
done <<EOF
|
||||
frida|pipPackage|pipx|install|--force|$(json_value frida pipPackage)
|
||||
idalib-mcp|pipSource|pipx|install|--force|$(json_value idalib-mcp pipSource)
|
||||
agent-browser|npmPackage|npm|install|-g|$(json_value agent-browser npmPackage)
|
||||
proxycat|repo|pipx|install|git+$(json_value proxycat repo)@$(json_value proxycat pinnedCommit)
|
||||
pwntools|pipPackage|pipx|install|$(json_value pwntools pipPackage)
|
||||
EOF
|
||||
|
||||
# pipx itself is pinned; a failed pinned install has no mutable fallback.
|
||||
: > "$CALL_LOG"
|
||||
STUB_FAIL_PIP_INSTALL=1 run_generic frida --skip-refresh >/dev/null 2>&1 && exit 1 || true
|
||||
expect_line "python3|-m|pip|install|--user|--upgrade|$pipx_package"
|
||||
[[ $(grep -c '|pip|install|' "$CALL_LOG") -eq 1 ]]
|
||||
! grep -Eq '^pipx\|(install|upgrade)' "$CALL_LOG"
|
||||
|
||||
# Generic Anything Analyzer: staged checkout, pinned pnpm, frozen install, clean recheck, then dev.
|
||||
: > "$CALL_LOG"
|
||||
STUB_PIPX_VERSION=1.16.5 STUB_PNPM_VERSION=0 run_generic anything-analyzer --start-services --skip-refresh >/dev/null
|
||||
anything_dir="$SCRATCH/tools/anything-analyzer"
|
||||
expect_line "npm|install|-g|$pnpm_package"
|
||||
expect_line 'pnpm|install|--frozen-lockfile'
|
||||
expect_line 'pnpm|dev'
|
||||
expect_fragment "remote|add|origin|$anything_repo"
|
||||
expect_fragment "fetch|--depth|1|origin|$anything_pin"
|
||||
[[ -d "$anything_dir/.git" ]]
|
||||
[[ $(grep -c '|status|--porcelain|--untracked-files=all' "$CALL_LOG") -ge 2 ]]
|
||||
|
||||
# Dirty sources never reach install/dev.
|
||||
touch "$anything_dir/.stub-dirty"
|
||||
rejects_without_pnpm run_generic anything-analyzer --start-services --skip-refresh
|
||||
rm "$anything_dir/.stub-dirty"
|
||||
|
||||
# Failed fetch leaves no final checkout or staging poison; a retry can succeed.
|
||||
retry_root="$SCRATCH/retry-tools"
|
||||
TEST_TOOLS_ROOT="$retry_root" STUB_FAIL_FETCH=1 rejects_without_pnpm run_generic anything-analyzer --start-services --skip-refresh
|
||||
[[ ! -e "$retry_root/anything-analyzer" ]]
|
||||
[[ -z "$(find "$retry_root" -maxdepth 1 -name '.reverse-bootstrap-*' -print -quit)" ]]
|
||||
: > "$CALL_LOG"
|
||||
TEST_TOOLS_ROOT="$retry_root" STUB_PNPM_VERSION=10.24.0 run_generic anything-analyzer --start-services --skip-refresh >/dev/null
|
||||
[[ -d "$retry_root/anything-analyzer/.git" ]]
|
||||
|
||||
# Kali exercises the same source-before-execution boundary where associative arrays are supported.
|
||||
if (( BASH_VERSINFO[0] >= 4 )); then
|
||||
kali_dir="$SCRATCH/home/tools/anything-analyzer"
|
||||
rm -rf "$kali_dir"
|
||||
set +e
|
||||
run_kali_bootstrap anything-analyzer --start-services --skip-refresh >"$SCRATCH/kali-bootstrap.out" 2>&1
|
||||
set -e
|
||||
check_log_line "git|init|-q|$kali_dir"
|
||||
check_log_line "git|-C|$kali_dir|remote|add|origin|$anything_repo"
|
||||
check_log_line "git|-C|$kali_dir|fetch|--depth|1|origin|$anything_pin"
|
||||
check_log_line "git|-C|$kali_dir|checkout|-q|--detach|FETCH_HEAD"
|
||||
check_log_line 'pnpm|install'
|
||||
check_log_line 'pnpm|dev'
|
||||
|
||||
: > "$CALL_LOG"
|
||||
mkdir -p "$kali_dir/.git"
|
||||
printf '%s\n' 'different-commit' > "$kali_dir/.stub-head"
|
||||
set +e
|
||||
run_kali_bootstrap anything-analyzer --start-services --skip-refresh >/dev/null 2>&1
|
||||
kali_mismatch_exit=$?
|
||||
STUB_PNPM_VERSION=0 run_kali anything-analyzer --start-services --skip-refresh >/dev/null 2>&1
|
||||
set -e
|
||||
if [[ "$kali_mismatch_exit" -eq 0 ]]; then
|
||||
printf 'Kali anything-analyzer accepted an existing checkout at a different commit\n' >&2
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
if grep -Eq '^pnpm\|(install|dev)$' "$CALL_LOG"; then
|
||||
printf 'Kali anything-analyzer ran dependencies from an unpinned existing checkout\n' >&2
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
expect_line "npm|install|-g|$pnpm_package"
|
||||
expect_line 'pnpm|install|--frozen-lockfile'
|
||||
[[ $(grep -c '|status|--porcelain|--untracked-files=all' "$CALL_LOG") -ge 2 ]]
|
||||
touch "$kali_dir/.stub-dirty"
|
||||
rejects_without_pnpm run_kali anything-analyzer --start-services --skip-refresh
|
||||
|
||||
rm -rf "$kali_dir"
|
||||
: > "$CALL_LOG"
|
||||
STUB_FAIL_FETCH=1 rejects_without_pnpm run_kali anything-analyzer --start-services --skip-refresh
|
||||
[[ ! -e "$kali_dir" ]]
|
||||
[[ -z "$(find "${kali_dir%/*}" -maxdepth 1 -name '.reverse-bootstrap-*' -print -quit)" ]]
|
||||
set +e
|
||||
STUB_PNPM_VERSION=10.24.0 run_kali anything-analyzer --start-services --skip-refresh >/dev/null 2>&1
|
||||
set -e
|
||||
[[ -d "$kali_dir/.git" ]]
|
||||
expect_line 'pnpm|install|--frozen-lockfile'
|
||||
fi
|
||||
|
||||
if [[ "$failures" -ne 0 ]]; then
|
||||
if [[ -f "$SCRATCH/kali-bootstrap.out" ]]; then cat "$SCRATCH/kali-bootstrap.out" >&2; fi
|
||||
printf '%s\n' 'captured argv:' >&2
|
||||
cat "$CALL_LOG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' 'bootstrap manifest source regression passed'
|
||||
echo 'bootstrap manifest source regression passed'
|
||||
|
||||
@@ -395,6 +395,17 @@ foreach ($mf in @($skillsManifest, $kaliManifest)) {
|
||||
if (-not (Test-Path -LiteralPath $mf)) { continue }
|
||||
$mn = Split-Path $mf -Leaf
|
||||
$mc = Get-Content -LiteralPath $mf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
foreach ($dependencyProperty in @($mc.bootstrapDependencies.PSObject.Properties)) {
|
||||
$dependency = $dependencyProperty.Value
|
||||
$expectedSuffix = '(?:==|@)' + [regex]::Escape([string]$dependency.version) + '$'
|
||||
if ([string]::IsNullOrWhiteSpace([string]$dependency.package) -or
|
||||
[string]::IsNullOrWhiteSpace([string]$dependency.version) -or
|
||||
[string]$dependency.package -notmatch $expectedSuffix) {
|
||||
Bad "unpinned bootstrap dependency: $($dependencyProperty.Name) in $mn"
|
||||
} else {
|
||||
Ok "pinned bootstrap dependency $($dependencyProperty.Name) in $mn"
|
||||
}
|
||||
}
|
||||
foreach ($cap in $mc.capabilities) {
|
||||
if (-not $cap.canAutoInstall) { continue }
|
||||
$hasPin = ($cap.pinnedVersion -or $cap.pinnedCommit -or $cap.pinPolicy)
|
||||
|
||||
Reference in New Issue
Block a user