Skip to content

Commit 8a7519d

Browse files
authored
Merge pull request #2480 from DeusData/fix/setup-one-install-path
fix(setup): install through install.sh / install.ps1, one install path
2 parents 7bb1de0 + 6c00f96 commit 8a7519d

4 files changed

Lines changed: 483 additions & 91 deletions

File tree

‎scripts/setup-windows.ps1‎

Lines changed: 90 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# codebase-memory-mcp setup script (Windows)
2-
# Default: download pre-built native Windows binary
2+
# Default: install the pre-built native Windows binary through install.ps1
33
# -FromSource: build from source inside WSL (requires Go + gcc in WSL)
44

55
param(
@@ -123,7 +123,7 @@ if ($Help) {
123123
Write-Host ""
124124
Write-Host "Usage: .\setup-windows.ps1 [-FromSource] [-Help]"
125125
Write-Host ""
126-
Write-Host " Default: Download pre-built Windows binary"
126+
Write-Host " Default: Download the pre-built Windows binary through install.ps1"
127127
Write-Host " -FromSource: Build from source inside WSL (requires Go 1.23+ and gcc in WSL)"
128128
Write-Host ""
129129
exit 0
@@ -225,7 +225,7 @@ if ($FromSource) {
225225
exit 1
226226
}
227227

228-
# Configure — WSL binary needs wsl.exe wrapper
228+
# Configure -- WSL binary needs wsl.exe wrapper
229229
$mcpConfig = [ordered]@{
230230
type = "stdio"
231231
command = "wsl.exe"
@@ -243,41 +243,105 @@ if ($FromSource) {
243243
Write-Host " wsl.exe -- rm -rf ~/.cache/codebase-memory-mcp/"
244244

245245
} else {
246-
# --- Download pre-built native Windows binary ---
247-
Write-Host "Fetching latest release..." -ForegroundColor White
248-
249-
$releaseUrl = "https://api.github.com/repos/$Repo/releases/latest"
250-
$release = Invoke-RestMethod -Uri $releaseUrl -Headers @{ "User-Agent" = "codebase-memory-mcp-setup" }
251-
$tag = $release.tag_name
246+
# --- Download + install through install.ps1 ---
247+
#
248+
# install.ps1 is the one implementation of "fetch a release and install
249+
# it": it downloads checksums.txt next to the archive, verifies the
250+
# archive's SHA-256 against it, validates the zip layout and only then
251+
# runs the binary's own `install`. This script used to carry a second
252+
# copy of that download, which did not keep up with the installer. It
253+
# now fetches install.ps1 from the same origin and branch it is itself
254+
# served from and hands over to it, so there is exactly one install path.
255+
#
256+
# CBM_DOWNLOAD_URL (the installers' download-base override, for local
257+
# testing) also moves the installer fetch: install.ps1 is then taken from
258+
# "$env:CBM_DOWNLOAD_URL/install.ps1".
259+
$installerUrl = "https://raw.githubusercontent.com/$Repo/main/install.ps1"
260+
if ($env:CBM_DOWNLOAD_URL) {
261+
$installerUrl = $env:CBM_DOWNLOAD_URL.TrimEnd('/') + "/install.ps1"
262+
}
252263

253-
if (-not $tag) {
254-
Write-Fail "Could not determine latest release."
255-
Write-Host " Check: https://github.com/$Repo/releases"
264+
# Same transport rule as install.ps1: HTTPS, or plain HTTP for a loopback
265+
# authority only (the local test fixture). No redirects are followed: both
266+
# origins serve the installer directly.
267+
try { $installerUri = [Uri]$installerUrl } catch { $installerUri = $null }
268+
$loopbackHttp = (
269+
$installerUri -and $installerUri.IsAbsoluteUri -and
270+
$installerUri.Scheme -eq "http" -and $installerUri.IsLoopback -and
271+
[string]::IsNullOrEmpty($installerUri.UserInfo)
272+
)
273+
if (-not $installerUri -or -not $installerUri.IsAbsoluteUri -or
274+
($installerUri.Scheme -ne "https" -and -not $loopbackHttp) -or
275+
-not [string]::IsNullOrEmpty($installerUri.UserInfo)) {
276+
Write-Fail "Refusing non-HTTPS installer URL: $installerUrl"
256277
exit 1
257278
}
258-
Write-Ok "Latest release: $tag"
259279

260-
$asset = "codebase-memory-mcp-windows-amd64.zip"
261-
$downloadUrl = "https://github.com/$Repo/releases/download/$tag/$asset"
262-
263-
Write-Host "Downloading $asset..." -ForegroundColor White
280+
# TLS 1.2+ for the fetch (older Windows PowerShell defaults to TLS 1.0,
281+
# which GitHub rejects). TLS 1.3 only where schannel can negotiate it; see
282+
# the note in install.ps1.
283+
$protocols = [Net.SecurityProtocolType]::Tls12
284+
if ([Environment]::OSVersion.Version.Build -ge 20348 -and
285+
([enum]::GetNames([Net.SecurityProtocolType]) -contains 'Tls13')) {
286+
$protocols = $protocols -bor [Net.SecurityProtocolType]::Tls13
287+
}
288+
[Net.ServicePointManager]::SecurityProtocol = $protocols
264289

265-
# Create install directory
266-
if (-not (Test-Path $InstallDir)) {
267-
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
290+
# A fresh staging directory of its own with an owner-only DACL, the
291+
# way install.ps1 stages its own download. The ACL is best effort: a
292+
# filesystem that cannot carry one must not fail the install.
293+
$stageDir = Join-Path ([System.IO.Path]::GetTempPath()) "cbm-setup-$(Get-Random)"
294+
New-Item -ItemType Directory -Path $stageDir -Force | Out-Null
295+
try {
296+
$stageAcl = New-Object System.Security.AccessControl.DirectorySecurity
297+
$stageAcl.SetAccessRuleProtection($true, $false)
298+
$stageOwner = ([System.Security.Principal.WindowsIdentity]::GetCurrent()).User
299+
$stageAcl.SetOwner($stageOwner)
300+
$stageAcl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule(
301+
$stageOwner, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')))
302+
Set-Acl -Path $stageDir -AclObject $stageAcl -ErrorAction Stop
303+
} catch {
304+
Write-Host " note: could not harden the staging directory ACL: $($_.Exception.Message)"
268305
}
269306

270-
$tmpZip = Join-Path $env:TEMP $asset
271-
Invoke-WebRequest -Uri $downloadUrl -OutFile $tmpZip -UseBasicParsing
307+
$installer = Join-Path $stageDir "install.ps1"
308+
$failure = $null
309+
try {
310+
Write-Host "Fetching install.ps1..." -ForegroundColor White
311+
Invoke-WebRequest -Uri $installerUrl -OutFile $installer -UseBasicParsing -MaximumRedirection 0
312+
if (-not (Test-Path -LiteralPath $installer -PathType Leaf) -or
313+
(Get-Item -LiteralPath $installer).Length -eq 0) {
314+
throw "fetched an empty install.ps1 from $installerUrl"
315+
}
316+
Write-Ok "install.ps1 fetched"
272317

273-
# Extract
274-
Expand-Archive -Path $tmpZip -DestinationPath $InstallDir -Force
275-
Remove-Item $tmpZip -Force
318+
Write-Host ""
319+
Write-Host "Installing through install.ps1..." -ForegroundColor White
320+
# The installer configures nothing (--skip-config): agent configuration
321+
# stays this script's interactive step below. It runs in a child host
322+
# of the same PowerShell so its exit code is unambiguous and its global
323+
# settings do not leak into this session. The pre-seeded exit code
324+
# keeps a child that fails to start from reading as success.
325+
$hostExe = (Get-Process -Id $PID).Path
326+
$global:LASTEXITCODE = 1
327+
& $hostExe -NoProfile -ExecutionPolicy Bypass -File $installer "--dir=$InstallDir" --skip-config
328+
if ($LASTEXITCODE -ne 0) {
329+
throw "install.ps1 exited with $LASTEXITCODE"
330+
}
331+
} catch {
332+
$failure = "$_"
333+
} finally {
334+
Remove-Item -Recurse -Force $stageDir -ErrorAction SilentlyContinue
335+
}
336+
if ($failure) {
337+
Write-Fail "Installation through install.ps1 failed: $failure"
338+
exit 1
339+
}
276340

277341
$binaryPath = Join-Path $InstallDir "$BinaryName.exe"
278342

279343
if (-not (Test-Path $binaryPath)) {
280-
Write-Fail "Binary not found at $binaryPath after extraction"
344+
Write-Fail "Binary not found at $binaryPath after installation"
281345
exit 1
282346
}
283347
Write-Ok "Installed to $binaryPath"

‎scripts/setup.sh‎

Lines changed: 61 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
set -euo pipefail
33

44
# codebase-memory-mcp setup script (macOS + Linux)
5-
# Default: download pre-built binary from GitHub Release
5+
# Default: install the latest pre-built binary through install.sh
66
# --from-source: build from source (requires Go + C compiler)
77

88
REPO="DeusData/codebase-memory-mcp"
99
INSTALL_DIR="$HOME/.local/bin"
1010
BINARY_NAME="codebase-memory-mcp"
1111
SOURCE_DIR="$HOME/.local/share/codebase-memory-mcp"
12-
CLEANUP_DIR="" # set by download_binary for EXIT trap
12+
CLEANUP_DIR="" # set by install_release for EXIT trap
1313

1414
# --- Colors ---
1515

@@ -43,44 +43,14 @@ for arg in "$@"; do
4343
--help|-h)
4444
echo "Usage: $0 [--from-source]"
4545
echo ""
46-
echo " Default: Download pre-built binary from GitHub Release"
46+
echo " Default: Download the pre-built binary through install.sh"
4747
echo " --from-source: Clone and build from source (requires Go 1.23+ and a C compiler)"
4848
exit 0
4949
;;
5050
*) die "Unknown argument: $arg" ;;
5151
esac
5252
done
5353

54-
# --- Platform detection ---
55-
56-
detect_platform() {
57-
local os arch
58-
os=$(uname -s)
59-
arch=$(uname -m)
60-
61-
case "$os" in
62-
Darwin) os="darwin" ;;
63-
Linux) os="linux" ;;
64-
*) die "Unsupported OS: $os. Use WSL2 on Windows." ;;
65-
esac
66-
67-
case "$arch" in
68-
arm64|aarch64) arch="arm64" ;;
69-
x86_64|amd64)
70-
# On macOS, uname -m returns x86_64 under Rosetta even on Apple Silicon.
71-
# Check the actual hardware to pick the right binary.
72-
if [ "$os" = "darwin" ] && sysctl -n hw.optional.arm64 2>/dev/null | grep -q '1'; then
73-
arch="arm64"
74-
else
75-
arch="amd64"
76-
fi
77-
;;
78-
*) die "Unsupported architecture: $arch" ;;
79-
esac
80-
81-
echo "${os}-${arch}"
82-
}
83-
8454
# --- Prerequisite checks ---
8555

8656
check_download_tool() {
@@ -133,46 +103,74 @@ check_git() {
133103
ok "Git found"
134104
}
135105

136-
# --- Download binary ---
106+
# --- Download + install through install.sh ---
107+
#
108+
# install.sh is the one implementation of "fetch a release and install it": it
109+
# downloads checksums.txt next to the archive, verifies the archive's SHA-256
110+
# against it, checks the archive layout, and only then runs the binary's own
111+
# `install`. This script used to carry a second copy of that download,
112+
# which did not keep up with the installer. It now fetches install.sh from the
113+
# same origin and branch it is itself served from and hands over to it, so
114+
# there is exactly one install path.
115+
#
116+
# CBM_DOWNLOAD_URL (the installers' download-base override, for local testing)
117+
# also moves the installer fetch: install.sh is then taken from
118+
# "$CBM_DOWNLOAD_URL/install.sh".
119+
120+
INSTALLER_URL="https://raw.githubusercontent.com/${REPO}/main/install.sh"
121+
if [ -n "${CBM_DOWNLOAD_URL:-}" ]; then
122+
INSTALLER_URL="${CBM_DOWNLOAD_URL%/}/install.sh"
123+
fi
124+
125+
# Same transport rule as install.sh: HTTPS everywhere; plain HTTP only for an
126+
# exact loopback authority (the local test fixture), with redirects disabled
127+
# there so a fixture cannot bounce the fetch to the network.
128+
is_loopback_http_url() {
129+
[[ "$1" =~ ^http://(localhost|127\.0\.0\.1|\[::1\])(:[0-9]+)?([/?\#].*)?$ ]]
130+
}
137131

138-
fetch() {
139-
local url="$1" tool="$2"
140-
if [ "$tool" = "curl" ]; then
141-
curl -fsSL "$url"
132+
fetch_installer() {
133+
local url="$1" destination="$2" tool="$3"
134+
if is_loopback_http_url "$url"; then
135+
if [ "$tool" = "curl" ]; then
136+
curl -fsS --noproxy '*' --proto '=http' -o "$destination" "$url"
137+
else
138+
wget -q --no-proxy --max-redirect=0 -O "$destination" "$url"
139+
fi
140+
elif [[ "$url" == https://* ]]; then
141+
if [ "$tool" = "curl" ]; then
142+
curl -fsSL --max-redirs 5 --proto '=https' --proto-redir '=https' \
143+
-o "$destination" "$url"
144+
else
145+
wget -q --https-only --max-redirect=5 -O "$destination" "$url"
146+
fi
142147
else
143-
wget -qO- "$url"
148+
die "Refusing non-HTTPS installer URL: $url"
144149
fi
145150
}
146151

147-
download_binary() {
148-
local platform="$1" tool="$2"
152+
install_release() {
153+
local tool="$1"
149154

150155
echo ""
151-
echo "${BOLD}Fetching latest release...${RESET}"
152-
local tag
153-
tag=$(fetch "https://api.github.com/repos/${REPO}/releases/latest" "$tool" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"//;s/".*//')
154-
155-
if [ -z "$tag" ]; then
156-
die "Could not determine latest release. Check https://github.com/${REPO}/releases"
157-
fi
158-
ok "Latest release: $tag"
159-
160-
local asset="codebase-memory-mcp-${platform}.tar.gz"
161-
local url="https://github.com/${REPO}/releases/download/${tag}/${asset}"
162-
163-
echo "${BOLD}Downloading ${asset}...${RESET}"
156+
echo "${BOLD}Fetching install.sh...${RESET}"
164157
CLEANUP_DIR=$(mktemp -d)
165158
trap 'rm -rf "$CLEANUP_DIR"' EXIT
166-
local tmpdir="$CLEANUP_DIR"
159+
local installer="$CLEANUP_DIR/install.sh"
167160

168-
fetch "$url" "$tool" > "${tmpdir}/${asset}"
169-
tar -xzf "${tmpdir}/${asset}" -C "$tmpdir"
161+
fetch_installer "$INSTALLER_URL" "$installer" "$tool" ||
162+
die "Could not fetch install.sh from ${INSTALLER_URL}"
163+
[ -s "$installer" ] || die "Fetched an empty install.sh from ${INSTALLER_URL}"
164+
ok "install.sh fetched"
170165

171-
mkdir -p "$INSTALL_DIR"
172-
mv "${tmpdir}/${BINARY_NAME}" "${INSTALL_DIR}/${BINARY_NAME}"
173-
chmod +x "${INSTALL_DIR}/${BINARY_NAME}"
174-
175-
ok "Installed to ${INSTALL_DIR}/${BINARY_NAME}"
166+
echo ""
167+
echo "${BOLD}Installing through install.sh...${RESET}"
168+
# The installer configures nothing (--skip-config): agent configuration
169+
# stays this script's interactive step below. stdin is detached so that,
170+
# when this script itself arrives through `curl | bash`, nothing the
171+
# installer runs can read the rest of this script as its input.
172+
bash "$installer" --dir "$INSTALL_DIR" --skip-config </dev/null ||
173+
die "install.sh failed (see the messages above)"
176174
}
177175

178176
# --- Build from source ---
@@ -297,11 +295,9 @@ echo ""
297295
if [ "$FROM_SOURCE" = true ]; then
298296
build_from_source
299297
else
300-
platform=$(detect_platform)
301-
ok "Platform: ${platform}"
302298
tool=$(check_download_tool)
303299
ok "Download tool: ${tool}"
304-
download_binary "$platform" "$tool"
300+
install_release "$tool"
305301
fi
306302

307303
# Verify binary

‎scripts/test.sh‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,9 @@ bash "$ROOT/tests/test_version_metadata_contract.sh"
338338
echo "=== Step 0y: VM leg verdict contract ==="
339339
bash "$ROOT/tests/test_vm_verdict_contract.sh"
340340

341+
echo "=== Step 0z: setup scripts install through the installers' verified path ==="
342+
bash "$ROOT/tests/test_setup_scripts_contract.sh"
343+
341344
# Verify compiler supports target arch
342345
verify_compiler "$CC"
343346

0 commit comments

Comments
 (0)