mirror of
https://github.com/webinstall/webi-installers.git
synced 2026-06-01 05:22:51 +00:00
Compare commits
15 Commits
ref-cache-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e86b7c4374 | ||
|
|
0e7c22ec2c | ||
|
|
7d6a01c200 | ||
|
|
946a340423 | ||
|
|
c3fff12f2b | ||
|
|
d62cbb1be0 | ||
|
|
49f2f26c91 | ||
|
|
ac88090aed | ||
|
|
d0fa554117 | ||
|
|
d0a9fdda05 | ||
|
|
e6ad76382d | ||
|
|
3fc6dcdb73 | ||
|
|
cd5f06c653 | ||
|
|
aafb6ffabe | ||
|
|
f112a1c90b |
13
.gitignore
vendored
13
.gitignore
vendored
@@ -18,24 +18,24 @@ install-*.ps1
|
|||||||
*.env
|
*.env
|
||||||
.env
|
.env
|
||||||
!example.env
|
!example.env
|
||||||
|
LOCAL.md
|
||||||
|
|
||||||
# caches
|
# caches
|
||||||
_cache/
|
_cache/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
# local test fixtures (regenerated by _webi/test-live-*.js)
|
|
||||||
testdata/
|
|
||||||
LIVE_cache/
|
|
||||||
distributables.csv
|
|
||||||
|
|
||||||
# temporary & backup files
|
# temporary & backup files
|
||||||
|
agents/
|
||||||
.*.sw*
|
.*.sw*
|
||||||
*.bak
|
*.bak
|
||||||
*.bak.*
|
*.bak.*
|
||||||
|
tmp
|
||||||
|
*.tmp
|
||||||
|
tmp.*
|
||||||
|
*.tmp.*
|
||||||
|
|
||||||
# agent session files
|
# agent session files
|
||||||
agents/
|
agents/
|
||||||
LOCAL.md
|
|
||||||
|
|
||||||
# other
|
# other
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -44,3 +44,4 @@ desktop.ini
|
|||||||
LIVE_cache
|
LIVE_cache
|
||||||
/webid
|
/webid
|
||||||
bin/
|
bin/
|
||||||
|
.claude/
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
node_modules/**/*
|
DELETEMEnode_modules/**/*
|
||||||
_webi/test-*.js
|
|
||||||
|
|||||||
129
AGENTS.md
129
AGENTS.md
@@ -3,26 +3,6 @@
|
|||||||
Webi installs dev tools to `~/.local/` without sudo. Each installer is a small
|
Webi installs dev tools to `~/.local/` without sudo. Each installer is a small
|
||||||
package of 3-4 files. This guide tells you how to create and modify them.
|
package of 3-4 files. This guide tells you how to create and modify them.
|
||||||
|
|
||||||
## Domains
|
|
||||||
|
|
||||||
| Environment | Domains |
|
|
||||||
| ----------- | ------------------------------------------------ |
|
|
||||||
| Production | webi.sh, webi.ms, webinstall.dev |
|
|
||||||
| Beta | beta.webi.sh, beta.webi.ms, beta.webinstall.dev |
|
|
||||||
| Next | next.webi.sh, next.webi.ms, next.webinstall.dev |
|
|
||||||
|
|
||||||
- **webi.sh** — POSIX shell installer: `curl https://webi.sh/node | sh`
|
|
||||||
- **webi.ms** — PowerShell installer: `curl.exe https://webi.ms/node | powershell`
|
|
||||||
- **webinstall.dev** — canonical domain, serves both shell scripts and the
|
|
||||||
browser-facing cheat sheet pages
|
|
||||||
|
|
||||||
The domain controls the default script format. You can force the same behavior
|
|
||||||
on any domain via User-Agent:
|
|
||||||
- `curl -A "MS" ...` — triggers PowerShell output (same as webi.ms)
|
|
||||||
- `curl ...` (default UA) — triggers POSIX shell output (same as webi.sh)
|
|
||||||
- `curl -A "$(uname -srm)" ...` — once the API is activated, the full
|
|
||||||
`uname -srm` string (e.g. `Linux 6.1.0 x86_64`) guides OS/arch selection
|
|
||||||
|
|
||||||
## Why Webi Exists
|
## Why Webi Exists
|
||||||
|
|
||||||
Webi makes tool installation trivially repeatable for people who aren't
|
Webi makes tool installation trivially repeatable for people who aren't
|
||||||
@@ -57,11 +37,9 @@ about PATH, permissions, or platform differences. Three things matter:
|
|||||||
|
|
||||||
Key infrastructure directories (do not modify without good reason):
|
Key infrastructure directories (do not modify without good reason):
|
||||||
|
|
||||||
- `_webi/` — bootstrap templates, `transform-releases.js` (API endpoint),
|
- `_webi/` — bootstrap templates, `normalize.js` (auto-detects OS/arch/ext from
|
||||||
`builds-cacher.js` (reads cache JSON), `serve-installer.js` (installer
|
filenames)
|
||||||
scripts)
|
- `_common/` — shared JS: `github.js`, `githubish.js`, `gitea.js`, `fetcher.js`
|
||||||
- `_common/` — shared JS fetcher libraries (being phased out — Go daemon now
|
|
||||||
fetches upstream)
|
|
||||||
- `_example/` — canonical template for new packages
|
- `_example/` — canonical template for new packages
|
||||||
- `_examples/` — specialized templates (goreleaser, xz-compressed)
|
- `_examples/` — specialized templates (goreleaser, xz-compressed)
|
||||||
|
|
||||||
@@ -79,50 +57,83 @@ Ref: <https://github.com/webinstall/webi-installers/issues/412>
|
|||||||
| 🔗 | Alias/redirect to another package | `ripgrep` → `rg` |
|
| 🔗 | Alias/redirect to another package | `ripgrep` → `rg` |
|
||||||
| 📝 | Bespoke / custom install | `rustlang` |
|
| 📝 | Bespoke / custom install | `rustlang` |
|
||||||
|
|
||||||
## Data Architecture
|
## releases.js
|
||||||
|
|
||||||
There are two data paths. Both read from pre-generated cache — the Node.js
|
Fetches release metadata and returns a normalized object. Most packages use
|
||||||
server does NOT fetch upstream APIs.
|
GitHub releases:
|
||||||
|
|
||||||
```
|
```js
|
||||||
API path (JSON/TAB output):
|
'use strict';
|
||||||
Request → transform-releases.js → ~/.cache/webi/legacy/{pkg}.json → filter + sort
|
|
||||||
Vocabulary: macos, amd64, arm64, armv7l (API vocabulary)
|
|
||||||
normalize.js: REMOVED — cache provides all fields directly
|
|
||||||
|
|
||||||
Installer path (bash/ps1 script output):
|
var github = require('../_common/github.js');
|
||||||
Request → serve-installer.js → builds.js → builds-cacher.js
|
var owner = 'OWNER';
|
||||||
Vocabulary: darwin, x86_64, aarch64 (build-classifier vocabulary)
|
var repo = 'REPO';
|
||||||
|
|
||||||
|
let Releases = module.exports;
|
||||||
|
|
||||||
|
Releases.latest = async function () {
|
||||||
|
let all = await github(null, owner, repo);
|
||||||
|
return all;
|
||||||
|
};
|
||||||
|
|
||||||
|
Releases.sample = async function () {
|
||||||
|
let normalize = require('../_webi/normalize.js');
|
||||||
|
let all = await Releases.latest();
|
||||||
|
all = normalize(all);
|
||||||
|
all.releases = all.releases.slice(0, 5);
|
||||||
|
return all;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (module === require.main) {
|
||||||
|
(async function () {
|
||||||
|
let samples = await Releases.sample();
|
||||||
|
console.info(JSON.stringify(samples, null, 2));
|
||||||
|
})();
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Cache is generated by the Go daemon (`webicached`) and stored flat in
|
### Common release transformations
|
||||||
`~/.cache/webi/legacy/{pkg}.json` (resolved via `Os.homedir()` in the Node
|
|
||||||
readers — no `_cache` symlink, no month subdirectory). Each file contains:
|
|
||||||
- Top-level summary arrays: `oses`, `arches`, `libcs`, `formats`
|
|
||||||
- `releases` array with pre-classified fields: `os`, `arch`, `libc`, `ext`,
|
|
||||||
`version`, `channel`, `download`
|
|
||||||
- `download` template string
|
|
||||||
|
|
||||||
### Canonical vocabulary (cache and API)
|
**Strip version prefix** (monorepo or tool-prefixed tags):
|
||||||
|
|
||||||
**OS**: `macos` (not darwin), `linux`, `windows`, `freebsd`, etc.
|
```js
|
||||||
**Arch**: `amd64` (not x86_64), `arm64` (not aarch64), `armv7l` (not armv7)
|
// e.g. "tools/monorel/v0.6.5" → "v0.6.5"
|
||||||
**Libc**: `none` (never empty), `gnu`, `musl`, `msvc`
|
rel.version = rel.version.replace(/^tools\/monorel\//, '');
|
||||||
**Ext**: `tar.gz`, `zip`, `exe` (no leading dot; `exe` for bare binaries)
|
|
||||||
|
|
||||||
## releases.js (legacy — being phased out)
|
// e.g. "cli-v1.2.3" → "v1.2.3"
|
||||||
|
rel.version = rel.version.replace(/^cli-/, '');
|
||||||
|
```
|
||||||
|
|
||||||
The `{pkg}/releases.js` files previously fetched upstream release metadata.
|
**Filter releases** (monorepo with multiple tools, or unwanted assets):
|
||||||
These are being replaced by the Go cache daemon. Existing files are kept as
|
|
||||||
documentation of release sources but are no longer called by the server.
|
|
||||||
|
|
||||||
### Testing the API (current)
|
```js
|
||||||
|
all.releases = all.releases.filter(function (rel) {
|
||||||
|
// Keep only releases for this tool
|
||||||
|
return rel.version.startsWith('tools/monorel/');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Apply transformations inside `Releases.latest`, before returning `all`.
|
||||||
|
|
||||||
|
**Available sources** beyond `github.js`:
|
||||||
|
|
||||||
|
- `_common/gitea.js` — Gitea servers
|
||||||
|
- `_common/git-tag.js` — Git tag listing
|
||||||
|
- Custom fetch from any JSON API (see `go/releases.js`, `terraform/releases.js`)
|
||||||
|
|
||||||
|
### Testing releases.js
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl -sS 'https://beta.webi.sh/api/releases/<name>.json?os=macos&arch=arm64&limit=5' | jq .
|
node -e "
|
||||||
|
let Releases = require('./<name>/releases.js');
|
||||||
|
Releases.sample().then(function (all) {
|
||||||
|
console.log(JSON.stringify(all, null, 2));
|
||||||
|
});
|
||||||
|
"
|
||||||
```
|
```
|
||||||
|
|
||||||
Verify: versions present, correct OS/arch vocabulary, download URLs resolve.
|
Verify: versions are clean semver (`0.6.5` not `tools/monorel/v0.6.5`), OS/arch
|
||||||
|
detected correctly, download URLs resolve.
|
||||||
|
|
||||||
## install.sh
|
## install.sh
|
||||||
|
|
||||||
@@ -354,11 +365,9 @@ Commit messages: `feat(<pkg>): add installer`, `fix(<pkg>): update install.sh`,
|
|||||||
must filter in `releases.js` and strip the tag prefix from the version.
|
must filter in `releases.js` and strip the tag prefix from the version.
|
||||||
- **No `--version` flag**: Some tools lack version introspection. Comment out
|
- **No `--version` flag**: Some tools lack version introspection. Comment out
|
||||||
`pkg_get_current_version` — webi still works, it just can't skip reinstalls.
|
`pkg_get_current_version` — webi still works, it just can't skip reinstalls.
|
||||||
- **Cache directory**: `builds-cacher.js` and `transform-releases.js` read
|
- **normalize.js auto-detection**: OS/arch/ext are guessed from download
|
||||||
exclusively from `~/.cache/webi/legacy/{pkg}.json` (resolved via
|
filenames. If the tool uses non-standard naming, you may need to set `os`,
|
||||||
`Os.homedir()`). No date bucketing, no `_cache` symlink, no month rollover
|
`arch`, or `ext` explicitly in `releases.js`.
|
||||||
hazard. If a package returns `0.0.0`, check that `~/.cache/webi/legacy/{pkg}.json`
|
|
||||||
exists and that `webicached` is running.
|
|
||||||
- **Goreleaser archives**: Typically contain a bare binary at the archive root
|
- **Goreleaser archives**: Typically contain a bare binary at the archive root
|
||||||
(not nested in a directory). Use `mv ./cmd "$pkg_src_cmd"`.
|
(not nested in a directory). Use `mv ./cmd "$pkg_src_cmd"`.
|
||||||
|
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
set -u
|
|
||||||
|
|
||||||
# Deploy the Node.js webinstall (installer server) to a target host.
|
|
||||||
# Usage: _scripts/deploy-webinstall <host>
|
|
||||||
# e.g. _scripts/deploy-webinstall beta.webi.sh
|
|
||||||
# _scripts/deploy-webinstall next.webi.sh
|
|
||||||
|
|
||||||
fn_main() {
|
|
||||||
g_host="${1:-}"
|
|
||||||
if test -z "${g_host}"; then
|
|
||||||
printf 'Usage: %s <host>\n' "$0" >&2
|
|
||||||
printf ' e.g. %s beta.webi.sh\n' "$0" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# beta.webi.sh → ~/srv/beta.webinstall.dev/installers/
|
|
||||||
# next.webi.sh → ~/srv/next.webinstall.dev/installers/
|
|
||||||
b_subdomain="${g_host%%.*}"
|
|
||||||
g_dest="~/srv/${b_subdomain}.webinstall.dev/installers/"
|
|
||||||
|
|
||||||
# Verify the build-classifier submodule is populated. rsync will
|
|
||||||
# happily push an empty submodule directory, after which Node
|
|
||||||
# crashes at startup with `Cannot find module './build-classifier/
|
|
||||||
# host-targets.js'`. New worktrees from `git worktree add` don't
|
|
||||||
# init submodules by default.
|
|
||||||
if ! test -f ./_webi/build-classifier/host-targets.js; then
|
|
||||||
printf 'fatal: _webi/build-classifier submodule not initialized\n' >&2
|
|
||||||
printf ' run: git submodule update --init _webi/build-classifier\n' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s\n' "Deploying to ${g_host}:${g_dest} ..."
|
|
||||||
|
|
||||||
rsync -avz --delete \
|
|
||||||
--exclude='.git' \
|
|
||||||
--exclude='node_modules' \
|
|
||||||
--exclude='_cache' \
|
|
||||||
--exclude='LIVE_cache' \
|
|
||||||
--exclude='webicached' \
|
|
||||||
--exclude='classify' \
|
|
||||||
--exclude='fetchraw' \
|
|
||||||
--exclude='inspect' \
|
|
||||||
--exclude='e2etest' \
|
|
||||||
--exclude='zigtest' \
|
|
||||||
--exclude='distributables.csv' \
|
|
||||||
--exclude='agents/' \
|
|
||||||
./ "${g_host}:${g_dest}"
|
|
||||||
|
|
||||||
printf '%s\n' "Restarting webinstall ..."
|
|
||||||
|
|
||||||
# shellcheck disable=SC2029
|
|
||||||
ssh "${g_host}" "
|
|
||||||
. ~/.config/envman/PATH.env 2>/dev/null
|
|
||||||
rm -rf ${g_dest}_cache 2>/dev/null
|
|
||||||
serviceman restart webinstall
|
|
||||||
"
|
|
||||||
|
|
||||||
printf '%s\n' "Waiting for restart ..."
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
fn_smoke_test
|
|
||||||
}
|
|
||||||
|
|
||||||
fn_smoke_test() {
|
|
||||||
printf '%s\n' "Smoke testing https://${g_host}/ ..."
|
|
||||||
|
|
||||||
b_headers="$(curl -sI --max-time 5 "https://${g_host}/api/releases/go.json?limit=1")"
|
|
||||||
|
|
||||||
if printf '%s' "${b_headers}" | grep -qi 'X-Robots-Tag: noindex'; then
|
|
||||||
printf '%s\n' " [ok] X-Robots-Tag: noindex"
|
|
||||||
else
|
|
||||||
printf '%s\n' " [FAIL] Missing X-Robots-Tag header"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if printf '%s' "${b_headers}" | grep -qi 'rel="canonical"'; then
|
|
||||||
printf '%s\n' " [ok] Link: canonical"
|
|
||||||
else
|
|
||||||
printf '%s\n' " [FAIL] Missing canonical Link header"
|
|
||||||
fi
|
|
||||||
|
|
||||||
b_version="$(curl -sS --max-time 5 "https://${g_host}/api/releases/go.json?limit=1" | jq -r '.[0].version')"
|
|
||||||
if test "${b_version}" != "0.0.0" && test -n "${b_version}"; then
|
|
||||||
printf '%s\n' " [ok] API returns go ${b_version}"
|
|
||||||
else
|
|
||||||
printf '%s\n' " [FAIL] API returned error or empty for go"
|
|
||||||
fi
|
|
||||||
|
|
||||||
b_shebang="$(curl -sS --max-time 5 "https://${g_host}/node" | head -1)"
|
|
||||||
if test "${b_shebang}" = "#!/bin/sh"; then
|
|
||||||
printf '%s\n' " [ok] Installer path serves shell script"
|
|
||||||
else
|
|
||||||
printf '%s\n' " [FAIL] Installer path unexpected: ${b_shebang}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s\n' "Done."
|
|
||||||
}
|
|
||||||
|
|
||||||
fn_main "$@"
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
let Fs = require('node:fs/promises');
|
|
||||||
let Path = require('node:path');
|
|
||||||
|
|
||||||
let Releases = require('./transform-releases.js');
|
|
||||||
|
|
||||||
let TESTDATA_DIR = Path.join(__dirname, 'testdata');
|
|
||||||
|
|
||||||
// These mirror what the live API returns for /api/releases/{pkg}@stable.json?...
|
|
||||||
let FILTERED_CASES = [
|
|
||||||
{ pkg: 'bat', os: 'macos', arch: 'amd64' },
|
|
||||||
{ pkg: 'bat', os: 'macos', arch: 'arm64' },
|
|
||||||
{ pkg: 'bat', os: 'linux', arch: 'amd64' },
|
|
||||||
{ pkg: 'bat', os: 'windows', arch: 'amd64' },
|
|
||||||
{ pkg: 'go', os: 'macos', arch: 'amd64' },
|
|
||||||
{ pkg: 'go', os: 'macos', arch: 'arm64' },
|
|
||||||
{ pkg: 'go', os: 'linux', arch: 'amd64' },
|
|
||||||
{ pkg: 'go', os: 'windows', arch: 'amd64' },
|
|
||||||
{ pkg: 'rg', os: 'macos', arch: 'amd64' },
|
|
||||||
{ pkg: 'rg', os: 'macos', arch: 'arm64' },
|
|
||||||
{ pkg: 'rg', os: 'linux', arch: 'amd64' },
|
|
||||||
{ pkg: 'rg', os: 'windows', arch: 'amd64' },
|
|
||||||
{ pkg: 'caddy', os: 'macos', arch: 'amd64' },
|
|
||||||
{ pkg: 'caddy', os: 'macos', arch: 'arm64' },
|
|
||||||
{ pkg: 'caddy', os: 'linux', arch: 'amd64' },
|
|
||||||
{ pkg: 'caddy', os: 'windows', arch: 'amd64' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Fields to compare between live and local
|
|
||||||
let COMPARE_FIELDS = ['version', 'os', 'arch', 'ext', 'libc', 'channel', 'download'];
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
let failures = 0;
|
|
||||||
let passes = 0;
|
|
||||||
let skips = 0;
|
|
||||||
|
|
||||||
// Test 1: Unfiltered release list — compare structure and field values
|
|
||||||
console.log('=== Test 1: Unfiltered /api/releases/{pkg}.json ===');
|
|
||||||
console.log('');
|
|
||||||
for (let pkg of ['bat', 'go', 'node', 'rg', 'jq', 'caddy']) {
|
|
||||||
let liveFile = `${TESTDATA_DIR}/live_${pkg}.json`;
|
|
||||||
let liveExists = await Fs.access(liveFile).then(
|
|
||||||
function () { return true; },
|
|
||||||
function () { return false; },
|
|
||||||
);
|
|
||||||
if (!liveExists) {
|
|
||||||
console.log(` SKIP ${pkg}: no golden data`);
|
|
||||||
skips++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let liveJson = await Fs.readFile(liveFile, 'utf8');
|
|
||||||
let liveReleases = JSON.parse(liveJson);
|
|
||||||
|
|
||||||
let localResult = await Releases.getReleases({
|
|
||||||
pkg: pkg,
|
|
||||||
ver: '',
|
|
||||||
os: '',
|
|
||||||
arch: '',
|
|
||||||
libc: '',
|
|
||||||
lts: false,
|
|
||||||
channel: '',
|
|
||||||
formats: [],
|
|
||||||
limit: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
let localReleases = localResult.releases;
|
|
||||||
|
|
||||||
// Compare OS vocabulary
|
|
||||||
let liveOses = [...new Set(liveReleases.map(function (r) { return r.os; }))].sort();
|
|
||||||
let localOses = [...new Set(localReleases.map(function (r) { return r.os; }))].sort();
|
|
||||||
let osMatch = JSON.stringify(liveOses) === JSON.stringify(localOses);
|
|
||||||
if (!osMatch) {
|
|
||||||
console.log(` FAIL ${pkg} OS values: live=${JSON.stringify(liveOses)} local=${JSON.stringify(localOses)}`);
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
console.log(` PASS ${pkg} OS values: ${JSON.stringify(liveOses)}`);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare arch vocabulary
|
|
||||||
let liveArches = [...new Set(liveReleases.map(function (r) { return r.arch; }))].sort();
|
|
||||||
let localArches = [...new Set(localReleases.map(function (r) { return r.arch; }))].sort();
|
|
||||||
let archMatch = JSON.stringify(liveArches) === JSON.stringify(localArches);
|
|
||||||
if (!archMatch) {
|
|
||||||
console.log(` FAIL ${pkg} arch values: live=${JSON.stringify(liveArches)} local=${JSON.stringify(localArches)}`);
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
console.log(` PASS ${pkg} arch values: ${JSON.stringify(liveArches)}`);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare latest version
|
|
||||||
let liveLatest = liveReleases[0]?.version;
|
|
||||||
let localLatest = localReleases[0]?.version;
|
|
||||||
if (liveLatest !== localLatest) {
|
|
||||||
// Version differences may be expected if cache is newer/older
|
|
||||||
console.log(` WARN ${pkg} latest version: live=${liveLatest} local=${localLatest}`);
|
|
||||||
} else {
|
|
||||||
console.log(` PASS ${pkg} latest version: ${liveLatest}`);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare ext vocabulary
|
|
||||||
let liveExts = [...new Set(liveReleases.map(function (r) { return r.ext; }))].sort();
|
|
||||||
let localExts = [...new Set(localReleases.map(function (r) { return r.ext; }))].sort();
|
|
||||||
let extMatch = JSON.stringify(liveExts) === JSON.stringify(localExts);
|
|
||||||
if (!extMatch) {
|
|
||||||
console.log(` FAIL ${pkg} ext values: live=${JSON.stringify(liveExts)} local=${JSON.stringify(localExts)}`);
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
console.log(` PASS ${pkg} ext values: ${JSON.stringify(liveExts)}`);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare that version strings don't have 'v' prefix
|
|
||||||
let localHasVPrefix = localReleases.some(function (r) {
|
|
||||||
return r.version.startsWith('v');
|
|
||||||
});
|
|
||||||
if (localHasVPrefix) {
|
|
||||||
console.log(` FAIL ${pkg} versions have 'v' prefix (should be stripped)`);
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
console.log(` PASS ${pkg} no 'v' prefix on versions`);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 2: Filtered queries — compare selected package for specific OS/arch
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 2: Filtered /api/releases/{pkg}@stable.json?os=...&arch=... ===');
|
|
||||||
console.log('');
|
|
||||||
for (let tc of FILTERED_CASES) {
|
|
||||||
let fname = `live_${tc.pkg}_os_${tc.os}_arch_${tc.arch}.json`;
|
|
||||||
let liveFile = `${TESTDATA_DIR}/${fname}`;
|
|
||||||
let liveExists = await Fs.access(liveFile).then(
|
|
||||||
function () { return true; },
|
|
||||||
function () { return false; },
|
|
||||||
);
|
|
||||||
if (!liveExists) {
|
|
||||||
skips++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let liveJson = await Fs.readFile(liveFile, 'utf8');
|
|
||||||
let liveReleases = JSON.parse(liveJson);
|
|
||||||
let liveFirst = liveReleases[0];
|
|
||||||
if (!liveFirst || liveFirst.channel === 'error') {
|
|
||||||
console.log(` SKIP ${tc.pkg} ${tc.os}/${tc.arch}: live returned error/empty`);
|
|
||||||
skips++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let localResult = await Releases.getReleases({
|
|
||||||
pkg: tc.pkg,
|
|
||||||
ver: '',
|
|
||||||
os: tc.os,
|
|
||||||
arch: tc.arch,
|
|
||||||
libc: '',
|
|
||||||
lts: false,
|
|
||||||
channel: 'stable',
|
|
||||||
formats: ['tar', 'zip', 'exe', 'xz'],
|
|
||||||
limit: 1,
|
|
||||||
});
|
|
||||||
let localFirst = localResult.releases[0];
|
|
||||||
|
|
||||||
if (!localFirst || localFirst.channel === 'error') {
|
|
||||||
console.log(` FAIL ${tc.pkg} ${tc.os}/${tc.arch}: local returned error/empty, live had ${liveFirst.version}`);
|
|
||||||
failures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let diffs = [];
|
|
||||||
for (let field of COMPARE_FIELDS) {
|
|
||||||
let liveVal = String(liveFirst[field] || '');
|
|
||||||
let localVal = String(localFirst[field] || '');
|
|
||||||
if (liveVal !== localVal) {
|
|
||||||
// Version differences are OK if cache age differs
|
|
||||||
if (field === 'version' || field === 'download' || field === 'date') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
diffs.push(`${field}: live=${liveVal} local=${localVal}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (diffs.length > 0) {
|
|
||||||
console.log(` FAIL ${tc.pkg} ${tc.os}/${tc.arch}: ${diffs.join(', ')}`);
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
let ver = localFirst.version;
|
|
||||||
let ext = localFirst.ext;
|
|
||||||
console.log(` PASS ${tc.pkg} ${tc.os}/${tc.arch}: v${ver} .${ext}`);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
console.log(`=== Results: ${passes} passed, ${failures} failed, ${skips} skipped ===`);
|
|
||||||
if (failures > 0) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(function (err) {
|
|
||||||
console.error(err.stack);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
// Broad sweep: test that all cached packages resolve on macOS arm64
|
|
||||||
// and Linux amd64. Catches any package that completely fails to resolve.
|
|
||||||
//
|
|
||||||
// Usage: node _webi/test-broad-resolve.js
|
|
||||||
|
|
||||||
var Path = require('node:path');
|
|
||||||
var InstallerServer = require('./serve-installer.js');
|
|
||||||
var Builds = require('./builds.js');
|
|
||||||
var BuildsCacher = require('./builds-cacher.js');
|
|
||||||
|
|
||||||
var UA_CASES = [
|
|
||||||
{ label: 'macOS arm64', ua: 'aarch64/unknown Darwin/24.2.0 libc' },
|
|
||||||
{ label: 'Linux amd64', ua: 'x86_64/unknown Linux/5.15.0 libc' },
|
|
||||||
];
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log('Initializing build cache...');
|
|
||||||
await Builds.init();
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
var bc = BuildsCacher.create({
|
|
||||||
caches: Path.join(__dirname, '../_cache'),
|
|
||||||
installers: Path.join(__dirname, '..'),
|
|
||||||
});
|
|
||||||
var dirs = await bc.getProjectsByType();
|
|
||||||
var pkgs = Object.keys(dirs.valid).sort();
|
|
||||||
console.log('Testing ' + pkgs.length + ' packages...');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
var pass = 0;
|
|
||||||
var fail = 0;
|
|
||||||
var failures = [];
|
|
||||||
|
|
||||||
for (var i = 0; i < pkgs.length; i++) {
|
|
||||||
var pkg = pkgs[i];
|
|
||||||
for (var j = 0; j < UA_CASES.length; j++) {
|
|
||||||
var tc = UA_CASES[j];
|
|
||||||
try {
|
|
||||||
var r = await InstallerServer.helper({
|
|
||||||
unameAgent: tc.ua,
|
|
||||||
projectName: pkg,
|
|
||||||
tag: 'stable',
|
|
||||||
formats: ['tar', 'exe', 'zip', 'xz', 'dmg'],
|
|
||||||
libc: '',
|
|
||||||
});
|
|
||||||
var p = r[0];
|
|
||||||
if (p.channel === 'error' || p.ext === 'err') {
|
|
||||||
failures.push(pkg + ' ' + tc.label + ': error (v' + p.version + ')');
|
|
||||||
fail++;
|
|
||||||
} else {
|
|
||||||
pass++;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
failures.push(pkg + ' ' + tc.label + ': ' + e.message.substring(0, 60));
|
|
||||||
fail++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failures.length > 0) {
|
|
||||||
console.log('Failures:');
|
|
||||||
for (var k = 0; k < failures.length; k++) {
|
|
||||||
console.log(' FAIL ' + failures[k]);
|
|
||||||
}
|
|
||||||
console.log('');
|
|
||||||
}
|
|
||||||
|
|
||||||
var total = pkgs.length * UA_CASES.length;
|
|
||||||
console.log('=== ' + pass + '/' + total + ' passed (' + fail + ' failed) ===');
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(function (err) {
|
|
||||||
console.error(err.stack);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
// Tests that _cache JSON files are ready for direct use by the API endpoint
|
|
||||||
// (transform-releases.js) WITHOUT needing normalize.js to fix up fields.
|
|
||||||
//
|
|
||||||
// These tests drive GOER to add missing features to ExportLegacy so that
|
|
||||||
// normalize.js can be eliminated from the API path entirely.
|
|
||||||
//
|
|
||||||
// Usage: node _webi/test-cache-api-ready.js
|
|
||||||
|
|
||||||
var Fs = require('node:fs');
|
|
||||||
var Os = require('node:os');
|
|
||||||
var Path = require('node:path');
|
|
||||||
|
|
||||||
var CACHE_DIR = Path.join(Os.homedir(), '.cache/webi/legacy');
|
|
||||||
|
|
||||||
// Packages to spot-check (mix of github_releases, custom releases.js, gittag)
|
|
||||||
var CHECK_PKGS = [
|
|
||||||
'bat',
|
|
||||||
'caddy',
|
|
||||||
'go',
|
|
||||||
'hugo',
|
|
||||||
'jq',
|
|
||||||
'node',
|
|
||||||
'rg',
|
|
||||||
'terraform',
|
|
||||||
'zig',
|
|
||||||
];
|
|
||||||
|
|
||||||
function loadReleases(dir, pkg) {
|
|
||||||
var file = Path.join(dir, pkg + '.json');
|
|
||||||
if (!Fs.existsSync(file)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.parse(Fs.readFileSync(file, 'utf8'));
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
var passes = 0;
|
|
||||||
var failures = 0;
|
|
||||||
|
|
||||||
if (!Fs.existsSync(CACHE_DIR)) {
|
|
||||||
console.error('No cache directory at ' + CACHE_DIR);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
console.log('Testing ' + CACHE_DIR);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 1: Version has no 'v' prefix
|
|
||||||
// ================================================================
|
|
||||||
console.log('=== Test 1: Version v-prefix stripped ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
for (var vi = 0; vi < CHECK_PKGS.length; vi++) {
|
|
||||||
var vpkg = CHECK_PKGS[vi];
|
|
||||||
var vdata = loadReleases(CACHE_DIR, vpkg);
|
|
||||||
if (!vdata) {
|
|
||||||
console.log(' SKIP ' + vpkg + ': no data');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var vPrefixed = 0;
|
|
||||||
for (var vri = 0; vri < vdata.releases.length; vri++) {
|
|
||||||
var ver = vdata.releases[vri].version || '';
|
|
||||||
if (ver.startsWith('v')) {
|
|
||||||
vPrefixed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vPrefixed === 0) {
|
|
||||||
console.log(' PASS ' + vpkg + ': no v-prefixed versions');
|
|
||||||
passes++;
|
|
||||||
} else {
|
|
||||||
console.log(' FAIL ' + vpkg + ': ' + vPrefixed + '/' + vdata.releases.length + ' versions have v prefix');
|
|
||||||
failures++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 2: libc is never empty string (should be "none", "gnu", "musl", "msvc")
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 2: libc never empty ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
for (var li = 0; li < CHECK_PKGS.length; li++) {
|
|
||||||
var lpkg = CHECK_PKGS[li];
|
|
||||||
var ldata = loadReleases(CACHE_DIR, lpkg);
|
|
||||||
if (!ldata) {
|
|
||||||
console.log(' SKIP ' + lpkg + ': no data');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var emptyLibc = 0;
|
|
||||||
for (var lri = 0; lri < ldata.releases.length; lri++) {
|
|
||||||
if (ldata.releases[lri].libc === '') {
|
|
||||||
emptyLibc++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emptyLibc === 0) {
|
|
||||||
console.log(' PASS ' + lpkg + ': all entries have libc set');
|
|
||||||
passes++;
|
|
||||||
} else {
|
|
||||||
console.log(' FAIL ' + lpkg + ': ' + emptyLibc + '/' + ldata.releases.length + ' entries have empty libc (should be "none")');
|
|
||||||
failures++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 3: ext has no leading dot
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 3: ext has no leading dot ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
for (var ei = 0; ei < CHECK_PKGS.length; ei++) {
|
|
||||||
var epkg = CHECK_PKGS[ei];
|
|
||||||
var edata = loadReleases(CACHE_DIR, epkg);
|
|
||||||
if (!edata) {
|
|
||||||
console.log(' SKIP ' + epkg + ': no data');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dotExt = 0;
|
|
||||||
for (var eri = 0; eri < edata.releases.length; eri++) {
|
|
||||||
var ext = edata.releases[eri].ext || '';
|
|
||||||
if (ext.startsWith('.')) {
|
|
||||||
dotExt++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dotExt === 0) {
|
|
||||||
console.log(' PASS ' + epkg + ': no leading dots on ext');
|
|
||||||
passes++;
|
|
||||||
} else {
|
|
||||||
console.log(' FAIL ' + epkg + ': ' + dotExt + '/' + edata.releases.length + ' entries have leading dot on ext');
|
|
||||||
failures++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 4: bare binaries have ext "exe" (not empty)
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 4: bare binaries have ext "exe" ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
var barePkgs = ['jq', 'bat', 'rg', 'caddy'];
|
|
||||||
for (var bi = 0; bi < barePkgs.length; bi++) {
|
|
||||||
var bpkg = barePkgs[bi];
|
|
||||||
var bdata = loadReleases(CACHE_DIR, bpkg);
|
|
||||||
if (!bdata) {
|
|
||||||
console.log(' SKIP ' + bpkg + ': no data');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var emptyExt = 0;
|
|
||||||
for (var bri = 0; bri < bdata.releases.length; bri++) {
|
|
||||||
var brel = bdata.releases[bri];
|
|
||||||
if (brel.ext === '' && brel.name && !brel.name.includes('.')) {
|
|
||||||
emptyExt++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emptyExt === 0) {
|
|
||||||
console.log(' PASS ' + bpkg + ': no bare binaries with empty ext');
|
|
||||||
passes++;
|
|
||||||
} else {
|
|
||||||
console.log(' FAIL ' + bpkg + ': ' + emptyExt + ' bare binaries have empty ext (should be "exe")');
|
|
||||||
failures++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 5: Top-level summary arrays present
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 5: Summary arrays (oses, arches, libcs, formats) ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
for (var si = 0; si < CHECK_PKGS.length; si++) {
|
|
||||||
var spkg = CHECK_PKGS[si];
|
|
||||||
var sdata = loadReleases(CACHE_DIR, spkg);
|
|
||||||
if (!sdata) {
|
|
||||||
console.log(' SKIP ' + spkg + ': no data');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var missing = [];
|
|
||||||
if (!Array.isArray(sdata.oses)) { missing.push('oses'); }
|
|
||||||
if (!Array.isArray(sdata.arches)) { missing.push('arches'); }
|
|
||||||
if (!Array.isArray(sdata.libcs)) { missing.push('libcs'); }
|
|
||||||
if (!Array.isArray(sdata.formats)) { missing.push('formats'); }
|
|
||||||
|
|
||||||
if (missing.length === 0) {
|
|
||||||
// Verify they contain the right values
|
|
||||||
var hasMacos = sdata.oses.includes('macos') || sdata.oses.includes('darwin');
|
|
||||||
var hasLinux = sdata.oses.includes('linux');
|
|
||||||
var ok = true;
|
|
||||||
if (sdata.releases.some(function (r) { return r.os === 'macos' || r.os === 'darwin'; }) && !hasMacos) {
|
|
||||||
console.log(' FAIL ' + spkg + ': oses array missing macos/darwin');
|
|
||||||
failures++;
|
|
||||||
ok = false;
|
|
||||||
}
|
|
||||||
if (sdata.releases.some(function (r) { return r.os === 'linux'; }) && !hasLinux) {
|
|
||||||
console.log(' FAIL ' + spkg + ': oses array missing linux');
|
|
||||||
failures++;
|
|
||||||
ok = false;
|
|
||||||
}
|
|
||||||
if (ok) {
|
|
||||||
console.log(' PASS ' + spkg + ': has oses, arches, libcs, formats');
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(' FAIL ' + spkg + ': missing top-level arrays: ' + missing.join(', '));
|
|
||||||
failures++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 6: Version sort order — stable before beta, newest first
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 6: Version sort order ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
var sortPkgs = ['go', 'node', 'terraform'];
|
|
||||||
for (var oi = 0; oi < sortPkgs.length; oi++) {
|
|
||||||
var opkg = sortPkgs[oi];
|
|
||||||
var odata = loadReleases(CACHE_DIR, opkg);
|
|
||||||
if (!odata) {
|
|
||||||
console.log(' SKIP ' + opkg + ': no data');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find first stable release
|
|
||||||
var firstStable = odata.releases.find(function (r) { return r.channel === 'stable'; });
|
|
||||||
// Find first beta release
|
|
||||||
var firstBeta = odata.releases.find(function (r) { return r.channel === 'beta'; });
|
|
||||||
|
|
||||||
if (!firstStable) {
|
|
||||||
console.log(' SKIP ' + opkg + ': no stable release');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The first entry overall should be a stable release (newest stable > any beta)
|
|
||||||
// unless the beta is a newer version number
|
|
||||||
var firstEntry = odata.releases[0];
|
|
||||||
if (firstEntry.channel === 'stable') {
|
|
||||||
console.log(' PASS ' + opkg + ': first entry is stable (' + firstEntry.version + ')');
|
|
||||||
passes++;
|
|
||||||
} else {
|
|
||||||
console.log(' FAIL ' + opkg + ': first entry is ' + firstEntry.channel + ' (' + firstEntry.version + '), expected stable (' + firstStable.version + ')');
|
|
||||||
failures++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Summary
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Results: ' + passes + ' passed, ' + failures + ' failed ===');
|
|
||||||
if (failures > 0) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(function (err) {
|
|
||||||
console.error(err.stack);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,687 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
// Cache compatibility tests: verify that Go-generated cache files work
|
|
||||||
// correctly with the Node build-classifier and installer resolution pipeline.
|
|
||||||
//
|
|
||||||
// Tests cover:
|
|
||||||
// 1. Cache completeness — all packages have releases, required fields present
|
|
||||||
// 2. Format selection — correct archive format per platform
|
|
||||||
// 3. Edge-case platforms — FreeBSD, ARM variants, musl/Alpine
|
|
||||||
// 4. Script generation — installer scripts render without error
|
|
||||||
// 5. API compat — transform-releases output matches expected vocabulary
|
|
||||||
// 6. Version format — no 'v' prefix, valid semver-ish
|
|
||||||
// 7. Channel detection — stable vs beta correctly identified
|
|
||||||
//
|
|
||||||
// Usage: node _webi/test-cache-compat.js
|
|
||||||
|
|
||||||
var Fs = require('node:fs');
|
|
||||||
var Path = require('node:path');
|
|
||||||
|
|
||||||
var InstallerServer = require('./serve-installer.js');
|
|
||||||
var Builds = require('./builds.js');
|
|
||||||
var Releases = require('./transform-releases.js');
|
|
||||||
|
|
||||||
var CACHE_DIR = Path.join(require('node:os').homedir(), '.cache/webi/legacy');
|
|
||||||
|
|
||||||
// ====================================================================
|
|
||||||
// Test 1: Cache completeness — every package has releases with valid fields
|
|
||||||
// ====================================================================
|
|
||||||
|
|
||||||
// Packages that are expected to have binary releases (not gittag-only)
|
|
||||||
var BINARY_PKGS = [
|
|
||||||
'bat',
|
|
||||||
'caddy',
|
|
||||||
'cmake',
|
|
||||||
'delta',
|
|
||||||
'deno',
|
|
||||||
'fd',
|
|
||||||
'fzf',
|
|
||||||
'gh',
|
|
||||||
'go',
|
|
||||||
'goreleaser',
|
|
||||||
'hugo',
|
|
||||||
'jq',
|
|
||||||
'k9s',
|
|
||||||
'node',
|
|
||||||
'ollama',
|
|
||||||
'rg',
|
|
||||||
'shellcheck',
|
|
||||||
'shfmt',
|
|
||||||
'syncthing',
|
|
||||||
'terraform',
|
|
||||||
'xz',
|
|
||||||
'yq',
|
|
||||||
'zig',
|
|
||||||
'zoxide',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Packages that are gittag/source-only — must have releases but os/arch may be special
|
|
||||||
var GITTAG_PKGS = [
|
|
||||||
'aliasman',
|
|
||||||
'serviceman',
|
|
||||||
'vim-airline',
|
|
||||||
'vim-go',
|
|
||||||
'vim-sensible',
|
|
||||||
];
|
|
||||||
|
|
||||||
// ====================================================================
|
|
||||||
// Test 2: Format selection — correct ext per platform
|
|
||||||
// ====================================================================
|
|
||||||
|
|
||||||
// For these packages, verify the resolved format is correct for each platform.
|
|
||||||
// Linux/macOS should get .tar.gz or .tar.xz (not .zip except for specific packages).
|
|
||||||
// Windows should get .zip or .exe (not .tar.gz).
|
|
||||||
var FORMAT_CASES = [
|
|
||||||
// Go projects — should be .tar.gz on Linux/macOS, .zip on Windows
|
|
||||||
{
|
|
||||||
label: 'go Linux amd64 format',
|
|
||||||
pkg: 'go',
|
|
||||||
ua: 'x86_64/unknown Linux/5.15.0 libc',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'go Windows amd64 format',
|
|
||||||
pkg: 'go',
|
|
||||||
ua: 'x86_64/unknown Windows/10.0.19041 msvc',
|
|
||||||
expectExt: 'zip',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'go macOS arm64 format',
|
|
||||||
pkg: 'go',
|
|
||||||
ua: 'aarch64/unknown Darwin/24.2.0 libc',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
},
|
|
||||||
// Rust projects — should be .tar.gz on Linux/macOS, .zip on Windows
|
|
||||||
{
|
|
||||||
label: 'bat Linux amd64 format',
|
|
||||||
pkg: 'bat',
|
|
||||||
ua: 'x86_64/unknown Linux/5.15.0 libc',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'bat Windows amd64 format',
|
|
||||||
pkg: 'bat',
|
|
||||||
ua: 'x86_64/unknown Windows/10.0.19041 msvc',
|
|
||||||
expectExt: 'zip',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'rg Linux amd64 format',
|
|
||||||
pkg: 'rg',
|
|
||||||
ua: 'x86_64/unknown Linux/5.15.0 libc',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'rg Windows amd64 format',
|
|
||||||
pkg: 'rg',
|
|
||||||
ua: 'x86_64/unknown Windows/10.0.19041 msvc',
|
|
||||||
expectExt: 'zip',
|
|
||||||
},
|
|
||||||
// Node — uses .tar.xz on Linux/macOS
|
|
||||||
{
|
|
||||||
label: 'node Linux amd64 format',
|
|
||||||
pkg: 'node',
|
|
||||||
ua: 'x86_64/unknown Linux/5.15.0 libc',
|
|
||||||
expectExt: 'tar.xz',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'node macOS arm64 format',
|
|
||||||
pkg: 'node',
|
|
||||||
ua: 'aarch64/unknown Darwin/24.2.0 libc',
|
|
||||||
expectExt: 'tar.xz',
|
|
||||||
},
|
|
||||||
// delta — Rust, .tar.gz on Linux/macOS
|
|
||||||
{
|
|
||||||
label: 'delta Linux amd64 format',
|
|
||||||
pkg: 'delta',
|
|
||||||
ua: 'x86_64/unknown Linux/5.15.0 libc',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
},
|
|
||||||
// shfmt — Go, bare exe on Linux, .exe on Windows
|
|
||||||
{
|
|
||||||
label: 'shfmt Linux amd64 format',
|
|
||||||
pkg: 'shfmt',
|
|
||||||
ua: 'x86_64/unknown Linux/5.15.0 libc',
|
|
||||||
expectExt: 'exe',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// ====================================================================
|
|
||||||
// Test 3: Edge-case platforms
|
|
||||||
// ====================================================================
|
|
||||||
|
|
||||||
var EDGE_CASES = [
|
|
||||||
// Linux ARM variants
|
|
||||||
{
|
|
||||||
label: 'go Linux aarch64',
|
|
||||||
pkg: 'go',
|
|
||||||
ua: 'aarch64/unknown Linux/5.15.0 libc',
|
|
||||||
expectOs: 'linux',
|
|
||||||
expectNotError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'node Linux aarch64',
|
|
||||||
pkg: 'node',
|
|
||||||
ua: 'aarch64/unknown Linux/5.15.0 libc',
|
|
||||||
expectOs: 'linux',
|
|
||||||
expectNotError: true,
|
|
||||||
},
|
|
||||||
// Alpine/musl — bat should resolve to musl build
|
|
||||||
{
|
|
||||||
label: 'bat Linux musl amd64',
|
|
||||||
pkg: 'bat',
|
|
||||||
ua: 'x86_64/unknown Linux/5.15.0 musl',
|
|
||||||
expectOs: 'linux',
|
|
||||||
expectNotError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'rg Linux musl amd64',
|
|
||||||
pkg: 'rg',
|
|
||||||
ua: 'x86_64/unknown Linux/5.15.0 musl',
|
|
||||||
expectOs: 'linux',
|
|
||||||
expectNotError: true,
|
|
||||||
},
|
|
||||||
// node musl — separate musl build
|
|
||||||
{
|
|
||||||
label: 'node Linux musl amd64',
|
|
||||||
pkg: 'node',
|
|
||||||
ua: 'x86_64/unknown Linux/5.15.0 musl',
|
|
||||||
expectOs: 'linux',
|
|
||||||
expectNotError: true,
|
|
||||||
},
|
|
||||||
// FreeBSD — packages that have freebsd builds
|
|
||||||
{
|
|
||||||
label: 'syncthing FreeBSD amd64',
|
|
||||||
pkg: 'syncthing',
|
|
||||||
ua: 'x86_64/unknown FreeBSD/14.0 libc',
|
|
||||||
expectOs: 'freebsd',
|
|
||||||
expectNotError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'caddy FreeBSD amd64',
|
|
||||||
pkg: 'caddy',
|
|
||||||
ua: 'x86_64/unknown FreeBSD/14.0 libc',
|
|
||||||
expectOs: 'freebsd',
|
|
||||||
expectNotError: true,
|
|
||||||
},
|
|
||||||
// Windows aarch64 — should fallback to amd64 for most packages
|
|
||||||
{
|
|
||||||
label: 'go Windows aarch64',
|
|
||||||
pkg: 'go',
|
|
||||||
ua: 'aarch64/unknown Windows/10.0.22000 msvc',
|
|
||||||
expectOs: 'windows',
|
|
||||||
expectNotError: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// ====================================================================
|
|
||||||
// Test 4: Script generation smoke tests
|
|
||||||
// ====================================================================
|
|
||||||
|
|
||||||
var SCRIPT_CASES = [
|
|
||||||
{ pkg: 'bat', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'bat Linux' },
|
|
||||||
{ pkg: 'go', ua: 'aarch64/unknown Darwin/24.2.0 libc', label: 'go macOS' },
|
|
||||||
{ pkg: 'node', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'node Linux' },
|
|
||||||
{ pkg: 'rg', ua: 'x86_64/unknown Windows/10.0.19041 msvc', label: 'rg Windows' },
|
|
||||||
{ pkg: 'jq', ua: 'aarch64/unknown Darwin/24.2.0 libc', label: 'jq macOS' },
|
|
||||||
{ pkg: 'caddy', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'caddy Linux' },
|
|
||||||
{ pkg: 'shellcheck', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'shellcheck Linux' },
|
|
||||||
{ pkg: 'shfmt', ua: 'aarch64/unknown Darwin/24.2.0 libc', label: 'shfmt macOS' },
|
|
||||||
{ pkg: 'hugo', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'hugo Linux' },
|
|
||||||
{ pkg: 'delta', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'delta Linux' },
|
|
||||||
{ pkg: 'fd', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'fd Linux' },
|
|
||||||
{ pkg: 'fzf', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'fzf Linux' },
|
|
||||||
{ pkg: 'zoxide', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'zoxide Linux' },
|
|
||||||
{ pkg: 'k9s', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'k9s Linux' },
|
|
||||||
{ pkg: 'yq', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'yq Linux' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// ====================================================================
|
|
||||||
// Test 5: API compat — transform-releases vocabulary
|
|
||||||
// ====================================================================
|
|
||||||
|
|
||||||
// The /api/releases/ endpoint uses normalize.js which has its own OS/arch names.
|
|
||||||
// Verify that the Go cache produces correct values after normalization.
|
|
||||||
var API_VOCAB_CASES = [
|
|
||||||
{
|
|
||||||
label: 'bat has macos+linux+windows',
|
|
||||||
pkg: 'bat',
|
|
||||||
expectOses: ['linux', 'macos', 'windows'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'go has macos+linux+windows',
|
|
||||||
pkg: 'go',
|
|
||||||
expectOses: ['linux', 'macos', 'windows'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'node has linux+macos+windows',
|
|
||||||
pkg: 'node',
|
|
||||||
expectOses: ['linux', 'macos', 'windows'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'syncthing has freebsd+linux+macos+windows',
|
|
||||||
pkg: 'syncthing',
|
|
||||||
expectOses: ['freebsd', 'linux', 'macos', 'windows'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'bat has amd64+arm64 arches',
|
|
||||||
pkg: 'bat',
|
|
||||||
expectArches: ['amd64', 'arm64'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'go has amd64+arm64 arches',
|
|
||||||
pkg: 'go',
|
|
||||||
expectArches: ['amd64', 'arm64'],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// ====================================================================
|
|
||||||
// Test 6: Version format validation
|
|
||||||
// ====================================================================
|
|
||||||
|
|
||||||
var VERSION_CHECKS = [
|
|
||||||
'bat',
|
|
||||||
'go',
|
|
||||||
'node',
|
|
||||||
'rg',
|
|
||||||
'caddy',
|
|
||||||
'hugo',
|
|
||||||
'terraform',
|
|
||||||
'jq',
|
|
||||||
'cmake',
|
|
||||||
'zig',
|
|
||||||
];
|
|
||||||
|
|
||||||
// ====================================================================
|
|
||||||
// Test 7: Channel detection
|
|
||||||
// ====================================================================
|
|
||||||
|
|
||||||
// Packages that should have both stable and beta releases
|
|
||||||
var CHANNEL_CASES = [
|
|
||||||
{
|
|
||||||
label: 'go has stable releases',
|
|
||||||
pkg: 'go',
|
|
||||||
channel: 'stable',
|
|
||||||
expectMinCount: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'node has stable releases',
|
|
||||||
pkg: 'node',
|
|
||||||
channel: 'stable',
|
|
||||||
expectMinCount: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'cmake has stable releases',
|
|
||||||
pkg: 'cmake',
|
|
||||||
channel: 'stable',
|
|
||||||
expectMinCount: 5,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// ====================================================================
|
|
||||||
// Runner
|
|
||||||
// ====================================================================
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
var passes = 0;
|
|
||||||
var failures = 0;
|
|
||||||
var knowns = 0;
|
|
||||||
var skips = 0;
|
|
||||||
|
|
||||||
console.log('Initializing build cache...');
|
|
||||||
await Builds.init();
|
|
||||||
|
|
||||||
if (!Fs.existsSync(CACHE_DIR)) {
|
|
||||||
console.error('No cache directory at ' + CACHE_DIR);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
var cachePath = CACHE_DIR;
|
|
||||||
console.log('Using cache: ' + cachePath);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 1: Cache completeness
|
|
||||||
// ================================================================
|
|
||||||
console.log('=== Test 1: Cache Completeness ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
for (var bi = 0; bi < BINARY_PKGS.length; bi++) {
|
|
||||||
var bpkg = BINARY_PKGS[bi];
|
|
||||||
var bfile = Path.join(cachePath, bpkg + '.json');
|
|
||||||
if (!Fs.existsSync(bfile)) {
|
|
||||||
console.log(' FAIL ' + bpkg + ': cache file missing');
|
|
||||||
failures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
var bdata = JSON.parse(Fs.readFileSync(bfile, 'utf8'));
|
|
||||||
if (!bdata.releases || bdata.releases.length === 0) {
|
|
||||||
console.log(' FAIL ' + bpkg + ': 0 releases');
|
|
||||||
failures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Check that at least one release has a download URL
|
|
||||||
var hasDownload = bdata.releases.some(function (r) {
|
|
||||||
return r.download && r.download.startsWith('http');
|
|
||||||
});
|
|
||||||
if (!hasDownload) {
|
|
||||||
console.log(' FAIL ' + bpkg + ': no releases with download URLs');
|
|
||||||
failures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
console.log(' PASS ' + bpkg + ': ' + bdata.releases.length + ' releases');
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var gi = 0; gi < GITTAG_PKGS.length; gi++) {
|
|
||||||
var gpkg = GITTAG_PKGS[gi];
|
|
||||||
var gfile = Path.join(cachePath, gpkg + '.json');
|
|
||||||
if (!Fs.existsSync(gfile)) {
|
|
||||||
console.log(' FAIL ' + gpkg + ' (gittag): cache file missing');
|
|
||||||
failures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
var gdata = JSON.parse(Fs.readFileSync(gfile, 'utf8'));
|
|
||||||
if (!gdata.releases || gdata.releases.length === 0) {
|
|
||||||
console.log(' FAIL ' + gpkg + ' (gittag): 0 releases');
|
|
||||||
failures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
console.log(' PASS ' + gpkg + ' (gittag): ' + gdata.releases.length + ' releases');
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 2: Format selection
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 2: Format Selection ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
for (var fi = 0; fi < FORMAT_CASES.length; fi++) {
|
|
||||||
var ftc = FORMAT_CASES[fi];
|
|
||||||
try {
|
|
||||||
var fr = await InstallerServer.helper({
|
|
||||||
unameAgent: ftc.ua,
|
|
||||||
projectName: ftc.pkg,
|
|
||||||
tag: 'stable',
|
|
||||||
formats: ['tar', 'exe', 'zip', 'xz', 'dmg'],
|
|
||||||
libc: '',
|
|
||||||
});
|
|
||||||
var fpkg = fr[0];
|
|
||||||
if (fpkg.channel === 'error') {
|
|
||||||
console.log(' FAIL ' + ftc.label + ': resolved to error');
|
|
||||||
failures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (fpkg.ext !== ftc.expectExt) {
|
|
||||||
console.log(' FAIL ' + ftc.label + ': got .' + fpkg.ext + ' want .' + ftc.expectExt);
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
console.log(' PASS ' + ftc.label + ': .' + fpkg.ext);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(' ERROR ' + ftc.label + ': ' + e.message);
|
|
||||||
failures++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 3: Edge-case platforms
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 3: Edge-Case Platforms ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
for (var ei = 0; ei < EDGE_CASES.length; ei++) {
|
|
||||||
var etc = EDGE_CASES[ei];
|
|
||||||
try {
|
|
||||||
var er = await InstallerServer.helper({
|
|
||||||
unameAgent: etc.ua,
|
|
||||||
projectName: etc.pkg,
|
|
||||||
tag: 'stable',
|
|
||||||
formats: ['tar', 'exe', 'zip', 'xz', 'dmg'],
|
|
||||||
libc: '',
|
|
||||||
});
|
|
||||||
var epkg = er[0];
|
|
||||||
if (etc.expectNotError && epkg.channel === 'error') {
|
|
||||||
console.log(' FAIL ' + etc.label + ': resolved to error (v' + epkg.version + ')');
|
|
||||||
failures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (etc.expectOs && epkg.os !== etc.expectOs) {
|
|
||||||
console.log(' FAIL ' + etc.label + ': os=' + epkg.os + ' want=' + etc.expectOs);
|
|
||||||
failures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
console.log(' PASS ' + etc.label + ': v' + epkg.version + ' .' + epkg.ext);
|
|
||||||
passes++;
|
|
||||||
} catch (e) {
|
|
||||||
if (etc.known) {
|
|
||||||
console.log(' KNOWN ' + etc.label + ': ' + e.message);
|
|
||||||
knowns++;
|
|
||||||
} else {
|
|
||||||
console.log(' FAIL ' + etc.label + ': ' + e.message);
|
|
||||||
failures++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 4: Script generation smoke tests
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 4: Script Generation ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
for (var si = 0; si < SCRIPT_CASES.length; si++) {
|
|
||||||
var stc = SCRIPT_CASES[si];
|
|
||||||
try {
|
|
||||||
var script = await InstallerServer.serveInstaller(
|
|
||||||
'https://webi.sh',
|
|
||||||
stc.ua,
|
|
||||||
stc.pkg,
|
|
||||||
'stable',
|
|
||||||
'sh',
|
|
||||||
['tar', 'exe', 'zip', 'xz', 'dmg'],
|
|
||||||
'',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Script must contain WEBI_PKG_URL and WEBI_VERSION
|
|
||||||
var hasUrl = /WEBI_PKG_URL='[^']+'/m.test(script);
|
|
||||||
var hasVersion = /WEBI_VERSION='[^']+'/m.test(script);
|
|
||||||
var hasExt = /WEBI_EXT='[^']+'/m.test(script);
|
|
||||||
|
|
||||||
if (!hasUrl) {
|
|
||||||
console.log(' FAIL ' + stc.label + ': missing WEBI_PKG_URL');
|
|
||||||
failures++;
|
|
||||||
} else if (!hasVersion) {
|
|
||||||
console.log(' FAIL ' + stc.label + ': missing WEBI_VERSION');
|
|
||||||
failures++;
|
|
||||||
} else if (!hasExt) {
|
|
||||||
console.log(' FAIL ' + stc.label + ': missing WEBI_EXT');
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
var vMatch = script.match(/WEBI_VERSION='([^']+)'/);
|
|
||||||
var extMatch = script.match(/WEBI_EXT='([^']+)'/);
|
|
||||||
console.log(' PASS ' + stc.label + ': v' + vMatch[1] + ' .' + extMatch[1]);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(' FAIL ' + stc.label + ': ' + e.message.substring(0, 80));
|
|
||||||
failures++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 5: API compat — transform-releases vocabulary
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 5: API Vocabulary (transform-releases) ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
for (var ai = 0; ai < API_VOCAB_CASES.length; ai++) {
|
|
||||||
var atc = API_VOCAB_CASES[ai];
|
|
||||||
try {
|
|
||||||
var ares = await Releases.getReleases({
|
|
||||||
pkg: atc.pkg,
|
|
||||||
ver: '',
|
|
||||||
os: '',
|
|
||||||
arch: '',
|
|
||||||
libc: '',
|
|
||||||
lts: false,
|
|
||||||
channel: '',
|
|
||||||
formats: [],
|
|
||||||
limit: 100,
|
|
||||||
});
|
|
||||||
var arels = ares.releases;
|
|
||||||
|
|
||||||
if (atc.expectOses) {
|
|
||||||
var actualOses = [];
|
|
||||||
for (var oi = 0; oi < arels.length; oi++) {
|
|
||||||
if (actualOses.indexOf(arels[oi].os) === -1) {
|
|
||||||
actualOses.push(arels[oi].os);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
actualOses.sort();
|
|
||||||
var missingOses = [];
|
|
||||||
for (var mi = 0; mi < atc.expectOses.length; mi++) {
|
|
||||||
if (actualOses.indexOf(atc.expectOses[mi]) === -1) {
|
|
||||||
missingOses.push(atc.expectOses[mi]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (missingOses.length > 0) {
|
|
||||||
console.log(' FAIL ' + atc.label + ': missing ' + JSON.stringify(missingOses) + ' (has ' + JSON.stringify(actualOses) + ')');
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
console.log(' PASS ' + atc.label + ': ' + JSON.stringify(atc.expectOses));
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (atc.expectArches) {
|
|
||||||
var actualArches = [];
|
|
||||||
for (var ari = 0; ari < arels.length; ari++) {
|
|
||||||
if (arels[ari].arch && actualArches.indexOf(arels[ari].arch) === -1) {
|
|
||||||
actualArches.push(arels[ari].arch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
actualArches.sort();
|
|
||||||
var missingArches = [];
|
|
||||||
for (var mai = 0; mai < atc.expectArches.length; mai++) {
|
|
||||||
if (actualArches.indexOf(atc.expectArches[mai]) === -1) {
|
|
||||||
missingArches.push(atc.expectArches[mai]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (missingArches.length > 0) {
|
|
||||||
console.log(' FAIL ' + atc.label + ': missing ' + JSON.stringify(missingArches) + ' (has ' + JSON.stringify(actualArches) + ')');
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
console.log(' PASS ' + atc.label);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(' FAIL ' + atc.label + ': ' + e.message);
|
|
||||||
failures++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 6: Version format
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 6: Version Format ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// Version format check: the Go cache may have 'v' prefixes (that's OK —
|
|
||||||
// normalize.js and serve-installer.js strip them). What matters is that
|
|
||||||
// after normalization via transform-releases, versions have no 'v' prefix.
|
|
||||||
for (var vi = 0; vi < VERSION_CHECKS.length; vi++) {
|
|
||||||
var vpkg = VERSION_CHECKS[vi];
|
|
||||||
try {
|
|
||||||
var vres = await Releases.getReleases({
|
|
||||||
pkg: vpkg,
|
|
||||||
ver: '',
|
|
||||||
os: '',
|
|
||||||
arch: '',
|
|
||||||
libc: '',
|
|
||||||
lts: false,
|
|
||||||
channel: '',
|
|
||||||
formats: [],
|
|
||||||
limit: 10,
|
|
||||||
});
|
|
||||||
var vrels = vres.releases;
|
|
||||||
var badVersions = [];
|
|
||||||
for (var vri = 0; vri < vrels.length; vri++) {
|
|
||||||
var ver = vrels[vri].version;
|
|
||||||
if (!ver) {
|
|
||||||
badVersions.push('(empty)');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (ver.startsWith('v')) {
|
|
||||||
badVersions.push(ver);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (badVersions.length > 0) {
|
|
||||||
console.log(' FAIL ' + vpkg + ': v-prefix not stripped: ' + badVersions[0]);
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
console.log(' PASS ' + vpkg + ': latest=' + (vrels[0] ? vrels[0].version : '?'));
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(' FAIL ' + vpkg + ': ' + e.message);
|
|
||||||
failures++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 7: Channel detection
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 7: Channel Detection ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
for (var ci = 0; ci < CHANNEL_CASES.length; ci++) {
|
|
||||||
var ctc = CHANNEL_CASES[ci];
|
|
||||||
try {
|
|
||||||
var cres = await Releases.getReleases({
|
|
||||||
pkg: ctc.pkg,
|
|
||||||
ver: '',
|
|
||||||
os: '',
|
|
||||||
arch: '',
|
|
||||||
libc: '',
|
|
||||||
lts: false,
|
|
||||||
channel: ctc.channel,
|
|
||||||
formats: [],
|
|
||||||
limit: 100,
|
|
||||||
});
|
|
||||||
var count = cres.releases.length;
|
|
||||||
if (count < ctc.expectMinCount) {
|
|
||||||
console.log(' FAIL ' + ctc.label + ': only ' + count + ' (want >=' + ctc.expectMinCount + ')');
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
console.log(' PASS ' + ctc.label + ': ' + count + ' releases');
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(' FAIL ' + ctc.label + ': ' + e.message);
|
|
||||||
failures++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Summary
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Results: ' + passes + ' passed, ' + failures + ' failed, ' + knowns + ' known, ' + skips + ' skipped ===');
|
|
||||||
if (failures > 0) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(function (err) {
|
|
||||||
console.error(err.stack);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,339 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
// Fleet-wide diff: compare a candidate host (e.g. beta.webi.sh) against
|
|
||||||
// production for every cached package, across multiple OS/arch combos.
|
|
||||||
// Outputs TSV for grep/sort.
|
|
||||||
//
|
|
||||||
// Usage:
|
|
||||||
// node _webi/test-fleet-diff.js
|
|
||||||
// --cand-url=https://beta.webi.sh
|
|
||||||
// --prod-url=https://webinstall.dev # default
|
|
||||||
// --kind=api # default; or "installer"
|
|
||||||
// --pkgs=bat,go,... # default: all cached packages
|
|
||||||
// --concurrency=8 # default
|
|
||||||
// --out=fleet-api.tsv # default: stdout
|
|
||||||
|
|
||||||
let Fs = require('node:fs/promises');
|
|
||||||
let Os = require('node:os');
|
|
||||||
let Path = require('node:path');
|
|
||||||
let Https = require('node:https');
|
|
||||||
|
|
||||||
let CACHE_DIR = Path.join(Os.homedir(), '.cache/webi/legacy');
|
|
||||||
|
|
||||||
function arg(name, dflt) {
|
|
||||||
for (let a of process.argv) {
|
|
||||||
if (a.startsWith(`--${name}=`)) {
|
|
||||||
return a.slice(name.length + 3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dflt;
|
|
||||||
}
|
|
||||||
|
|
||||||
let CAND_URL = arg('cand-url', 'https://beta.webi.sh').replace(/\/+$/, '');
|
|
||||||
let PROD_URL = arg('prod-url', 'https://webinstall.dev').replace(/\/+$/, '');
|
|
||||||
let KIND = arg('kind', 'api');
|
|
||||||
let PKGS_ARG = arg('pkgs', '');
|
|
||||||
let CONCURRENCY = parseInt(arg('concurrency', '8'), 10);
|
|
||||||
let OUT = arg('out', '');
|
|
||||||
|
|
||||||
// OS/arch matrix for API mode
|
|
||||||
let API_MATRIX = [
|
|
||||||
{ os: 'macos', arch: 'amd64' },
|
|
||||||
{ os: 'macos', arch: 'arm64' },
|
|
||||||
{ os: 'linux', arch: 'amd64' },
|
|
||||||
{ os: 'linux', arch: 'arm64' },
|
|
||||||
{ os: 'linux', arch: 'armv7l' },
|
|
||||||
{ os: 'windows', arch: 'amd64' },
|
|
||||||
{ os: 'freebsd', arch: 'amd64' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// UA strings for installer mode
|
|
||||||
let INSTALLER_MATRIX = [
|
|
||||||
{ label: 'macos_arm64', ua: 'aarch64/unknown Darwin/24.2.0 libc' },
|
|
||||||
{ label: 'macos_amd64', ua: 'x86_64/unknown Darwin/23.0.0 libc' },
|
|
||||||
{ label: 'linux_amd64', ua: 'x86_64/unknown Linux/5.15.0 libc' },
|
|
||||||
{ label: 'linux_arm64', ua: 'aarch64/unknown Linux/5.15.0 libc' },
|
|
||||||
{ label: 'linux_musl', ua: 'x86_64/unknown Linux/5.15.0 musl' },
|
|
||||||
{ label: 'windows_amd64', ua: 'x86_64/unknown Windows/10.0.19041 msvc' },
|
|
||||||
];
|
|
||||||
|
|
||||||
function httpsGet(url, headers) {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
let opts = { headers: headers || {}, timeout: 15000 };
|
|
||||||
let req = Https.get(url, opts, function (res) {
|
|
||||||
// Follow one redirect
|
|
||||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
||||||
let redir = res.headers.location;
|
|
||||||
if (redir.startsWith('/')) {
|
|
||||||
let m = url.match(/^(https?:\/\/[^/]+)/);
|
|
||||||
redir = (m ? m[1] : '') + redir;
|
|
||||||
}
|
|
||||||
Https.get(redir, opts, function (res2) {
|
|
||||||
let data = '';
|
|
||||||
res2.on('data', function (c) { data += c; });
|
|
||||||
res2.on('end', function () { resolve({ status: res2.statusCode, body: data }); });
|
|
||||||
}).on('error', reject);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let data = '';
|
|
||||||
res.on('data', function (c) { data += c; });
|
|
||||||
res.on('end', function () { resolve({ status: res.statusCode, body: data }); });
|
|
||||||
});
|
|
||||||
req.on('error', reject);
|
|
||||||
req.on('timeout', function () { req.destroy(new Error('timeout')); });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function listCachedPkgs() {
|
|
||||||
let entries = await Fs.readdir(CACHE_DIR);
|
|
||||||
return entries
|
|
||||||
.filter(function (n) { return n.endsWith('.json') && !n.endsWith('.updated.txt'); })
|
|
||||||
.map(function (n) { return n.slice(0, -5); })
|
|
||||||
.sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeFirst(json) {
|
|
||||||
try {
|
|
||||||
let arr = JSON.parse(json);
|
|
||||||
if (!Array.isArray(arr) || arr.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return arr[0];
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseInstallerVars(script) {
|
|
||||||
let vars = {};
|
|
||||||
let re = /^(?:export\s+)?(WEBI_\w+|PKG_NAME)='([^']*)'/gm;
|
|
||||||
let m;
|
|
||||||
while ((m = re.exec(script)) !== null) {
|
|
||||||
vars[m[1]] = m[2];
|
|
||||||
}
|
|
||||||
return vars;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function diffApi(pkg, os, arch) {
|
|
||||||
let qs = `?os=${os}&arch=${arch}&limit=1`;
|
|
||||||
let candUrl = `${CAND_URL}/api/releases/${pkg}@stable.json${qs}`;
|
|
||||||
let prodUrl = `${PROD_URL}/api/releases/${pkg}@stable.json${qs}`;
|
|
||||||
|
|
||||||
let cand, prod;
|
|
||||||
try {
|
|
||||||
[cand, prod] = await Promise.all([httpsGet(candUrl), httpsGet(prodUrl)]);
|
|
||||||
} catch (e) {
|
|
||||||
return { pkg, os, arch, status: 'fetch_error', detail: e.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cand.status !== 200 || prod.status !== 200) {
|
|
||||||
return {
|
|
||||||
pkg, os, arch, status: 'http_error',
|
|
||||||
detail: `cand=${cand.status} prod=${prod.status}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let candFirst = safeFirst(cand.body);
|
|
||||||
let prodFirst = safeFirst(prod.body);
|
|
||||||
|
|
||||||
let candErr = !candFirst || candFirst.channel === 'error';
|
|
||||||
let prodErr = !prodFirst || prodFirst.channel === 'error';
|
|
||||||
|
|
||||||
if (candErr && prodErr) {
|
|
||||||
return { pkg, os, arch, status: 'both_error', detail: '' };
|
|
||||||
}
|
|
||||||
if (candErr && !prodErr) {
|
|
||||||
return {
|
|
||||||
pkg, os, arch, status: 'cand_only_error',
|
|
||||||
detail: `prod=${prodFirst.version}/${prodFirst.ext}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!candErr && prodErr) {
|
|
||||||
return {
|
|
||||||
pkg, os, arch, status: 'prod_only_error',
|
|
||||||
detail: `cand=${candFirst.version}/${candFirst.ext}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both succeeded — diff the key fields
|
|
||||||
let diffs = [];
|
|
||||||
for (let f of ['os', 'arch', 'libc', 'ext']) {
|
|
||||||
if (candFirst[f] !== prodFirst[f]) {
|
|
||||||
diffs.push(`${f}:cand=${candFirst[f]}|prod=${prodFirst[f]}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let ver = candFirst.version === prodFirst.version
|
|
||||||
? candFirst.version
|
|
||||||
: `cand=${candFirst.version}|prod=${prodFirst.version}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
pkg, os, arch,
|
|
||||||
status: diffs.length === 0 ? 'match' : 'diff',
|
|
||||||
detail: diffs.length === 0 ? `v${ver} ${candFirst.ext}` : `v${ver} ${diffs.join(',')}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function diffInstaller(pkg, label, ua) {
|
|
||||||
let candUrl = `${CAND_URL}/api/installers/${pkg}@stable.sh`;
|
|
||||||
let prodUrl = `${PROD_URL}/api/installers/${pkg}@stable.sh`;
|
|
||||||
let headers = { 'User-Agent': ua };
|
|
||||||
|
|
||||||
let cand, prod;
|
|
||||||
try {
|
|
||||||
[cand, prod] = await Promise.all([
|
|
||||||
httpsGet(candUrl, headers),
|
|
||||||
httpsGet(prodUrl, headers),
|
|
||||||
]);
|
|
||||||
} catch (e) {
|
|
||||||
return { pkg, label, status: 'fetch_error', detail: e.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cand.status !== 200 || prod.status !== 200) {
|
|
||||||
return {
|
|
||||||
pkg, label, status: 'http_error',
|
|
||||||
detail: `cand=${cand.status} prod=${prod.status}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let candVars = parseInstallerVars(cand.body);
|
|
||||||
let prodVars = parseInstallerVars(prod.body);
|
|
||||||
|
|
||||||
let candHas = candVars.WEBI_PKG_URL && candVars.WEBI_EXT && candVars.WEBI_EXT !== 'err';
|
|
||||||
let prodHas = prodVars.WEBI_PKG_URL && prodVars.WEBI_EXT && prodVars.WEBI_EXT !== 'err';
|
|
||||||
|
|
||||||
if (!candHas && !prodHas) {
|
|
||||||
return { pkg, label, status: 'both_error', detail: '' };
|
|
||||||
}
|
|
||||||
if (!candHas && prodHas) {
|
|
||||||
return {
|
|
||||||
pkg, label, status: 'cand_only_error',
|
|
||||||
detail: `prod=${prodVars.WEBI_VERSION}/${prodVars.WEBI_EXT}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (candHas && !prodHas) {
|
|
||||||
return {
|
|
||||||
pkg, label, status: 'prod_only_error',
|
|
||||||
detail: `cand=${candVars.WEBI_VERSION}/${candVars.WEBI_EXT}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Diff WEBI_OS, WEBI_ARCH, WEBI_EXT (PKG_NAME may differ for aliases)
|
|
||||||
let diffs = [];
|
|
||||||
for (let v of ['WEBI_OS', 'WEBI_ARCH', 'WEBI_EXT']) {
|
|
||||||
if (candVars[v] !== prodVars[v]) {
|
|
||||||
diffs.push(`${v}:cand=${candVars[v]}|prod=${prodVars[v]}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let ver = candVars.WEBI_VERSION === prodVars.WEBI_VERSION
|
|
||||||
? candVars.WEBI_VERSION
|
|
||||||
: `cand=${candVars.WEBI_VERSION}|prod=${prodVars.WEBI_VERSION}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
pkg, label,
|
|
||||||
status: diffs.length === 0 ? 'match' : 'diff',
|
|
||||||
detail: diffs.length === 0 ? `v${ver} ${candVars.WEBI_EXT}` : `v${ver} ${diffs.join(',')}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pool(items, fn, concurrency) {
|
|
||||||
let results = new Array(items.length);
|
|
||||||
let i = 0;
|
|
||||||
async function worker() {
|
|
||||||
while (true) {
|
|
||||||
let idx = i++;
|
|
||||||
if (idx >= items.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
results[idx] = await fn(items[idx], idx);
|
|
||||||
} catch (e) {
|
|
||||||
results[idx] = { status: 'exception', detail: e.message, _item: items[idx] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let workers = [];
|
|
||||||
for (let k = 0; k < concurrency; k++) {
|
|
||||||
workers.push(worker());
|
|
||||||
}
|
|
||||||
await Promise.all(workers);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
let pkgs;
|
|
||||||
if (PKGS_ARG) {
|
|
||||||
pkgs = PKGS_ARG.split(',').filter(Boolean);
|
|
||||||
} else {
|
|
||||||
pkgs = await listCachedPkgs();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(`Comparing ${pkgs.length} packages: ${CAND_URL} (cand) vs ${PROD_URL} (prod)`);
|
|
||||||
console.error(`Mode: ${KIND}, concurrency: ${CONCURRENCY}`);
|
|
||||||
|
|
||||||
let jobs = [];
|
|
||||||
if (KIND === 'api') {
|
|
||||||
for (let pkg of pkgs) {
|
|
||||||
for (let combo of API_MATRIX) {
|
|
||||||
jobs.push({ pkg, os: combo.os, arch: combo.arch });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (KIND === 'installer') {
|
|
||||||
for (let pkg of pkgs) {
|
|
||||||
for (let combo of INSTALLER_MATRIX) {
|
|
||||||
jobs.push({ pkg, label: combo.label, ua: combo.ua });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error(`Unknown kind: ${KIND}`);
|
|
||||||
process.exit(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
let started = Date.now();
|
|
||||||
let results = await pool(jobs, async function (job) {
|
|
||||||
if (KIND === 'api') {
|
|
||||||
return diffApi(job.pkg, job.os, job.arch);
|
|
||||||
}
|
|
||||||
return diffInstaller(job.pkg, job.label, job.ua);
|
|
||||||
}, CONCURRENCY);
|
|
||||||
let elapsed = ((Date.now() - started) / 1000).toFixed(1);
|
|
||||||
|
|
||||||
// TSV output
|
|
||||||
let lines = [];
|
|
||||||
if (KIND === 'api') {
|
|
||||||
lines.push(['pkg', 'os', 'arch', 'status', 'detail'].join('\t'));
|
|
||||||
for (let r of results) {
|
|
||||||
lines.push([r.pkg, r.os, r.arch, r.status, r.detail || ''].join('\t'));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lines.push(['pkg', 'target', 'status', 'detail'].join('\t'));
|
|
||||||
for (let r of results) {
|
|
||||||
lines.push([r.pkg, r.label, r.status, r.detail || ''].join('\t'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let body = lines.join('\n') + '\n';
|
|
||||||
|
|
||||||
if (OUT) {
|
|
||||||
await Fs.writeFile(OUT, body, 'utf8');
|
|
||||||
console.error(`Wrote ${OUT}`);
|
|
||||||
} else {
|
|
||||||
process.stdout.write(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Summary to stderr
|
|
||||||
let counts = {};
|
|
||||||
for (let r of results) {
|
|
||||||
counts[r.status] = (counts[r.status] || 0) + 1;
|
|
||||||
}
|
|
||||||
console.error('');
|
|
||||||
console.error(`=== Summary (${elapsed}s, ${results.length} jobs) ===`);
|
|
||||||
for (let s of Object.keys(counts).sort()) {
|
|
||||||
console.error(` ${s}: ${counts[s]}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(function (err) {
|
|
||||||
console.error(err.stack);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,444 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
let InstallerServer = require('./serve-installer.js');
|
|
||||||
let Builds = require('./builds.js');
|
|
||||||
|
|
||||||
// Real User-Agent strings sent by webi bootstrap scripts.
|
|
||||||
//
|
|
||||||
// Libc taxonomy:
|
|
||||||
// none = static build, no runtime libc dep (often built with musl, but self-contained)
|
|
||||||
// musl = requires musl C/C++ runtime at runtime (e.g. node-musl)
|
|
||||||
// gnu = requires glibc at runtime (crashes on musl-only/Alpine)
|
|
||||||
// libc = host UA value meaning "I have glibc" (not used in release metadata)
|
|
||||||
//
|
|
||||||
// Known issues:
|
|
||||||
//
|
|
||||||
// 1. WATERFALL libc vs gnu: The WATERFALL maps `libc` => ['none', 'libc']
|
|
||||||
// but never tries 'gnu'. Packages with glibc-linked builds (libc='gnu' in
|
|
||||||
// Go cache) won't match for hosts reporting 'libc'. Fix: update WATERFALL
|
|
||||||
// to `libc: ['none', 'gnu', 'libc']` in build-classifier submodule.
|
|
||||||
//
|
|
||||||
// 2. Go cache .git regression: The Go cache includes .git source repo URLs
|
|
||||||
// as releases, creating ANYOS/ANYARCH triplets. These match before
|
|
||||||
// platform-specific binaries. Fix: exclude .git from Go cache output.
|
|
||||||
|
|
||||||
let UA_CASES = [
|
|
||||||
// === macOS (no libc issue — darwin uses libc='none') ===
|
|
||||||
{
|
|
||||||
label: 'bat macOS arm64',
|
|
||||||
pkg: 'bat',
|
|
||||||
ua: 'aarch64/unknown Darwin/24.2.0 libc',
|
|
||||||
expectOs: 'darwin',
|
|
||||||
expectArch: 'aarch64',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'bat macOS amd64',
|
|
||||||
pkg: 'bat',
|
|
||||||
ua: 'x86_64/unknown Darwin/23.0.0 libc',
|
|
||||||
expectOs: 'darwin',
|
|
||||||
expectArch: 'x86_64',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'go macOS arm64',
|
|
||||||
pkg: 'go',
|
|
||||||
ua: 'aarch64/unknown Darwin/24.2.0 libc',
|
|
||||||
expectOs: 'darwin',
|
|
||||||
expectArch: 'aarch64',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'node macOS arm64',
|
|
||||||
pkg: 'node',
|
|
||||||
ua: 'aarch64/unknown Darwin/24.2.0 libc',
|
|
||||||
expectOs: 'darwin',
|
|
||||||
expectArch: 'aarch64',
|
|
||||||
expectExt: 'tar.xz',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'rg macOS arm64',
|
|
||||||
pkg: 'rg',
|
|
||||||
ua: 'aarch64/unknown Darwin/24.2.0 libc',
|
|
||||||
expectOs: 'darwin',
|
|
||||||
expectArch: 'aarch64',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
},
|
|
||||||
|
|
||||||
// === macOS universal2 — packages where recent darwin builds are universal-only ===
|
|
||||||
// These currently resolve to ancient versions because universal2 entries are
|
|
||||||
// dropped by the classifier. The GOER's legacy export needs to emit these
|
|
||||||
// with arch: "x86_64" so the classifier accepts them. The darwin WATERFALL
|
|
||||||
// (aarch64 falls back to x86_64) handles aarch64 users.
|
|
||||||
{
|
|
||||||
label: 'cmake macOS arm64 (universal2)',
|
|
||||||
pkg: 'cmake',
|
|
||||||
ua: 'aarch64/unknown Darwin/24.2.0 libc',
|
|
||||||
expectOs: 'darwin',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
expectMinVersion: '4.0.0',
|
|
||||||
known: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'cmake macOS amd64 (universal2)',
|
|
||||||
pkg: 'cmake',
|
|
||||||
ua: 'x86_64/unknown Darwin/23.0.0 libc',
|
|
||||||
expectOs: 'darwin',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
expectMinVersion: '4.0.0',
|
|
||||||
known: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'hugo macOS arm64 (universal2)',
|
|
||||||
pkg: 'hugo',
|
|
||||||
ua: 'aarch64/unknown Darwin/24.2.0 libc',
|
|
||||||
expectOs: 'darwin',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
expectMinVersion: '0.140.0',
|
|
||||||
known: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'hugo macOS amd64 (universal2)',
|
|
||||||
pkg: 'hugo',
|
|
||||||
ua: 'x86_64/unknown Darwin/23.0.0 libc',
|
|
||||||
expectOs: 'darwin',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
expectMinVersion: '0.140.0',
|
|
||||||
known: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
// === Windows ===
|
|
||||||
{
|
|
||||||
label: 'bat Windows amd64',
|
|
||||||
pkg: 'bat',
|
|
||||||
ua: 'x86_64/unknown Windows/10.0.19041 msvc',
|
|
||||||
expectOs: 'windows',
|
|
||||||
expectArch: 'x86_64',
|
|
||||||
expectExt: 'zip',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'go Windows amd64',
|
|
||||||
pkg: 'go',
|
|
||||||
ua: 'x86_64/unknown Windows/10.0.19041 msvc',
|
|
||||||
expectOs: 'windows',
|
|
||||||
expectArch: 'x86_64',
|
|
||||||
expectExt: 'zip',
|
|
||||||
},
|
|
||||||
|
|
||||||
// === Linux musl (Alpine/Docker) ===
|
|
||||||
{
|
|
||||||
label: 'bat Linux musl',
|
|
||||||
pkg: 'bat',
|
|
||||||
ua: 'x86_64/unknown Linux/5.15.0 musl',
|
|
||||||
expectOs: 'linux',
|
|
||||||
expectArch: 'x86_64',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
},
|
|
||||||
|
|
||||||
// === Linux glibc — packages with libc='none' in cache ===
|
|
||||||
{
|
|
||||||
label: 'go Linux amd64',
|
|
||||||
pkg: 'go',
|
|
||||||
ua: 'x86_64/unknown Linux/5.15.0 libc',
|
|
||||||
expectOs: 'linux',
|
|
||||||
expectArch: 'x86_64',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
},
|
|
||||||
// === Linux glibc — packages with libc='gnu' in cache ===
|
|
||||||
// These previously failed (WATERFALL libc→gnu gap). Fixed by adding
|
|
||||||
// 'gnu' to the libc candidates for glibc hosts in _enumerateTriplets.
|
|
||||||
{
|
|
||||||
label: 'bat Linux amd64',
|
|
||||||
pkg: 'bat',
|
|
||||||
ua: 'x86_64/unknown Linux/5.15.0 libc',
|
|
||||||
expectOs: 'linux',
|
|
||||||
expectArch: 'x86_64',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'rg Linux amd64',
|
|
||||||
pkg: 'rg',
|
|
||||||
ua: 'x86_64/unknown Linux/5.15.0 libc',
|
|
||||||
expectOs: 'linux',
|
|
||||||
expectArch: 'x86_64',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'node Linux amd64',
|
|
||||||
pkg: 'node',
|
|
||||||
ua: 'x86_64/unknown Linux/5.15.0 libc',
|
|
||||||
expectOs: 'linux',
|
|
||||||
expectArch: 'x86_64',
|
|
||||||
expectExt: 'tar.xz',
|
|
||||||
},
|
|
||||||
|
|
||||||
// === Packages with .git source URLs in old releases ===
|
|
||||||
// These previously failed (ANYOS .git matched before platform binary).
|
|
||||||
// Fixed by putting specific OS before ANYOS in triplet enumeration.
|
|
||||||
{
|
|
||||||
label: 'jq macOS arm64',
|
|
||||||
pkg: 'jq',
|
|
||||||
ua: 'aarch64/unknown Darwin/24.2.0 libc',
|
|
||||||
expectOs: 'darwin',
|
|
||||||
expectArch: 'aarch64',
|
|
||||||
expectExt: 'exe',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'caddy macOS arm64',
|
|
||||||
pkg: 'caddy',
|
|
||||||
ua: 'aarch64/unknown Darwin/24.2.0 libc',
|
|
||||||
expectOs: 'darwin',
|
|
||||||
expectArch: 'aarch64',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'caddy Linux amd64',
|
|
||||||
pkg: 'caddy',
|
|
||||||
ua: 'x86_64/unknown Linux/5.15.0 libc',
|
|
||||||
expectOs: 'linux',
|
|
||||||
expectArch: 'x86_64',
|
|
||||||
expectExt: 'tar.gz',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
let failures = 0;
|
|
||||||
let passes = 0;
|
|
||||||
let knowns = 0;
|
|
||||||
let errors = 0;
|
|
||||||
|
|
||||||
console.log('Initializing build cache...');
|
|
||||||
await Builds.init();
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
console.log('=== Installer Resolution Tests ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
for (let tc of UA_CASES) {
|
|
||||||
try {
|
|
||||||
let [pkg, params] = await InstallerServer.helper({
|
|
||||||
unameAgent: tc.ua,
|
|
||||||
projectName: tc.pkg,
|
|
||||||
tag: 'stable',
|
|
||||||
formats: ['tar', 'exe', 'zip', 'xz', 'dmg'],
|
|
||||||
libc: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Known issue — just verify it fails as expected
|
|
||||||
if (tc.known) {
|
|
||||||
let isError = pkg.channel === 'error' || !pkg.download || pkg.download.includes('doesntexist') || pkg.ext === 'git';
|
|
||||||
let isStale = false;
|
|
||||||
if (tc.expectMinVersion && pkg.version) {
|
|
||||||
let got = pkg.version.replace(/^v/, '').split('.').map(Number);
|
|
||||||
let want = tc.expectMinVersion.split('.').map(Number);
|
|
||||||
for (let i = 0; i < want.length; i++) {
|
|
||||||
if ((got[i] || 0) < want[i]) { isStale = true; break; }
|
|
||||||
if ((got[i] || 0) > want[i]) { break; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isError || isStale) {
|
|
||||||
let detail = isStale ? `stale v${pkg.version} < v${tc.expectMinVersion}` : '';
|
|
||||||
console.log(` KNOWN ${tc.label}${detail ? ': ' + detail : ''}`);
|
|
||||||
knowns++;
|
|
||||||
} else {
|
|
||||||
console.log(` PASS ${tc.label} (known issue resolved!): v${pkg.version} .${pkg.ext} ${(pkg.download || '').split('/').pop()}`);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pkg.channel === 'error') {
|
|
||||||
console.log(` FAIL ${tc.label}: resolved to error package`);
|
|
||||||
failures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let diffs = [];
|
|
||||||
|
|
||||||
if (tc.expectOs && pkg.os !== tc.expectOs) {
|
|
||||||
diffs.push(`os: got=${pkg.os} want=${tc.expectOs}`);
|
|
||||||
}
|
|
||||||
if (tc.expectArch && pkg.arch !== tc.expectArch) {
|
|
||||||
diffs.push(`arch: got=${pkg.arch} want=${tc.expectArch}`);
|
|
||||||
}
|
|
||||||
if (tc.expectExt && pkg.ext !== tc.expectExt) {
|
|
||||||
diffs.push(`ext: got=${pkg.ext} want=${tc.expectExt}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pkg.version || pkg.version === '0.0.0') {
|
|
||||||
diffs.push('version: missing or zero');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pkg.download || pkg.download.includes('doesntexist')) {
|
|
||||||
diffs.push('download: missing or error');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (diffs.length > 0) {
|
|
||||||
console.log(` FAIL ${tc.label}: ${diffs.join(', ')}`);
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
console.log(` PASS ${tc.label}: v${pkg.version} .${pkg.ext} ${pkg.download.split('/').pop()}`);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (tc.known) {
|
|
||||||
console.log(` KNOWN ${tc.label} (error: ${err.message})`);
|
|
||||||
knowns++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
console.log(` ERROR ${tc.label}: ${err.message}`);
|
|
||||||
errors++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
console.log(`=== Results: ${passes} passed, ${failures} failed, ${knowns} known, ${errors} errors ===`);
|
|
||||||
if (failures > 0 || errors > 0) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache value validation: the classifier re-parses filenames and rejects
|
|
||||||
// entries where the cache os/arch doesn't match. These checks prevent
|
|
||||||
// regressions where someone "normalizes" cache values in a way that
|
|
||||||
// breaks the classifier.
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Cache Value Validation ===');
|
|
||||||
console.log('');
|
|
||||||
let cacheFailures = await validateCacheValues();
|
|
||||||
if (cacheFailures > 0) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that cache os/arch values match what the Node classifier expects
|
|
||||||
// to extract from the download filename. The classifier is a submodule and
|
|
||||||
// is NOT being modified — the cache must emit values it already recognizes.
|
|
||||||
//
|
|
||||||
// Known bug (LIVE_cache): the Go legacy export previously translated
|
|
||||||
// solaris/illumos → sunos in the cache, but the filenames still say
|
|
||||||
// solaris/illumos. The classifier detects the filename value and rejects
|
|
||||||
// the entry when it doesn't match. Same issue with universal2 arch.
|
|
||||||
//
|
|
||||||
// Rule: cache os/arch must match the filename, not some "canonical" form.
|
|
||||||
|
|
||||||
// Cache os/arch values must match what the Node classifier extracts from the
|
|
||||||
// download filename. The classifier already recognizes solaris, illumos, sunos,
|
|
||||||
// armhf, armel, etc. — these are not new values. The only value the classifier
|
|
||||||
// does NOT recognize is "universal2" — use "x86_64" instead.
|
|
||||||
//
|
|
||||||
// matchField: which field to check in the release entry ('name' or 'download')
|
|
||||||
let CACHE_CHECKS = [
|
|
||||||
// The classifier knows "solaris" as an OS. Filenames/URLs say "solaris".
|
|
||||||
// Do NOT translate to "sunos" — that creates a mismatch and drops the entry.
|
|
||||||
{
|
|
||||||
label: 'terraform solaris entries have os=solaris (not sunos)',
|
|
||||||
pkg: 'terraform',
|
|
||||||
matchField: 'download',
|
|
||||||
filenameMatch: /solaris/,
|
|
||||||
field: 'os',
|
|
||||||
expect: 'solaris',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'syncthing solaris entries have os=solaris (not sunos)',
|
|
||||||
pkg: 'syncthing',
|
|
||||||
matchField: 'download',
|
|
||||||
filenameMatch: /solaris/,
|
|
||||||
field: 'os',
|
|
||||||
expect: 'solaris',
|
|
||||||
},
|
|
||||||
// The classifier knows "illumos" as an OS. Don't translate to sunos.
|
|
||||||
{
|
|
||||||
label: 'syncthing illumos entries have os=illumos (not sunos)',
|
|
||||||
pkg: 'syncthing',
|
|
||||||
matchField: 'download',
|
|
||||||
filenameMatch: /illumos/,
|
|
||||||
field: 'os',
|
|
||||||
expect: 'illumos',
|
|
||||||
},
|
|
||||||
// node.js uses "sunos" in filenames — cache must say "sunos" (already correct)
|
|
||||||
{
|
|
||||||
label: 'node sunos entries have os=sunos',
|
|
||||||
pkg: 'node',
|
|
||||||
matchField: 'name',
|
|
||||||
filenameMatch: /sunos/,
|
|
||||||
field: 'os',
|
|
||||||
expect: 'sunos',
|
|
||||||
},
|
|
||||||
// The classifier maps "universal" in filenames → x86_64. The classifier does
|
|
||||||
// NOT recognize "universal2". Cache must say arch="x86_64" for these entries.
|
|
||||||
// aarch64 users get them via the darwin WATERFALL (aarch64 → x86_64 fallback).
|
|
||||||
{
|
|
||||||
label: 'cmake universal entries have arch=x86_64 (not universal2)',
|
|
||||||
pkg: 'cmake',
|
|
||||||
matchField: 'download',
|
|
||||||
filenameMatch: /universal/,
|
|
||||||
field: 'arch',
|
|
||||||
expect: 'x86_64',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'hugo universal entries have arch=x86_64 (not universal2)',
|
|
||||||
pkg: 'hugo',
|
|
||||||
matchField: 'download',
|
|
||||||
filenameMatch: /universal/,
|
|
||||||
field: 'arch',
|
|
||||||
expect: 'x86_64',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
async function validateCacheValues() {
|
|
||||||
let Os = require('node:os');
|
|
||||||
let Path = require('path');
|
|
||||||
let Fs = require('fs');
|
|
||||||
|
|
||||||
let cachePath = Path.join(Os.homedir(), '.cache/webi/legacy');
|
|
||||||
if (!Fs.existsSync(cachePath)) {
|
|
||||||
console.log(' SKIP: no cache directory at ' + cachePath);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let failures = 0;
|
|
||||||
|
|
||||||
for (let check of CACHE_CHECKS) {
|
|
||||||
let filePath = Path.join(cachePath, `${check.pkg}.json`);
|
|
||||||
if (!Fs.existsSync(filePath)) {
|
|
||||||
console.log(` SKIP ${check.label}: no cache file`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = JSON.parse(Fs.readFileSync(filePath, 'utf8'));
|
|
||||||
let matchField = check.matchField || 'name';
|
|
||||||
let matched = data.releases.filter(function (r) {
|
|
||||||
return check.filenameMatch.test(r[matchField]);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (matched.length === 0) {
|
|
||||||
console.log(` SKIP ${check.label}: no matching filenames`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let wrong = matched.filter(function (r) {
|
|
||||||
return r[check.field] !== check.expect;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (wrong.length > 0) {
|
|
||||||
let sample = wrong[0];
|
|
||||||
console.log(
|
|
||||||
` FAIL ${check.label}: ${wrong.length}/${matched.length} entries have` +
|
|
||||||
` ${check.field}="${sample[check.field]}" (want "${check.expect}")` +
|
|
||||||
` e.g. ${sample.name}`,
|
|
||||||
);
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
console.log(` PASS ${check.label}: ${matched.length} entries OK`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
console.log(`=== Cache Validation: ${CACHE_CHECKS.length - failures} passed, ${failures} failed ===`);
|
|
||||||
return failures;
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(function (err) {
|
|
||||||
console.error(err.stack);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,450 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
// Compare _cache vs LIVE_cache for correctness and compatibility.
|
|
||||||
//
|
|
||||||
// Rules (from NODER_PURPOSE.md):
|
|
||||||
// - _cache should be more complete and more correct than LIVE_cache
|
|
||||||
// - Must NOT introduce new tags (OS, arch, libc) that don't exist in LIVE_cache
|
|
||||||
// - Must NOT break compatibility with existing data
|
|
||||||
//
|
|
||||||
// Usage: node _webi/test-live-cache-diff.js
|
|
||||||
|
|
||||||
var Fs = require('node:fs');
|
|
||||||
var Os = require('node:os');
|
|
||||||
var Path = require('node:path');
|
|
||||||
|
|
||||||
// CACHE_DIR is the live cache produced by webicached (flat layout).
|
|
||||||
// LIVE_DIR is the historical snapshot taken pre-cutover (month-bucketed).
|
|
||||||
var CACHE_DIR = Path.join(Os.homedir(), '.cache/webi/legacy');
|
|
||||||
var LIVE_DIR = Path.join(__dirname, '..', 'LIVE_cache');
|
|
||||||
|
|
||||||
// resolveLayout: figures out whether dir uses the flat layout
|
|
||||||
// (~/.cache/webi/legacy/<pkg>.json) or the legacy month-bucketed layout
|
|
||||||
// (LIVE_cache/<YYYY-MM>/<pkg>.json) and returns the directory to read from.
|
|
||||||
function resolveLayout(dir) {
|
|
||||||
if (!Fs.existsSync(dir)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var entries = Fs.readdirSync(dir);
|
|
||||||
var months = entries
|
|
||||||
.filter(function (d) { return /^\d{4}-\d{2}$/.test(d); })
|
|
||||||
.sort()
|
|
||||||
.reverse();
|
|
||||||
if (months[0]) {
|
|
||||||
return Path.join(dir, months[0]);
|
|
||||||
}
|
|
||||||
// Flat layout (cache files directly under dir).
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadReleases(layoutPath, pkg) {
|
|
||||||
var file = Path.join(layoutPath, pkg + '.json');
|
|
||||||
if (!Fs.existsSync(file)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.parse(Fs.readFileSync(file, 'utf8'));
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function uniqueValues(releases, field) {
|
|
||||||
var seen = {};
|
|
||||||
for (var i = 0; i < releases.length; i++) {
|
|
||||||
var val = releases[i][field];
|
|
||||||
if (val !== null && val !== undefined && val !== '') {
|
|
||||||
seen[val] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Object.keys(seen).sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
var passes = 0;
|
|
||||||
var failures = 0;
|
|
||||||
var warns = 0;
|
|
||||||
|
|
||||||
var cachePath = resolveLayout(CACHE_DIR);
|
|
||||||
var livePath = resolveLayout(LIVE_DIR);
|
|
||||||
|
|
||||||
if (!cachePath) {
|
|
||||||
console.error('No _cache directory found');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
if (!livePath) {
|
|
||||||
console.error('No LIVE_cache directory found');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Using cache: ' + cachePath + ' vs ' + livePath);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// Get all packages that exist in both caches
|
|
||||||
var cacheFiles = Fs.readdirSync(cachePath)
|
|
||||||
.filter(function (f) { return f.endsWith('.json'); })
|
|
||||||
.map(function (f) { return f.replace('.json', ''); });
|
|
||||||
var liveFiles = Fs.readdirSync(livePath)
|
|
||||||
.filter(function (f) { return f.endsWith('.json'); })
|
|
||||||
.map(function (f) { return f.replace('.json', ''); });
|
|
||||||
|
|
||||||
var cacheSet = {};
|
|
||||||
var liveSet = {};
|
|
||||||
cacheFiles.forEach(function (f) { cacheSet[f] = true; });
|
|
||||||
liveFiles.forEach(function (f) { liveSet[f] = true; });
|
|
||||||
|
|
||||||
var common = cacheFiles.filter(function (f) { return liveSet[f]; });
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 1: Global vocabulary — no new OS/arch/libc tags
|
|
||||||
// ================================================================
|
|
||||||
console.log('=== Test 1: No New Tags (OS/Arch/Libc) ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
var allLiveOs = {};
|
|
||||||
var allLiveArch = {};
|
|
||||||
var allLiveLibc = {};
|
|
||||||
var allCacheOs = {};
|
|
||||||
var allCacheArch = {};
|
|
||||||
var allCacheLibc = {};
|
|
||||||
|
|
||||||
for (var ci = 0; ci < common.length; ci++) {
|
|
||||||
var pkg = common[ci];
|
|
||||||
var liveData = loadReleases(livePath, pkg);
|
|
||||||
var cacheData = loadReleases(cachePath, pkg);
|
|
||||||
if (!liveData || !cacheData) { continue; }
|
|
||||||
|
|
||||||
uniqueValues(liveData.releases, 'os').forEach(function (v) { allLiveOs[v] = true; });
|
|
||||||
uniqueValues(liveData.releases, 'arch').forEach(function (v) { allLiveArch[v] = true; });
|
|
||||||
uniqueValues(liveData.releases, 'libc').forEach(function (v) { allLiveLibc[v] = true; });
|
|
||||||
uniqueValues(cacheData.releases, 'os').forEach(function (v) { allCacheOs[v] = true; });
|
|
||||||
uniqueValues(cacheData.releases, 'arch').forEach(function (v) { allCacheArch[v] = true; });
|
|
||||||
uniqueValues(cacheData.releases, 'libc').forEach(function (v) { allCacheLibc[v] = true; });
|
|
||||||
}
|
|
||||||
|
|
||||||
var newOs = Object.keys(allCacheOs).filter(function (v) { return !allLiveOs[v]; }).sort();
|
|
||||||
var newArch = Object.keys(allCacheArch).filter(function (v) { return !allLiveArch[v]; }).sort();
|
|
||||||
var newLibc = Object.keys(allCacheLibc).filter(function (v) { return !allLiveLibc[v]; }).sort();
|
|
||||||
|
|
||||||
if (newOs.length > 0) {
|
|
||||||
console.log(' FAIL new OS values in _cache not in LIVE_cache: ' + JSON.stringify(newOs));
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
console.log(' PASS no new OS values');
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newArch.length > 0) {
|
|
||||||
console.log(' FAIL new arch values in _cache not in LIVE_cache: ' + JSON.stringify(newArch));
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
console.log(' PASS no new arch values');
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newLibc.length > 0) {
|
|
||||||
console.log(' FAIL new libc values in _cache not in LIVE_cache: ' + JSON.stringify(newLibc));
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
console.log(' PASS no new libc values');
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show what LIVE has that _cache doesn't (informational)
|
|
||||||
var missingOs = Object.keys(allLiveOs).filter(function (v) { return !allCacheOs[v]; }).sort();
|
|
||||||
var missingArch = Object.keys(allLiveArch).filter(function (v) { return !allCacheArch[v]; }).sort();
|
|
||||||
if (missingOs.length > 0) {
|
|
||||||
console.log(' INFO OS in LIVE but not _cache: ' + JSON.stringify(missingOs));
|
|
||||||
}
|
|
||||||
if (missingArch.length > 0) {
|
|
||||||
console.log(' INFO arch in LIVE but not _cache: ' + JSON.stringify(missingArch));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 2: Per-package release count — _cache should have >= LIVE
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 2: Release Count (per package) ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// LIVE_cache includes junk entries (.pem, .sig, .sha256, .deb, .rpm, etc.)
|
|
||||||
// that Go correctly filters out. Only count installable entries.
|
|
||||||
var junkExts = /\.(pem|sig|asc|sha256|sha512|sha256sum|sha512sum|deb|rpm|apk|sbom|json|txt|sum|md5|cosign-bundle|intoto-jsonl)$/i;
|
|
||||||
|
|
||||||
function countInstallable(releases) {
|
|
||||||
var n = 0;
|
|
||||||
for (var i = 0; i < releases.length; i++) {
|
|
||||||
if (!junkExts.test(releases[i].name || '')) {
|
|
||||||
n++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
var countIssues = [];
|
|
||||||
for (var pi = 0; pi < common.length; pi++) {
|
|
||||||
var ppkg = common[pi];
|
|
||||||
var pLive = loadReleases(livePath, ppkg);
|
|
||||||
var pCache = loadReleases(cachePath, ppkg);
|
|
||||||
if (!pLive || !pCache) { continue; }
|
|
||||||
|
|
||||||
var liveCount = countInstallable(pLive.releases);
|
|
||||||
var cacheCount = pCache.releases.length;
|
|
||||||
|
|
||||||
// _cache should have at least as many installable releases as LIVE
|
|
||||||
var ratio = liveCount > 0 ? cacheCount / liveCount : 1;
|
|
||||||
if (ratio < 0.5 && liveCount > 10) {
|
|
||||||
countIssues.push({ pkg: ppkg, live: liveCount, cache: cacheCount, ratio: ratio });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (countIssues.length === 0) {
|
|
||||||
console.log(' PASS all packages have adequate release counts');
|
|
||||||
passes++;
|
|
||||||
} else {
|
|
||||||
for (var ii = 0; ii < countIssues.length; ii++) {
|
|
||||||
var issue = countIssues[ii];
|
|
||||||
console.log(' FAIL ' + issue.pkg + ': _cache=' + issue.cache + ' LIVE=' + issue.live + ' (ratio=' + issue.ratio.toFixed(2) + ')');
|
|
||||||
failures++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 3: Per-package OS coverage — _cache should have same core OSes
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 3: OS Coverage (per package) ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
var coreOses = ['darwin', 'linux', 'windows'];
|
|
||||||
var osIssues = [];
|
|
||||||
for (var oi = 0; oi < common.length; oi++) {
|
|
||||||
var opkg = common[oi];
|
|
||||||
var oLive = loadReleases(livePath, opkg);
|
|
||||||
var oCache = loadReleases(cachePath, opkg);
|
|
||||||
if (!oLive || !oCache) { continue; }
|
|
||||||
|
|
||||||
var liveOses = uniqueValues(oLive.releases, 'os');
|
|
||||||
var cacheOses = uniqueValues(oCache.releases, 'os');
|
|
||||||
|
|
||||||
// For each core OS in LIVE, it should also be in _cache
|
|
||||||
for (var coi = 0; coi < coreOses.length; coi++) {
|
|
||||||
var os = coreOses[coi];
|
|
||||||
if (liveOses.indexOf(os) >= 0 && cacheOses.indexOf(os) < 0) {
|
|
||||||
osIssues.push({ pkg: opkg, os: os });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (osIssues.length === 0) {
|
|
||||||
console.log(' PASS all packages have matching core OS coverage');
|
|
||||||
passes++;
|
|
||||||
} else {
|
|
||||||
for (var oii = 0; oii < osIssues.length; oii++) {
|
|
||||||
var oisue = osIssues[oii];
|
|
||||||
console.log(' FAIL ' + oisue.pkg + ': missing os=' + oisue.os + ' (present in LIVE)');
|
|
||||||
failures++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 4: Per-package new tags — flag packages introducing new values
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 4: Per-Package New Tags ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
var tagIssues = [];
|
|
||||||
var tagSkipped = 0;
|
|
||||||
for (var ti = 0; ti < common.length; ti++) {
|
|
||||||
var tpkg = common[ti];
|
|
||||||
var tLive = loadReleases(livePath, tpkg);
|
|
||||||
var tCache = loadReleases(cachePath, tpkg);
|
|
||||||
if (!tLive || !tCache) { continue; }
|
|
||||||
|
|
||||||
var tLiveOs = {};
|
|
||||||
var tLiveArch = {};
|
|
||||||
uniqueValues(tLive.releases, 'os').forEach(function (v) { tLiveOs[v] = true; });
|
|
||||||
uniqueValues(tLive.releases, 'arch').forEach(function (v) { tLiveArch[v] = true; });
|
|
||||||
|
|
||||||
// Skip packages where LIVE has no classified entries (all empty os/arch).
|
|
||||||
// These are github_releases packages where classification happens at query
|
|
||||||
// time. Our _cache filling in values is an improvement, not a regression.
|
|
||||||
var liveOsKeys = Object.keys(tLiveOs);
|
|
||||||
var liveArchKeys = Object.keys(tLiveArch);
|
|
||||||
if (liveOsKeys.length === 0 && liveArchKeys.length === 0) {
|
|
||||||
tagSkipped++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tCacheOs = uniqueValues(tCache.releases, 'os');
|
|
||||||
var tCacheArch = uniqueValues(tCache.releases, 'arch');
|
|
||||||
|
|
||||||
var tNewOs = tCacheOs.filter(function (v) { return !tLiveOs[v]; });
|
|
||||||
var tNewArch = tCacheArch.filter(function (v) { return !tLiveArch[v]; });
|
|
||||||
|
|
||||||
if (tNewOs.length > 0 || tNewArch.length > 0) {
|
|
||||||
tagIssues.push({
|
|
||||||
pkg: tpkg,
|
|
||||||
newOs: tNewOs,
|
|
||||||
newArch: tNewArch,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tagSkipped > 0) {
|
|
||||||
console.log(' INFO skipped ' + tagSkipped + ' packages with no LIVE classification (unclassified github_releases)');
|
|
||||||
}
|
|
||||||
if (tagIssues.length === 0) {
|
|
||||||
console.log(' PASS no pre-classified packages introduce new tags');
|
|
||||||
passes++;
|
|
||||||
} else {
|
|
||||||
for (var tii = 0; tii < tagIssues.length; tii++) {
|
|
||||||
var tissue = tagIssues[tii];
|
|
||||||
var parts = [];
|
|
||||||
if (tissue.newOs.length > 0) {
|
|
||||||
parts.push('os: ' + JSON.stringify(tissue.newOs));
|
|
||||||
}
|
|
||||||
if (tissue.newArch.length > 0) {
|
|
||||||
parts.push('arch: ' + JSON.stringify(tissue.newArch));
|
|
||||||
}
|
|
||||||
console.log(' WARN ' + tissue.pkg + ': new tags: ' + parts.join(', '));
|
|
||||||
warns++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 5: Latest stable version — _cache should be >= LIVE
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 5: Latest Stable Version ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
var stableCheckPkgs = ['bat', 'go', 'node', 'rg', 'caddy', 'jq', 'hugo', 'terraform'];
|
|
||||||
for (var si = 0; si < stableCheckPkgs.length; si++) {
|
|
||||||
var spkg = stableCheckPkgs[si];
|
|
||||||
var sLive = loadReleases(livePath, spkg);
|
|
||||||
var sCache = loadReleases(cachePath, spkg);
|
|
||||||
if (!sLive || !sCache) {
|
|
||||||
console.log(' SKIP ' + spkg + ': missing data');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find first stable release in each
|
|
||||||
var liveStable = sLive.releases.find(function (r) { return r.channel === 'stable'; });
|
|
||||||
var cacheStable = sCache.releases.find(function (r) { return r.channel === 'stable'; });
|
|
||||||
|
|
||||||
if (!liveStable || !cacheStable) {
|
|
||||||
console.log(' SKIP ' + spkg + ': no stable release found');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var lv = (liveStable.version || '').replace(/^v/, '');
|
|
||||||
var cv = (cacheStable.version || '').replace(/^v/, '');
|
|
||||||
|
|
||||||
if (lv === cv) {
|
|
||||||
console.log(' PASS ' + spkg + ': ' + cv);
|
|
||||||
passes++;
|
|
||||||
} else {
|
|
||||||
// Just warn — versions may differ due to cache age
|
|
||||||
console.log(' WARN ' + spkg + ': LIVE=' + lv + ' _cache=' + cv);
|
|
||||||
warns++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 6: Download URLs — all entries should have valid URLs
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 6: Download URL Validity ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
var urlIssues = [];
|
|
||||||
for (var ui = 0; ui < common.length; ui++) {
|
|
||||||
var upkg = common[ui];
|
|
||||||
var uCache = loadReleases(cachePath, upkg);
|
|
||||||
if (!uCache) { continue; }
|
|
||||||
|
|
||||||
var emptyUrls = 0;
|
|
||||||
var badUrls = 0;
|
|
||||||
for (var uri = 0; uri < uCache.releases.length; uri++) {
|
|
||||||
var rel = uCache.releases[uri];
|
|
||||||
var url = rel.download || '';
|
|
||||||
if (url === '') {
|
|
||||||
emptyUrls++;
|
|
||||||
} else if (!/^https?:\/\//.test(url)) {
|
|
||||||
badUrls++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (emptyUrls > 0 || badUrls > 0) {
|
|
||||||
urlIssues.push({ pkg: upkg, empty: emptyUrls, bad: badUrls });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (urlIssues.length === 0) {
|
|
||||||
console.log(' PASS all packages have valid download URLs');
|
|
||||||
passes++;
|
|
||||||
} else {
|
|
||||||
for (var uii = 0; uii < urlIssues.length; uii++) {
|
|
||||||
var uissue = urlIssues[uii];
|
|
||||||
var uparts = [];
|
|
||||||
if (uissue.empty > 0) { uparts.push(uissue.empty + ' empty'); }
|
|
||||||
if (uissue.bad > 0) { uparts.push(uissue.bad + ' malformed'); }
|
|
||||||
console.log(' FAIL ' + uissue.pkg + ': ' + uparts.join(', '));
|
|
||||||
failures++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 7: Required fields — all entries should have version + name
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 7: Required Fields ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
var fieldIssues = [];
|
|
||||||
for (var fi = 0; fi < common.length; fi++) {
|
|
||||||
var fpkg = common[fi];
|
|
||||||
var fCache = loadReleases(cachePath, fpkg);
|
|
||||||
if (!fCache) { continue; }
|
|
||||||
|
|
||||||
var noVersion = 0;
|
|
||||||
var noName = 0;
|
|
||||||
for (var fri = 0; fri < fCache.releases.length; fri++) {
|
|
||||||
var frel = fCache.releases[fri];
|
|
||||||
if (!frel.version) { noVersion++; }
|
|
||||||
if (!frel.name && !frel.download) { noName++; }
|
|
||||||
}
|
|
||||||
if (noVersion > 0 || noName > 0) {
|
|
||||||
fieldIssues.push({ pkg: fpkg, noVersion: noVersion, noName: noName });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fieldIssues.length === 0) {
|
|
||||||
console.log(' PASS all packages have version and name/download');
|
|
||||||
passes++;
|
|
||||||
} else {
|
|
||||||
for (var fii = 0; fii < fieldIssues.length; fii++) {
|
|
||||||
var fissue = fieldIssues[fii];
|
|
||||||
var fparts = [];
|
|
||||||
if (fissue.noVersion > 0) { fparts.push(fissue.noVersion + ' missing version'); }
|
|
||||||
if (fissue.noName > 0) { fparts.push(fissue.noName + ' missing name+download'); }
|
|
||||||
console.log(' FAIL ' + fissue.pkg + ': ' + fparts.join(', '));
|
|
||||||
failures++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Summary
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Results: ' + passes + ' passed, ' + failures + ' failed, ' + warns + ' warnings ===');
|
|
||||||
if (failures > 0) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(function (err) {
|
|
||||||
console.error(err.stack);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,577 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
// Comprehensive live-vs-local comparison test.
|
|
||||||
// Fetches from a remote API (default: webinstall.dev) and compares against
|
|
||||||
// local cache-only output to catch regressions.
|
|
||||||
//
|
|
||||||
// Usage:
|
|
||||||
// node _webi/test-live-compare.js # compare against prod
|
|
||||||
// node _webi/test-live-compare.js --refresh # refresh golden data
|
|
||||||
// node _webi/test-live-compare.js --base-url=https://beta.webi.sh
|
|
||||||
// node _webi/test-live-compare.js --all # all cached pkgs
|
|
||||||
// node _webi/test-live-compare.js --all --tsv # TSV output
|
|
||||||
// node _webi/test-live-compare.js --base-url=https://webi.sh \
|
|
||||||
// --cand-url=https://beta.webi.sh # remote-vs-remote
|
|
||||||
// # (Test 4 only;
|
|
||||||
// # no local cache
|
|
||||||
// # needed)
|
|
||||||
|
|
||||||
let Fs = require('node:fs/promises');
|
|
||||||
let Os = require('node:os');
|
|
||||||
let Path = require('node:path');
|
|
||||||
let Https = require('node:https');
|
|
||||||
|
|
||||||
let Releases = require('./transform-releases.js');
|
|
||||||
let InstallerServer = require('./serve-installer.js');
|
|
||||||
let Builds = require('./builds.js');
|
|
||||||
|
|
||||||
let TESTDATA_DIR = Path.join(__dirname, 'testdata');
|
|
||||||
let CACHE_DIR = Path.join(Os.homedir(), '.cache/webi/legacy');
|
|
||||||
|
|
||||||
let REFRESH = process.argv.includes('--refresh');
|
|
||||||
let ALL_PKGS = process.argv.includes('--all');
|
|
||||||
let TSV = process.argv.includes('--tsv');
|
|
||||||
|
|
||||||
let BASE_URL = 'https://webinstall.dev';
|
|
||||||
let CAND_URL = '';
|
|
||||||
for (let arg of process.argv) {
|
|
||||||
if (arg.startsWith('--base-url=')) {
|
|
||||||
BASE_URL = arg.slice('--base-url='.length).replace(/\/+$/, '');
|
|
||||||
} else if (arg.startsWith('--cand-url=')) {
|
|
||||||
CAND_URL = arg.slice('--cand-url='.length).replace(/\/+$/, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Packages to test — mix of Go-built, Rust-built, and C/C++ projects
|
|
||||||
let TEST_PKGS = ['bat', 'go', 'node', 'rg', 'jq', 'caddy'];
|
|
||||||
|
|
||||||
async function listCachedPkgs() {
|
|
||||||
let entries;
|
|
||||||
try {
|
|
||||||
entries = await Fs.readdir(CACHE_DIR);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`No cache directory: ${CACHE_DIR}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
let pkgs = entries
|
|
||||||
.filter(function (n) { return n.endsWith('.json'); })
|
|
||||||
.map(function (n) { return n.slice(0, -5); })
|
|
||||||
.sort();
|
|
||||||
return pkgs;
|
|
||||||
}
|
|
||||||
|
|
||||||
// OS/arch combos for filtered release API tests
|
|
||||||
let RELEASE_API_CASES = [
|
|
||||||
{ os: 'macos', arch: 'amd64' },
|
|
||||||
{ os: 'macos', arch: 'arm64' },
|
|
||||||
{ os: 'linux', arch: 'amd64' },
|
|
||||||
{ os: 'windows', arch: 'amd64' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Older / channel-specific version specs that map to webi <pkg>@<spec>
|
|
||||||
// invocations. Each case exercises a code path that the unfiltered
|
|
||||||
// "@stable" sweep above would never hit:
|
|
||||||
// - LTS filter (lts=true)
|
|
||||||
// - Channel filter (channel=beta)
|
|
||||||
// - Major-series prefix (ver=20 → /^20\b/)
|
|
||||||
// - Older minor (ver=0.18 → /^0.18\b/)
|
|
||||||
// - Older major (ver=1.21 → /^1.21\b/) for projects with deep history
|
|
||||||
let VERSION_SPEC_CASES = [
|
|
||||||
{ pkg: 'node', spec: 'lts', ver: '', channel: '', lts: true, os: 'linux', arch: 'amd64' },
|
|
||||||
{ pkg: 'node', spec: '20', ver: '20', channel: '', lts: false, os: 'linux', arch: 'amd64' },
|
|
||||||
{ pkg: 'node', spec: 'beta', ver: '', channel: 'beta', lts: false, os: 'linux', arch: 'amd64' },
|
|
||||||
{ pkg: 'go', spec: '1.22', ver: '1.22', channel: '', lts: false, os: 'linux', arch: 'amd64' },
|
|
||||||
{ pkg: 'go', spec: '1.21', ver: '1.21', channel: '', lts: false, os: 'macos', arch: 'arm64' },
|
|
||||||
{ pkg: 'bat', spec: '0.20', ver: '0.20', channel: '', lts: false, os: 'linux', arch: 'amd64' },
|
|
||||||
{ pkg: 'bat', spec: '0.18', ver: '0.18', channel: '', lts: false, os: 'linux', arch: 'amd64' },
|
|
||||||
{ pkg: 'rg', spec: '13', ver: '13', channel: '', lts: false, os: 'linux', arch: 'amd64' },
|
|
||||||
{ pkg: 'caddy', spec: '2.7', ver: '2.7', channel: '', lts: false, os: 'linux', arch: 'amd64' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// UA strings for installer resolution tests
|
|
||||||
let INSTALLER_CASES = [
|
|
||||||
{ label: 'macOS arm64', ua: 'aarch64/unknown Darwin/24.2.0 libc' },
|
|
||||||
{ label: 'macOS amd64', ua: 'x86_64/unknown Darwin/23.0.0 libc' },
|
|
||||||
{ label: 'Linux musl', ua: 'x86_64/unknown Linux/5.15.0 musl' },
|
|
||||||
{ label: 'Windows amd64', ua: 'x86_64/unknown Windows/10.0.19041 msvc' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Known differences between Go cache and production (not regressions)
|
|
||||||
// Extensions that the Go cache correctly excludes (non-installable formats)
|
|
||||||
// OR that the Go cache includes but shouldn't (man pages, etc.)
|
|
||||||
let KNOWN_EXT_DIFFS = new Set([
|
|
||||||
'deb', 'rpm', 'sha256', 'sig', 'pem', 'sbom', 'txt',
|
|
||||||
'1', '2', '3', '4', '5', '6', '7', '8', // man page extensions
|
|
||||||
]);
|
|
||||||
|
|
||||||
function httpsGet(url) {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
Https.get(url, function (res) {
|
|
||||||
let data = '';
|
|
||||||
res.on('data', function (chunk) {
|
|
||||||
data += chunk;
|
|
||||||
});
|
|
||||||
res.on('end', function () {
|
|
||||||
if (res.statusCode !== 200) {
|
|
||||||
reject(new Error(`HTTP ${res.statusCode}: ${url}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(data);
|
|
||||||
});
|
|
||||||
}).on('error', reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchLiveReleases(pkg, os, arch, limit) {
|
|
||||||
let url = `${BASE_URL}/api/releases/${pkg}@stable.json?limit=${limit || 100}`;
|
|
||||||
if (os) {
|
|
||||||
url += `&os=${os}`;
|
|
||||||
}
|
|
||||||
if (arch) {
|
|
||||||
url += `&arch=${arch}`;
|
|
||||||
}
|
|
||||||
let json = await httpsGet(url);
|
|
||||||
return JSON.parse(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchAtSpec(baseUrl, pkg, spec, os, arch) {
|
|
||||||
let url = `${baseUrl}/api/releases/${pkg}@${spec}.json?limit=1`;
|
|
||||||
if (os) {
|
|
||||||
url += `&os=${os}`;
|
|
||||||
}
|
|
||||||
if (arch) {
|
|
||||||
url += `&arch=${arch}`;
|
|
||||||
}
|
|
||||||
let json = await httpsGet(url);
|
|
||||||
return JSON.parse(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchLiveInstaller(pkg, ua) {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
let url = `${BASE_URL}/${pkg}@stable.sh`;
|
|
||||||
let opts = {
|
|
||||||
headers: { 'User-Agent': ua },
|
|
||||||
};
|
|
||||||
Https.get(url, opts, function (res) {
|
|
||||||
// Follow redirects
|
|
||||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
||||||
let redir = res.headers.location;
|
|
||||||
if (redir.startsWith('/')) {
|
|
||||||
redir = BASE_URL + redir;
|
|
||||||
}
|
|
||||||
Https.get(redir, opts, function (res2) {
|
|
||||||
let data = '';
|
|
||||||
res2.on('data', function (chunk) {
|
|
||||||
data += chunk;
|
|
||||||
});
|
|
||||||
res2.on('end', function () {
|
|
||||||
resolve(data);
|
|
||||||
});
|
|
||||||
}).on('error', reject);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let data = '';
|
|
||||||
res.on('data', function (chunk) {
|
|
||||||
data += chunk;
|
|
||||||
});
|
|
||||||
res.on('end', function () {
|
|
||||||
resolve(data);
|
|
||||||
});
|
|
||||||
}).on('error', reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseInstallerVars(scriptText) {
|
|
||||||
let vars = {};
|
|
||||||
// Match both `export WEBI_FOO='...'` and `WEBI_FOO='...'`
|
|
||||||
let names = ['WEBI_PKG_URL', 'WEBI_VERSION', 'WEBI_EXT', 'WEBI_OS', 'WEBI_ARCH', 'PKG_NAME'];
|
|
||||||
for (let name of names) {
|
|
||||||
let re = new RegExp("^(?:export\\s+)?" + name + "='([^']*)'", 'm');
|
|
||||||
let m = scriptText.match(re);
|
|
||||||
if (m) {
|
|
||||||
vars[name] = m[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return vars;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveGolden(name, data) {
|
|
||||||
await Fs.mkdir(TESTDATA_DIR, { recursive: true });
|
|
||||||
let file = Path.join(TESTDATA_DIR, name);
|
|
||||||
await Fs.writeFile(file, JSON.stringify(data), 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadGolden(name) {
|
|
||||||
let file = Path.join(TESTDATA_DIR, name);
|
|
||||||
try {
|
|
||||||
let json = await Fs.readFile(file, 'utf8');
|
|
||||||
return JSON.parse(json);
|
|
||||||
} catch (e) {
|
|
||||||
if (e.code === 'ENOENT') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
let passes = 0;
|
|
||||||
let failures = 0;
|
|
||||||
let skips = 0;
|
|
||||||
let knowns = 0;
|
|
||||||
|
|
||||||
console.log('Initializing build cache...');
|
|
||||||
await Builds.init();
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 1: Release API — unfiltered
|
|
||||||
// ================================================================
|
|
||||||
console.log('=== Test 1: Unfiltered /api/releases/{pkg}.json ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
for (let pkg of TEST_PKGS) {
|
|
||||||
let goldenName = `live_${pkg}.json`;
|
|
||||||
let liveReleases;
|
|
||||||
|
|
||||||
if (REFRESH) {
|
|
||||||
try {
|
|
||||||
liveReleases = await fetchLiveReleases(pkg);
|
|
||||||
await saveGolden(goldenName, liveReleases);
|
|
||||||
console.log(` refreshed ${goldenName}`);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(` SKIP ${pkg}: fetch error: ${e.message}`);
|
|
||||||
skips++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
liveReleases = await loadGolden(goldenName);
|
|
||||||
if (!liveReleases) {
|
|
||||||
console.log(` SKIP ${pkg}: no golden data (run with --refresh)`);
|
|
||||||
skips++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let localResult = await Releases.getReleases({
|
|
||||||
pkg: pkg,
|
|
||||||
ver: '',
|
|
||||||
os: '',
|
|
||||||
arch: '',
|
|
||||||
libc: '',
|
|
||||||
lts: false,
|
|
||||||
channel: '',
|
|
||||||
formats: [],
|
|
||||||
limit: 100,
|
|
||||||
});
|
|
||||||
let localReleases = localResult.releases;
|
|
||||||
|
|
||||||
// Compare OS vocabulary — check that local has the core OSes that live has
|
|
||||||
let liveOses = [...new Set(liveReleases.map(function (r) { return r.os; }))].sort();
|
|
||||||
let localOses = [...new Set(localReleases.map(function (r) { return r.os; }))].sort();
|
|
||||||
let coreOses = ['linux', 'macos', 'windows'];
|
|
||||||
let liveCore = liveOses.filter(function (o) { return coreOses.includes(o); }).sort();
|
|
||||||
let localCore = localOses.filter(function (o) { return coreOses.includes(o); }).sort();
|
|
||||||
// Local should have at least the core OSes that live has
|
|
||||||
let missingCore = liveCore.filter(function (o) { return !localCore.includes(o); });
|
|
||||||
if (missingCore.length > 0) {
|
|
||||||
console.log(` FAIL ${pkg} OS: missing core OSes: ${JSON.stringify(missingCore)}`);
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
console.log(` PASS ${pkg} OS: ${JSON.stringify(localCore)}`);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare ext vocabulary (excluding known non-installable formats)
|
|
||||||
let liveExts = [...new Set(liveReleases.map(function (r) { return r.ext; }))].sort();
|
|
||||||
let localExts = [...new Set(localReleases.map(function (r) { return r.ext; }))].sort();
|
|
||||||
let liveExtsFiltered = liveExts.filter(function (e) { return !KNOWN_EXT_DIFFS.has(e); });
|
|
||||||
let localExtsFiltered = localExts.filter(function (e) { return !KNOWN_EXT_DIFFS.has(e); });
|
|
||||||
let extMatch = true;
|
|
||||||
for (let ext of localExtsFiltered) {
|
|
||||||
if (!liveExtsFiltered.includes(ext)) {
|
|
||||||
// Local has a real ext that live doesn't — may be due to limit or sampling
|
|
||||||
// Only fail if it's clearly wrong (not a standard installable format)
|
|
||||||
let installable = ['tar.gz', 'tar.xz', 'tar.bz2', 'zip', 'exe', 'dmg', 'pkg', 'msi', '7z', 'xz'];
|
|
||||||
if (!installable.includes(ext)) {
|
|
||||||
console.log(` FAIL ${pkg} ext: local has unexpected '${ext}'`);
|
|
||||||
failures++;
|
|
||||||
extMatch = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (extMatch) {
|
|
||||||
console.log(` PASS ${pkg} ext: ${JSON.stringify(localExtsFiltered)}`);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Version format — no 'v' prefix
|
|
||||||
let hasVPrefix = localReleases.some(function (r) {
|
|
||||||
return r.version && r.version.startsWith('v');
|
|
||||||
});
|
|
||||||
if (hasVPrefix) {
|
|
||||||
console.log(` FAIL ${pkg}: versions have 'v' prefix`);
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
console.log(` PASS ${pkg}: no 'v' prefix`);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 2: Release API — filtered by OS/arch
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 2: Filtered /api/releases/{pkg}@stable.json?os=...&arch=... ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
for (let pkg of TEST_PKGS) {
|
|
||||||
for (let tc of RELEASE_API_CASES) {
|
|
||||||
let goldenName = `live_${pkg}_os_${tc.os}_arch_${tc.arch}.json`;
|
|
||||||
let liveReleases;
|
|
||||||
|
|
||||||
if (REFRESH) {
|
|
||||||
try {
|
|
||||||
liveReleases = await fetchLiveReleases(pkg, tc.os, tc.arch, 1);
|
|
||||||
await saveGolden(goldenName, liveReleases);
|
|
||||||
} catch (e) {
|
|
||||||
skips++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
liveReleases = await loadGolden(goldenName);
|
|
||||||
if (!liveReleases) {
|
|
||||||
skips++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let liveFirst = liveReleases[0];
|
|
||||||
if (!liveFirst || liveFirst.channel === 'error') {
|
|
||||||
skips++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let localResult = await Releases.getReleases({
|
|
||||||
pkg: pkg,
|
|
||||||
ver: '',
|
|
||||||
os: tc.os,
|
|
||||||
arch: tc.arch,
|
|
||||||
libc: '',
|
|
||||||
lts: false,
|
|
||||||
channel: 'stable',
|
|
||||||
formats: [],
|
|
||||||
limit: 1,
|
|
||||||
});
|
|
||||||
let localFirst = localResult.releases[0];
|
|
||||||
|
|
||||||
if (!localFirst || localFirst.channel === 'error') {
|
|
||||||
console.log(` FAIL ${pkg} ${tc.os}/${tc.arch}: local returned error/empty`);
|
|
||||||
failures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let diffs = [];
|
|
||||||
// Compare os, arch, ext (skip version/download since cache age may differ)
|
|
||||||
if (liveFirst.os !== localFirst.os) {
|
|
||||||
diffs.push(`os: live=${liveFirst.os} local=${localFirst.os}`);
|
|
||||||
}
|
|
||||||
if (liveFirst.arch !== localFirst.arch) {
|
|
||||||
diffs.push(`arch: live=${liveFirst.arch} local=${localFirst.arch}`);
|
|
||||||
}
|
|
||||||
if (liveFirst.ext !== localFirst.ext) {
|
|
||||||
if (KNOWN_EXT_DIFFS.has(liveFirst.ext)) {
|
|
||||||
// Live returns a non-installable format (deb, pem, etc.) — known
|
|
||||||
console.log(` KNOWN ${pkg} ${tc.os}/${tc.arch}: live ext '${liveFirst.ext}' excluded by Go cache, local='${localFirst.ext}'`);
|
|
||||||
knowns++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
diffs.push(`ext: live=${liveFirst.ext} local=${localFirst.ext}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (diffs.length > 0) {
|
|
||||||
console.log(` FAIL ${pkg} ${tc.os}/${tc.arch}: ${diffs.join(', ')}`);
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
console.log(` PASS ${pkg} ${tc.os}/${tc.arch}: v${localFirst.version} .${localFirst.ext}`);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 3: Installer resolution — compare rendered script vars
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log('=== Test 3: Installer script variables (local serveInstaller vs live) ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
for (let pkg of ['bat', 'go', 'rg']) {
|
|
||||||
for (let tc of INSTALLER_CASES) {
|
|
||||||
// Get local result
|
|
||||||
let localVars;
|
|
||||||
try {
|
|
||||||
let script = await InstallerServer.serveInstaller(
|
|
||||||
'https://webi.sh',
|
|
||||||
tc.ua,
|
|
||||||
pkg,
|
|
||||||
'stable',
|
|
||||||
'sh',
|
|
||||||
['tar', 'exe', 'zip', 'xz', 'dmg'],
|
|
||||||
'',
|
|
||||||
);
|
|
||||||
localVars = parseInstallerVars(script);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(` ERROR ${pkg} ${tc.label}: ${e.message}`);
|
|
||||||
failures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!localVars.WEBI_PKG_URL || localVars.WEBI_PKG_URL === '') {
|
|
||||||
// Check if this is a known issue
|
|
||||||
if (localVars.WEBI_EXT === 'err') {
|
|
||||||
console.log(` KNOWN ${pkg} ${tc.label}: no match (WATERFALL gap)`);
|
|
||||||
knowns++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
console.log(` FAIL ${pkg} ${tc.label}: empty WEBI_PKG_URL`);
|
|
||||||
failures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the URL looks like a real download
|
|
||||||
let url = localVars.WEBI_PKG_URL;
|
|
||||||
let hasRealDomain = url.includes('github.com') ||
|
|
||||||
url.includes('dl.google.com') ||
|
|
||||||
url.includes('nodejs.org') ||
|
|
||||||
url.includes('jqlang');
|
|
||||||
if (!hasRealDomain) {
|
|
||||||
console.log(` FAIL ${pkg} ${tc.label}: suspicious URL: ${url}`);
|
|
||||||
failures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify version is set and has no 'v' prefix
|
|
||||||
if (!localVars.WEBI_VERSION || localVars.WEBI_VERSION === '0.0.0') {
|
|
||||||
console.log(` FAIL ${pkg} ${tc.label}: bad version: ${localVars.WEBI_VERSION}`);
|
|
||||||
failures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify ext is a real installable format
|
|
||||||
let ext = localVars.WEBI_EXT;
|
|
||||||
let goodExts = ['tar.gz', 'tar.xz', 'zip', 'exe', 'dmg', 'pkg', 'msi', '7z'];
|
|
||||||
if (!goodExts.includes(ext)) {
|
|
||||||
console.log(` FAIL ${pkg} ${tc.label}: bad ext: ${ext}`);
|
|
||||||
failures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` PASS ${pkg} ${tc.label}: v${localVars.WEBI_VERSION} .${ext} ${url.split('/').pop()}`);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Test 4: @version path-form parity (webi <pkg>@<spec>)
|
|
||||||
// Exercises lts/channel/version filters that the unfiltered sweep
|
|
||||||
// doesn't touch. Two modes:
|
|
||||||
// - --cand-url=<url> set: remote (BASE_URL) vs remote (CAND_URL).
|
|
||||||
// No local cache needed.
|
|
||||||
// - --cand-url unset: remote (BASE_URL) vs in-process Releases.
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
if (CAND_URL) {
|
|
||||||
console.log(`=== Test 4: @version filter parity (${BASE_URL} vs ${CAND_URL}) ===`);
|
|
||||||
} else {
|
|
||||||
console.log('=== Test 4: @version filter parity (remote vs local resolver) ===');
|
|
||||||
}
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
for (let tc of VERSION_SPEC_CASES) {
|
|
||||||
let label = `${tc.pkg}@${tc.spec} ${tc.os}/${tc.arch}`;
|
|
||||||
|
|
||||||
let baseFirst;
|
|
||||||
try {
|
|
||||||
let baseRel = await fetchAtSpec(BASE_URL, tc.pkg, tc.spec, tc.os, tc.arch);
|
|
||||||
baseFirst = baseRel[0];
|
|
||||||
} catch (e) {
|
|
||||||
console.log(` SKIP ${label}: base fetch error: ${e.message}`);
|
|
||||||
skips++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!baseFirst || baseFirst.channel === 'error') {
|
|
||||||
console.log(` SKIP ${label}: base returned error/empty`);
|
|
||||||
skips++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let candFirst;
|
|
||||||
if (CAND_URL) {
|
|
||||||
try {
|
|
||||||
let candRel = await fetchAtSpec(CAND_URL, tc.pkg, tc.spec, tc.os, tc.arch);
|
|
||||||
candFirst = candRel[0];
|
|
||||||
} catch (e) {
|
|
||||||
console.log(` FAIL ${label}: cand fetch error: ${e.message}`);
|
|
||||||
failures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let localResult = await Releases.getReleases({
|
|
||||||
pkg: tc.pkg,
|
|
||||||
ver: tc.ver,
|
|
||||||
os: tc.os,
|
|
||||||
arch: tc.arch,
|
|
||||||
libc: '',
|
|
||||||
lts: tc.lts,
|
|
||||||
channel: tc.channel,
|
|
||||||
formats: [],
|
|
||||||
limit: 1,
|
|
||||||
});
|
|
||||||
candFirst = localResult.releases && localResult.releases[0];
|
|
||||||
}
|
|
||||||
if (!candFirst || candFirst.channel === 'error') {
|
|
||||||
console.log(` FAIL ${label}: cand returned error/empty (base=${baseFirst.version})`);
|
|
||||||
failures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both should satisfy the requested spec. A version diff is only a
|
|
||||||
// real failure if cand picked something the spec excludes (e.g.
|
|
||||||
// requested '20' but got '26'). Same prefix on both sides is just
|
|
||||||
// cache-age skew (e.g. 1.21.13 vs 1.21.14).
|
|
||||||
if (baseFirst.version !== candFirst.version) {
|
|
||||||
let prefix = tc.ver;
|
|
||||||
let candMatches = !prefix || new RegExp('^' + prefix + '\\b').test(candFirst.version);
|
|
||||||
let baseMatches = !prefix || new RegExp('^' + prefix + '\\b').test(baseFirst.version);
|
|
||||||
if (!candMatches && baseMatches) {
|
|
||||||
console.log(` FAIL ${label}: cand=${candFirst.version} doesn't match spec; base=${baseFirst.version}`);
|
|
||||||
failures++;
|
|
||||||
} else if (candMatches && !baseMatches) {
|
|
||||||
console.log(` PASS ${label}: cand=${candFirst.version} (base=${baseFirst.version} is wrong)`);
|
|
||||||
passes++;
|
|
||||||
} else {
|
|
||||||
console.log(` PASS ${label}: cand=${candFirst.version} base=${baseFirst.version} (both match '${prefix}'; cache-age skew)`);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(` PASS ${label}: v${candFirst.version}`);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Summary
|
|
||||||
// ================================================================
|
|
||||||
console.log('');
|
|
||||||
console.log(`=== Results: ${passes} passed, ${failures} failed, ${knowns} known, ${skips} skipped ===`);
|
|
||||||
if (failures > 0) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(function (err) {
|
|
||||||
console.error(err.stack);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
// Direct side-by-side comparison: fetch the live installer script from
|
|
||||||
// a remote host with the same UA, and compare WEBI_* variables against
|
|
||||||
// what our local serveInstaller() produces.
|
|
||||||
//
|
|
||||||
// This is the most direct behavioral equivalence test — if the same UA
|
|
||||||
// produces the same WEBI_PKG_URL, WEBI_VERSION, WEBI_EXT, and WEBI_OS,
|
|
||||||
// then the user gets the same binary.
|
|
||||||
//
|
|
||||||
// Usage:
|
|
||||||
// node _webi/test-live-installer-diff.js
|
|
||||||
// node _webi/test-live-installer-diff.js --base-url=https://beta.webi.sh
|
|
||||||
|
|
||||||
let Https = require('node:https');
|
|
||||||
let InstallerServer = require('./serve-installer.js');
|
|
||||||
let Builds = require('./builds.js');
|
|
||||||
|
|
||||||
let BASE_URL = 'https://webinstall.dev';
|
|
||||||
for (let arg of process.argv) {
|
|
||||||
if (arg.startsWith('--base-url=')) {
|
|
||||||
BASE_URL = arg.slice('--base-url='.length).replace(/\/+$/, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let CASES = [
|
|
||||||
// bat — Rust project, gnu-linked Linux builds
|
|
||||||
{ pkg: 'bat', ua: 'aarch64/unknown Darwin/24.2.0 libc', label: 'bat macOS arm64' },
|
|
||||||
{ pkg: 'bat', ua: 'x86_64/unknown Darwin/23.0.0 libc', label: 'bat macOS amd64' },
|
|
||||||
{ pkg: 'bat', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'bat Linux amd64' },
|
|
||||||
{ pkg: 'bat', ua: 'x86_64/unknown Linux/5.15.0 musl', label: 'bat Linux musl' },
|
|
||||||
{ pkg: 'bat', ua: 'x86_64/unknown Windows/10.0.19041 msvc', label: 'bat Windows amd64' },
|
|
||||||
// go — Go project, static builds (libc='none')
|
|
||||||
{ pkg: 'go', ua: 'aarch64/unknown Darwin/24.2.0 libc', label: 'go macOS arm64' },
|
|
||||||
{ pkg: 'go', ua: 'x86_64/unknown Darwin/23.0.0 libc', label: 'go macOS amd64' },
|
|
||||||
{ pkg: 'go', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'go Linux amd64' },
|
|
||||||
{ pkg: 'go', ua: 'x86_64/unknown Windows/10.0.19041 msvc', label: 'go Windows amd64' },
|
|
||||||
// node — C++ project, gnu-linked Linux builds, separate musl build
|
|
||||||
{ pkg: 'node', ua: 'aarch64/unknown Darwin/24.2.0 libc', label: 'node macOS arm64' },
|
|
||||||
{ pkg: 'node', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'node Linux amd64',
|
|
||||||
known: 'live fails (WATERFALL gap), local correctly resolves gnu build' },
|
|
||||||
{ pkg: 'node', ua: 'x86_64/unknown Linux/5.15.0 musl', label: 'node Linux musl' },
|
|
||||||
// rg — Rust project, gnu-linked Linux builds
|
|
||||||
{ pkg: 'rg', ua: 'aarch64/unknown Darwin/24.2.0 libc', label: 'rg macOS arm64' },
|
|
||||||
{ pkg: 'rg', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'rg Linux amd64' },
|
|
||||||
{ pkg: 'rg', ua: 'x86_64/unknown Linux/5.15.0 musl', label: 'rg Linux musl' },
|
|
||||||
{ pkg: 'rg', ua: 'x86_64/unknown Windows/10.0.19041 msvc', label: 'rg Windows amd64' },
|
|
||||||
// jq — C project, had .git source URLs in old releases
|
|
||||||
{ pkg: 'jq', ua: 'aarch64/unknown Darwin/24.2.0 libc', label: 'jq macOS arm64' },
|
|
||||||
{ pkg: 'jq', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'jq Linux amd64' },
|
|
||||||
{ pkg: 'jq', ua: 'x86_64/unknown Windows/10.0.19041 msvc', label: 'jq Windows amd64' },
|
|
||||||
// caddy — Go project, had .git source URLs in old releases
|
|
||||||
{ pkg: 'caddy', ua: 'aarch64/unknown Darwin/24.2.0 libc', label: 'caddy macOS arm64' },
|
|
||||||
{ pkg: 'caddy', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'caddy Linux amd64' },
|
|
||||||
{ pkg: 'caddy', ua: 'x86_64/unknown Windows/10.0.19041 msvc', label: 'caddy Windows amd64' },
|
|
||||||
// Additional packages for broader coverage
|
|
||||||
{ pkg: 'shellcheck', ua: 'aarch64/unknown Darwin/24.2.0 libc', label: 'shellcheck macOS arm64' },
|
|
||||||
{ pkg: 'shellcheck', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'shellcheck Linux amd64' },
|
|
||||||
{ pkg: 'shfmt', ua: 'aarch64/unknown Darwin/24.2.0 libc', label: 'shfmt macOS arm64' },
|
|
||||||
{ pkg: 'shfmt', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'shfmt Linux amd64' },
|
|
||||||
{ pkg: 'fd', ua: 'aarch64/unknown Darwin/24.2.0 libc', label: 'fd macOS arm64' },
|
|
||||||
{ pkg: 'fd', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'fd Linux amd64' },
|
|
||||||
{ pkg: 'hugo', ua: 'aarch64/unknown Darwin/24.2.0 libc', label: 'hugo macOS arm64',
|
|
||||||
known: 'classifier rejects darwin-universal as x86_64!=universal2' },
|
|
||||||
{ pkg: 'hugo', ua: 'x86_64/unknown Linux/5.15.0 libc', label: 'hugo Linux amd64' },
|
|
||||||
// Alias tests — these should resolve to the real package
|
|
||||||
{ pkg: 'golang', ua: 'aarch64/unknown Darwin/24.2.0 libc', label: 'golang alias macOS arm64' },
|
|
||||||
{ pkg: 'ripgrep', ua: 'aarch64/unknown Darwin/24.2.0 libc', label: 'ripgrep alias macOS arm64' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Variables that must match between live and local for the install to work
|
|
||||||
let CRITICAL_VARS = ['PKG_NAME', 'WEBI_OS', 'WEBI_ARCH', 'WEBI_EXT'];
|
|
||||||
// Variables where version differences are OK (cache age)
|
|
||||||
let VERSION_VARS = ['WEBI_VERSION', 'WEBI_PKG_URL', 'WEBI_PKG_FILE'];
|
|
||||||
|
|
||||||
function fetchLiveInstaller(pkg, ua) {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
let url = `${BASE_URL}/api/installers/${pkg}@stable.sh`;
|
|
||||||
let opts = { headers: { 'User-Agent': ua } };
|
|
||||||
Https.get(url, opts, function (res) {
|
|
||||||
let data = '';
|
|
||||||
res.on('data', function (chunk) { data += chunk; });
|
|
||||||
res.on('end', function () { resolve(data); });
|
|
||||||
}).on('error', reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseVars(script) {
|
|
||||||
let vars = {};
|
|
||||||
// Match: PKG_NAME='bat' or WEBI_VERSION='1.2.3' (with or without export)
|
|
||||||
let re = /^(?:export\s+)?(WEBI_\w+|PKG_NAME)='([^']*)'/gm;
|
|
||||||
let m;
|
|
||||||
while ((m = re.exec(script)) !== null) {
|
|
||||||
vars[m[1]] = m[2];
|
|
||||||
}
|
|
||||||
return vars;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
let passes = 0;
|
|
||||||
let failures = 0;
|
|
||||||
let knowns = 0;
|
|
||||||
let errors = 0;
|
|
||||||
|
|
||||||
console.log('Initializing build cache...');
|
|
||||||
await Builds.init();
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
console.log('=== Live vs Local Installer Diff ===');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
for (let tc of CASES) {
|
|
||||||
// Fetch live
|
|
||||||
let liveScript;
|
|
||||||
try {
|
|
||||||
liveScript = await fetchLiveInstaller(tc.pkg, tc.ua);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(` SKIP ${tc.label}: fetch error: ${e.message}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let liveVars = parseVars(liveScript);
|
|
||||||
|
|
||||||
if (!liveVars.WEBI_PKG_URL) {
|
|
||||||
console.log(` SKIP ${tc.label}: live returned no WEBI_PKG_URL`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render local
|
|
||||||
let localScript;
|
|
||||||
try {
|
|
||||||
localScript = await InstallerServer.serveInstaller(
|
|
||||||
BASE_URL,
|
|
||||||
tc.ua,
|
|
||||||
tc.pkg,
|
|
||||||
'stable',
|
|
||||||
'sh',
|
|
||||||
['tar', 'exe', 'zip', 'xz', 'dmg'],
|
|
||||||
'',
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(` ERROR ${tc.label}: local error: ${e.message}`);
|
|
||||||
errors++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let localVars = parseVars(localScript);
|
|
||||||
|
|
||||||
if (tc.known) {
|
|
||||||
let localExt = localVars.WEBI_EXT || 'err';
|
|
||||||
let liveExt = liveVars.WEBI_EXT || '?';
|
|
||||||
if (localExt !== liveExt) {
|
|
||||||
console.log(` KNOWN ${tc.label}: ${tc.known} (live=${liveExt} local=${localExt})`);
|
|
||||||
knowns++;
|
|
||||||
} else {
|
|
||||||
console.log(` PASS ${tc.label}: known issue resolved! v${localVars.WEBI_VERSION} .${localExt}`);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!localVars.WEBI_PKG_URL || localVars.WEBI_EXT === 'err') {
|
|
||||||
console.log(` KNOWN ${tc.label}: local failed to resolve (live=${liveVars.WEBI_EXT})`);
|
|
||||||
knowns++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare critical vars
|
|
||||||
let diffs = [];
|
|
||||||
for (let v of CRITICAL_VARS) {
|
|
||||||
let liveVal = liveVars[v] || '';
|
|
||||||
let localVal = localVars[v] || '';
|
|
||||||
if (liveVal !== localVal) {
|
|
||||||
diffs.push(`${v}: live='${liveVal}' local='${localVal}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log version info (informational, not failure)
|
|
||||||
let versionNote = '';
|
|
||||||
if (liveVars.WEBI_VERSION !== localVars.WEBI_VERSION) {
|
|
||||||
versionNote = ` (version: live=${liveVars.WEBI_VERSION} local=${localVars.WEBI_VERSION})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (diffs.length > 0) {
|
|
||||||
console.log(` FAIL ${tc.label}: ${diffs.join(', ')}${versionNote}`);
|
|
||||||
failures++;
|
|
||||||
} else {
|
|
||||||
let file = (localVars.WEBI_PKG_URL || '').split('/').pop();
|
|
||||||
console.log(` PASS ${tc.label}: v${localVars.WEBI_VERSION} .${localVars.WEBI_EXT} ${file}${versionNote}`);
|
|
||||||
passes++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
console.log(`=== Results: ${passes} passed, ${failures} failed, ${knowns} known, ${errors} errors ===`);
|
|
||||||
if (failures > 0 || errors > 0) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(function (err) {
|
|
||||||
console.error(err.stack);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -71,6 +71,14 @@ Builds.getPackage({ name: projName }).then(async function (/*projInfo*/) {
|
|||||||
var nodeOs = os.platform();
|
var nodeOs = os.platform();
|
||||||
var nodeOsRelease = os.release();
|
var nodeOsRelease = os.release();
|
||||||
var nodeArch = os.arch();
|
var nodeArch = os.arch();
|
||||||
|
|
||||||
|
// To make arch names compatible across all helpers
|
||||||
|
if (nodeArch === 'x64') {
|
||||||
|
nodeArch = 'amd64';
|
||||||
|
} else if (nodeArch === 'arm64') {
|
||||||
|
nodeArch = 'arm64';
|
||||||
|
}
|
||||||
|
|
||||||
var nodeLibc = 'libc';
|
var nodeLibc = 'libc';
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
nodeLibc = 'gnu';
|
nodeLibc = 'gnu';
|
||||||
|
|||||||
128
basecamp/README.md
Normal file
128
basecamp/README.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
title: basecamp
|
||||||
|
homepage: https://github.com/basecamp/basecamp-cli
|
||||||
|
tagline: |
|
||||||
|
basecamp: CLI for Basecamp 3 — manage projects, todos, messages, cards, and more from the terminal.
|
||||||
|
---
|
||||||
|
|
||||||
|
To update or switch versions, run `webi basecamp@stable` (or `@v0.7`,
|
||||||
|
`@beta`, etc).
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
These are the files / directories that are created and/or modified with this
|
||||||
|
install:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.config/envman/PATH.env
|
||||||
|
~/.local/bin/basecamp
|
||||||
|
~/.local/opt/basecamp-VERSION/bin/basecamp
|
||||||
|
~/.local/opt/basecamp-VERSION/completions/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cheat Sheet
|
||||||
|
|
||||||
|
> `basecamp` is the official CLI for Basecamp 3. It provides full API coverage
|
||||||
|
> for projects, todos, messages, cards, schedule, files, and more — all from the
|
||||||
|
> command line.
|
||||||
|
|
||||||
|
### How to authenticate
|
||||||
|
|
||||||
|
```sh
|
||||||
|
basecamp auth login
|
||||||
|
```
|
||||||
|
|
||||||
|
For headless environments (CI, remote servers):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
basecamp auth login --device-code
|
||||||
|
```
|
||||||
|
|
||||||
|
Check auth status:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
basecamp auth status
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to list projects and todos
|
||||||
|
|
||||||
|
```sh
|
||||||
|
basecamp projects list --md
|
||||||
|
|
||||||
|
basecamp todos list --assignee me --in PROJECT_ID --md
|
||||||
|
```
|
||||||
|
|
||||||
|
Cross-project view of your assigned work:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
basecamp assignments --md
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to create and complete todos
|
||||||
|
|
||||||
|
```sh
|
||||||
|
basecamp todo "Write release notes" --in PROJECT_ID --list TODOLIST_ID --assignee me --due tomorrow
|
||||||
|
|
||||||
|
basecamp done TODO_ID
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to post a message or comment
|
||||||
|
|
||||||
|
```sh
|
||||||
|
basecamp message "Sprint Update" "Shipped v2.1 to production." --in PROJECT_ID
|
||||||
|
|
||||||
|
basecamp comment RECORDING_ID "Looks good." --in PROJECT_ID
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to move cards through a workflow
|
||||||
|
|
||||||
|
```sh
|
||||||
|
basecamp cards columns --in PROJECT_ID --md
|
||||||
|
|
||||||
|
basecamp cards move CARD_ID --to COLUMN_ID --in PROJECT_ID
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to set up per-project defaults
|
||||||
|
|
||||||
|
Create `.basecamp/config.json` in your repo (commit it):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"project_id": "12345",
|
||||||
|
"todolist_id": "67890"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then trust it once:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
basecamp config trust
|
||||||
|
```
|
||||||
|
|
||||||
|
After that, you can omit `--in` for most commands in that repo.
|
||||||
|
|
||||||
|
### Shell completions
|
||||||
|
|
||||||
|
Completions for bash, fish, and zsh ship with the installer. Find them at:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.local/opt/basecamp-VERSION/completions/
|
||||||
|
```
|
||||||
|
|
||||||
|
Bash:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
echo "source ~/.local/opt/basecamp-VERSION/completions/basecamp.bash" >> ~/.bashrc
|
||||||
|
```
|
||||||
|
|
||||||
|
Fish:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ln -s ~/.local/opt/basecamp-VERSION/completions/basecamp.fish ~/.config/fish/completions/
|
||||||
|
```
|
||||||
|
|
||||||
|
Zsh:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
echo "fpath+=( ~/.local/opt/basecamp-VERSION/completions )" >> ~/.zshrc
|
||||||
|
```
|
||||||
46
basecamp/install.ps1
Normal file
46
basecamp/install.ps1
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
|
||||||
|
$pkg_cmd_name = "basecamp"
|
||||||
|
|
||||||
|
$pkg_dst_cmd = "$Env:USERPROFILE\.local\bin\basecamp.exe"
|
||||||
|
$pkg_dst = "$pkg_dst_cmd"
|
||||||
|
|
||||||
|
$pkg_src_cmd = "$Env:USERPROFILE\.local\opt\basecamp-v$Env:WEBI_VERSION\bin\basecamp.exe"
|
||||||
|
$pkg_src_bin = "$Env:USERPROFILE\.local\opt\basecamp-v$Env:WEBI_VERSION\bin"
|
||||||
|
$pkg_src_dir = "$Env:USERPROFILE\.local\opt\basecamp-v$Env:WEBI_VERSION"
|
||||||
|
$pkg_src = "$pkg_src_cmd"
|
||||||
|
|
||||||
|
New-Item "$Env:USERPROFILE\Downloads\webi" -ItemType Directory -Force | Out-Null
|
||||||
|
$pkg_download = "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE"
|
||||||
|
|
||||||
|
if (!(Test-Path -Path "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE")) {
|
||||||
|
Write-Output "Downloading basecamp from $Env:WEBI_PKG_URL to $pkg_download"
|
||||||
|
& curl.exe -A "$Env:WEBI_UA" -fsSL "$Env:WEBI_PKG_URL" -o "$pkg_download.part"
|
||||||
|
& Move-Item "$pkg_download.part" "$pkg_download"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(Test-Path -Path "$pkg_src_cmd")) {
|
||||||
|
Write-Output "Installing basecamp"
|
||||||
|
|
||||||
|
Push-Location .local\tmp
|
||||||
|
|
||||||
|
Remove-Item -Path ".\basecamp-*" -Recurse -ErrorAction Ignore
|
||||||
|
Remove-Item -Path ".\basecamp.exe" -Recurse -ErrorAction Ignore
|
||||||
|
|
||||||
|
Write-Output "Unpacking $pkg_download"
|
||||||
|
& tar xf "$pkg_download"
|
||||||
|
|
||||||
|
New-Item "$pkg_src_bin" -ItemType Directory -Force | Out-Null
|
||||||
|
Move-Item -Path ".\basecamp.exe" -Destination "$pkg_src_bin"
|
||||||
|
|
||||||
|
New-Item "$pkg_src_dir\completions" -ItemType Directory -Force | Out-Null
|
||||||
|
if (Test-Path -Path ".\completions") {
|
||||||
|
Copy-Item -Path ".\completions\*" -Destination "$pkg_src_dir\completions" -Recurse
|
||||||
|
}
|
||||||
|
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "Copying into '$pkg_dst_cmd' from '$pkg_src_cmd'"
|
||||||
|
Remove-Item -Path "$pkg_dst_cmd" -Recurse -ErrorAction Ignore | Out-Null
|
||||||
|
Copy-Item -Path "$pkg_src" -Destination "$pkg_dst" -Recurse
|
||||||
44
basecamp/install.sh
Normal file
44
basecamp/install.sh
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
|
||||||
|
set -e
|
||||||
|
set -u
|
||||||
|
|
||||||
|
__init_basecamp() {
|
||||||
|
pkg_cmd_name="basecamp"
|
||||||
|
|
||||||
|
pkg_src_dir="$HOME/.local/opt/basecamp-v$WEBI_VERSION"
|
||||||
|
pkg_src_cmd="$pkg_src_dir/bin/basecamp"
|
||||||
|
pkg_src="$pkg_src_cmd"
|
||||||
|
|
||||||
|
pkg_dst_cmd="$HOME/.local/bin/basecamp"
|
||||||
|
pkg_dst="$pkg_dst_cmd"
|
||||||
|
|
||||||
|
pkg_install() {
|
||||||
|
mkdir -p "$(dirname "$pkg_src_cmd")"
|
||||||
|
mkdir -p "$pkg_src_dir/completions"
|
||||||
|
|
||||||
|
if test -f ./basecamp; then
|
||||||
|
mv ./basecamp "$pkg_src_cmd"
|
||||||
|
elif test -e ./basecamp-*/basecamp; then
|
||||||
|
mv ./basecamp-*/basecamp "$pkg_src_cmd"
|
||||||
|
elif test -e ./basecamp-*; then
|
||||||
|
mv ./basecamp-* "$pkg_src_cmd"
|
||||||
|
else
|
||||||
|
echo >&2 "failed to find 'basecamp' executable"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if test -d ./completions; then
|
||||||
|
cp -a ./completions/. "$pkg_src_dir/completions/"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
pkg_get_current_version() {
|
||||||
|
basecamp --version 2> /dev/null |
|
||||||
|
head -n 1 |
|
||||||
|
cut -d' ' -f3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__init_basecamp
|
||||||
2
basecamp/releases.conf
Normal file
2
basecamp/releases.conf
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github_releases = basecamp/basecamp-cli
|
||||||
|
exclude = .bundle .txt
|
||||||
188
brew/install.sh
188
brew/install.sh
@@ -4,73 +4,143 @@ set -e
|
|||||||
set -u
|
set -u
|
||||||
|
|
||||||
_install_brew() {
|
_install_brew() {
|
||||||
# Straight from https://brew.sh
|
# Straight from https://brew.sh
|
||||||
#/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
|
#/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||||
|
|
||||||
if test "Darwin" = "$(uname -s)"; then
|
if test "Darwin" = "$(uname -s)"; then
|
||||||
needs_xcode="$(/usr/bin/xcode-select -p > /dev/null 2> /dev/null || echo "true")"
|
needs_xcode="$(/usr/bin/xcode-select -p > /dev/null 2> /dev/null || echo "true")"
|
||||||
if test -n "${needs_xcode}"; then
|
if test -n "${needs_xcode}"; then
|
||||||
echo ""
|
echo ""
|
||||||
echo ""
|
echo ""
|
||||||
echo "ERROR: Run this command to install XCode Command Line Tools first:"
|
echo "ERROR: Run this command to install XCode Command Line Tools first:"
|
||||||
echo ""
|
echo ""
|
||||||
echo " xcode-select --install"
|
echo " xcode-select --install"
|
||||||
echo ""
|
echo ""
|
||||||
echo "After the install, close this terminal, open a new one, and try again."
|
echo "After the install, close this terminal, open a new one, and try again."
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if ! command -v gcc > /dev/null; then
|
if ! command -v gcc > /dev/null; then
|
||||||
echo >&2 "Warning: to install 'gcc' et al on Linux use the built-in package manager."
|
echo >&2 "Warning: to install 'gcc' et al on Linux use the built-in package manager."
|
||||||
echo >&2 " For example, try: sudo apt install -y build-essential"
|
echo >&2 " For example, try: sudo apt install -y build-essential"
|
||||||
fi
|
fi
|
||||||
if ! command -v git > /dev/null; then
|
if ! command -v git > /dev/null; then
|
||||||
echo >&2 "Error: to install 'git' on Linux use the built-in package manager."
|
echo >&2 "Error: to install 'git' on Linux use the built-in package manager."
|
||||||
echo >&2 " For example, try: sudo apt install -y git"
|
echo >&2 " For example, try: sudo apt install -y git"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# From Straight from https://brew.sh
|
# From Straight from https://brew.sh
|
||||||
if ! test -d "$HOME/.local/opt/brew"; then
|
if ! test -d "$HOME/.local/opt/brew"; then
|
||||||
echo "Installing to '$HOME/.local/opt/brew'"
|
echo "Installing to '$HOME/.local/opt/brew'"
|
||||||
echo ""
|
echo ""
|
||||||
echo "If you prefer to have brew installed to '/usr/local' cancel now and do the following:"
|
echo "If you prefer to have brew installed to '/usr/local' cancel now and do the following:"
|
||||||
echo " rm -rf '$HOME/.local/opt/brew'"
|
echo " rm -rf '$HOME/.local/opt/brew'"
|
||||||
# shellcheck disable=2016
|
# shellcheck disable=2016
|
||||||
echo ' /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"'
|
echo ' /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
|
||||||
echo ""
|
echo ""
|
||||||
sleep 3
|
sleep 3
|
||||||
git clone --depth=1 https://github.com/Homebrew/brew "$HOME/.local/opt/brew"
|
#mkdir -p "$HOME/.local/opt/brew"
|
||||||
fi
|
#curl -fsSL https://github.com/Homebrew/brew/tarball/main |
|
||||||
|
# tar xz --strip-components 1 -C "$HOME/.local/opt/brew"
|
||||||
|
git clone --depth 3 https://github.com/Homebrew/brew "$HOME/.local/opt/brew"
|
||||||
|
fi
|
||||||
|
|
||||||
rm -rf "$HOME/.local/bin/brew-update-service-install"
|
my_shellenv="$("$HOME/.local/opt/brew/bin/brew" shellenv)"
|
||||||
webi_download \
|
eval "${my_shellenv}"
|
||||||
"$WEBI_HOST/packages/brew/brew-update-service-install" \
|
chmod -R go-w "$(brew --prefix)/share/zsh" 2> /dev/null || true
|
||||||
"$HOME/.local/bin/brew-update-service-install" \
|
|
||||||
brew-update-service-install
|
|
||||||
chmod a+x "$HOME/.local/bin/brew-update-service-install"
|
|
||||||
|
|
||||||
webi_path_add "$HOME/.local/opt/brew/bin"
|
rm -rf "$HOME/.local/bin/brew-update-service-install"
|
||||||
export PATH="$HOME/.local/opt/brew/bin:$PATH"
|
webi_download \
|
||||||
|
"$WEBI_HOST/packages/brew/brew-update-service-install" \
|
||||||
|
"$HOME/.local/bin/brew-update-service-install" \
|
||||||
|
brew-update-service-install
|
||||||
|
chmod a+x "$HOME/.local/bin/brew-update-service-install"
|
||||||
|
|
||||||
echo "Updating brew..."
|
webi_path_add "$HOME/.local/opt/brew/bin"
|
||||||
brew update
|
webi_path_add "$HOME/.local/opt/brew/sbin"
|
||||||
|
|
||||||
webi_path_add "$HOME/.local/opt/brew/sbin"
|
fn_brew_shell_integrate_bash
|
||||||
export PATH="$HOME/.local/opt/brew/sbin:$PATH"
|
fn_brew_shell_integrate_zsh
|
||||||
|
fn_brew_shell_integrate_fish
|
||||||
|
|
||||||
echo "Installed 'brew' to '$HOME/.local/opt/brew'"
|
echo "Installed 'brew' to '$HOME/.local/opt/brew'"
|
||||||
echo ""
|
echo ""
|
||||||
echo "If you prefer to have brew installed to '/usr/local' do the following:"
|
echo "If you prefer to have brew installed to '/usr/local' do the following:"
|
||||||
echo " mv '$HOME/.local/opt/brew' '$HOME/.local/opt/brew.$(date '+%F_%H-%M-%S').bak'"
|
echo " mv '$HOME/.local/opt/brew' '$HOME/.local/opt/brew.$(date '+%F_%H-%M-%S').bak'"
|
||||||
# shellcheck disable=2016
|
# shellcheck disable=2016
|
||||||
echo ' /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"'
|
echo ' /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
echo "To register 'brew update' as a hourly system service:"
|
echo "To register 'brew update' as a hourly system service:"
|
||||||
echo " brew-update-service-install"
|
echo " brew-update-service-install"
|
||||||
echo ""
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fn_brew_shell_integrate_bash() {
|
||||||
|
if ! command -v bash > /dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if ! test -e ~/.bashrc && ! test -e ~/.bash_history; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
touch -a ~/.bashrc
|
||||||
|
if grep -q 'brew shellenv' ~/.bashrc; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo >&2 " Edit ~/.bashrc to init brew"
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
{
|
||||||
|
echo ''
|
||||||
|
echo '# Generated by Webi. Do not edit.'
|
||||||
|
echo 'eval "$('"$HOME/.local/opt/brew/bin/brew"' shellenv)"'
|
||||||
|
} >> ~/.bashrc
|
||||||
|
}
|
||||||
|
|
||||||
|
fn_brew_shell_integrate_zsh() {
|
||||||
|
if ! command -v zsh > /dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if ! test -e ~/.zshrc &&
|
||||||
|
! test -e ~/.zsh_sessions &&
|
||||||
|
! test -e ~/.zsh_history; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
touch -a ~/.zshrc
|
||||||
|
if grep -q 'brew shellenv' ~/.zshrc; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo >&2 " Edit ~/.zshrc to init brew"
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
{
|
||||||
|
echo ''
|
||||||
|
echo '# Generated by Webi. Do not edit.'
|
||||||
|
echo 'eval "$('"$HOME/.local/opt/brew/bin/brew"' shellenv)"'
|
||||||
|
} >> ~/.zshrc
|
||||||
|
}
|
||||||
|
|
||||||
|
fn_brew_shell_integrate_fish() {
|
||||||
|
if ! command -v fish > /dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p ~/.config/fish
|
||||||
|
touch -a ~/.config/fish/config.fish
|
||||||
|
if grep -q 'brew shellenv' ~/.config/fish/config.fish; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo >&2 " Edit ~/.config/fish/config.fish to init brew"
|
||||||
|
{
|
||||||
|
echo ''
|
||||||
|
echo '# Generated by Webi. Do not edit.'
|
||||||
|
echo "$HOME/.local/opt/brew/bin/brew shellenv | source"
|
||||||
|
} >> ~/.config/fish/config.fish
|
||||||
}
|
}
|
||||||
|
|
||||||
_install_brew
|
_install_brew
|
||||||
|
|||||||
109
btop/README.md
Normal file
109
btop/README.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
---
|
||||||
|
title: btop
|
||||||
|
homepage: https://github.com/aristocratos/btop
|
||||||
|
tagline: |
|
||||||
|
btop: a beautiful, interactive resource monitor
|
||||||
|
description: |
|
||||||
|
btop++ is a fast, feature-rich terminal resource monitor written in C++.
|
||||||
|
It shows real-time usage and stats for CPU, memory, disks, network, and
|
||||||
|
processes — with full mouse support, customizable themes, and an
|
||||||
|
easy-to-use menu system. The spiritual successor to bashtop and bpytop.
|
||||||
|
---
|
||||||
|
|
||||||
|
To update or switch versions, run `webi btop@stable` (or `@v1.4`, `@beta`, etc).
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
These are the files / directories that are created and/or modified with this
|
||||||
|
install:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.config/envman/PATH.env
|
||||||
|
~/.local/bin/btop
|
||||||
|
~/.local/opt/btop/
|
||||||
|
~/.local/opt/btop-<VERSION>/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cheat Sheet
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> btop gives you a gorgeous, interactive view of what your system is doing —
|
||||||
|
> CPU cores, RAM, swap, disk I/O, network throughput, and a filterable process
|
||||||
|
> list — all in one terminal window.
|
||||||
|
|
||||||
|
### Launch btop
|
||||||
|
|
||||||
|
```sh
|
||||||
|
btop
|
||||||
|
```
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
| -------------- | ----------------------------------- |
|
||||||
|
| `Arrow keys` | Move selection in process list |
|
||||||
|
| `Enter` | Show detailed stats for process |
|
||||||
|
| `F` | Filter / search processes |
|
||||||
|
| `K` | Send signal (kill, SIGTERM, etc.) |
|
||||||
|
| `R` | Renice (change process priority) |
|
||||||
|
| `T` | Toggle tree / flat process view |
|
||||||
|
| `M` | Change sort field |
|
||||||
|
| `ESC` | Open settings menu |
|
||||||
|
| `Q` | Quit |
|
||||||
|
|
||||||
|
Mouse support is fully enabled by default — scroll and click anywhere in the UI.
|
||||||
|
|
||||||
|
### Change the color theme
|
||||||
|
|
||||||
|
Press `ESC` to open the menu, navigate to **Options → Color theme**, and pick
|
||||||
|
from the built-in themes (Default, TTY, Dracula, Gruvbox, and more).
|
||||||
|
|
||||||
|
Custom themes can be placed in:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.config/btop/themes/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adjust update interval
|
||||||
|
|
||||||
|
In the Options menu, set **Update interval** (in milliseconds). The default is
|
||||||
|
`2000` (2 seconds). Lower values give a more live feel; higher values reduce CPU
|
||||||
|
overhead from btop itself.
|
||||||
|
|
||||||
|
### Config file location
|
||||||
|
|
||||||
|
btop's settings are saved automatically at:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.config/btop/btop.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
You can edit this file directly to set options like `update_ms`, `color_theme`,
|
||||||
|
`proc_sorting`, or `net_iface`.
|
||||||
|
|
||||||
|
### Run btop with a specific network interface shown
|
||||||
|
|
||||||
|
```sh
|
||||||
|
btop --utf-foce # force UTF-8 box drawing
|
||||||
|
btop --debug # verbose debug output to btop.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Network interface selection is done interactively inside btop via the network
|
||||||
|
panel — press `B` / `N` to cycle interfaces.
|
||||||
|
|
||||||
|
### GPU monitoring (Linux x86\_64)
|
||||||
|
|
||||||
|
On Linux, btop supports Nvidia, AMD, and Intel GPUs out of the box provided the
|
||||||
|
correct drivers are installed. If wattage or GPU stats are missing, you may need
|
||||||
|
to grant extended capabilities:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Run once after install (requires sudo)
|
||||||
|
sudo setcap cap_perfmon,cap_sys_ptrace+ep ~/.local/bin/btop
|
||||||
|
```
|
||||||
|
|
||||||
|
### See also
|
||||||
|
|
||||||
|
- [btop releases](https://github.com/aristocratos/btop/releases)
|
||||||
|
- [Theme gallery](https://github.com/aristocratos/btop/tree/main/themes)
|
||||||
58
btop/install.sh
Normal file
58
btop/install.sh
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
__init_btop() {
|
||||||
|
set -e
|
||||||
|
set -u
|
||||||
|
|
||||||
|
################
|
||||||
|
# Install btop #
|
||||||
|
################
|
||||||
|
|
||||||
|
# Every package should define these 6 variables
|
||||||
|
pkg_cmd_name="btop"
|
||||||
|
|
||||||
|
pkg_dst_cmd="$HOME/.local/bin/btop"
|
||||||
|
pkg_dst="$pkg_dst_cmd"
|
||||||
|
|
||||||
|
pkg_src_cmd="$HOME/.local/opt/btop-v$WEBI_VERSION/bin/btop"
|
||||||
|
pkg_src_dir="$HOME/.local/opt/btop-v$WEBI_VERSION"
|
||||||
|
pkg_src="$pkg_src_cmd"
|
||||||
|
|
||||||
|
# pkg_install must be defined by every package
|
||||||
|
pkg_install() {
|
||||||
|
# ~/.local/opt/btop-v1.4.6/bin
|
||||||
|
mkdir -p "$(dirname "${pkg_src_cmd}")"
|
||||||
|
|
||||||
|
# mv ./btop/bin/btop ~/.local/opt/btop-v1.4.6/bin/btop
|
||||||
|
mv ./btop/bin/btop "${pkg_src_cmd}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# pkg_get_current_version is recommended, but not required
|
||||||
|
pkg_get_current_version() {
|
||||||
|
# 'btop --version' has output in this format:
|
||||||
|
# btop 1.4.6 (rev abcdef0123)
|
||||||
|
# This trims it down to just the version number:
|
||||||
|
# 1.4.6
|
||||||
|
btop --version 2> /dev/null |
|
||||||
|
head -n 1 |
|
||||||
|
cut -d ' ' -f 2
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn_btop_brew_install() {
|
||||||
|
if ! command -v brew > /dev/null; then
|
||||||
|
"$HOME/.local/bin/webi" brew
|
||||||
|
export PATH="$HOME/.local/opt/brew/bin:$HOME/.local/opt/brew/sbin:$PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export HOMEBREW_NO_AUTO_UPDATE=1
|
||||||
|
export HOMEBREW_NO_ENV_HINTS=1
|
||||||
|
brew install btop
|
||||||
|
}
|
||||||
|
|
||||||
|
my_os=$(uname -s)
|
||||||
|
if test "Darwin" = "${my_os}" && test "$WEBI_CHANNEL" = "error"; then
|
||||||
|
fn_btop_brew_install
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
__init_btop
|
||||||
2
btop/releases.conf
Normal file
2
btop/releases.conf
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github_releases = aristocratos/btop
|
||||||
|
exclude = -m68k -bigsur -monterey -ventura -macos
|
||||||
@@ -332,14 +332,13 @@ func (wc *WebiCache) stalest(packages []pkgConf) []pkgConf {
|
|||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
data, err := wc.Store.Load(ctx, pkg.name)
|
data, err := wc.Store.Load(ctx, pkg.name)
|
||||||
var t time.Time
|
var t time.Time
|
||||||
hasAssets := false
|
|
||||||
if err == nil && data != nil {
|
if err == nil && data != nil {
|
||||||
t = data.UpdatedAt
|
t = data.UpdatedAt
|
||||||
hasAssets = len(data.Assets) > 0
|
|
||||||
}
|
}
|
||||||
// Never fetched, or has no assets despite having a timestamp
|
// Never fetched, or older than 10 minutes.
|
||||||
// (e.g. classified from empty rawcache), or older than 10 minutes.
|
// 0-asset results are not treated as perpetually stale — packages that
|
||||||
if t.IsZero() || !hasAssets || time.Since(t) > 10*time.Minute {
|
// produce no classifiable assets (e.g. galera) respect the timestamp.
|
||||||
|
if t.IsZero() || time.Since(t) > 10*time.Minute {
|
||||||
stale = append(stale, stamped{pkg: pkg, updatedAt: t})
|
stale = append(stale, stamped{pkg: pkg, updatedAt: t})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
149
fish/install.sh
149
fish/install.sh
@@ -3,23 +3,19 @@ set -e
|
|||||||
set -u
|
set -u
|
||||||
|
|
||||||
if command -v fish > /dev/null; then
|
if command -v fish > /dev/null; then
|
||||||
if [ ! -e ~/.config/fish/config.fish ]; then
|
if ! test -e ~/.config/fish/config.fish; then
|
||||||
mkdir -p ~/.config/fish
|
mkdir -p ~/.config/fish
|
||||||
touch ~/.config/fish/config.fish
|
touch ~/.config/fish/config.fish
|
||||||
chmod 0600 ~/.config/fish/config.fish
|
chmod 0600 ~/.config/fish/config.fish
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "Darwin" != "$(uname -s)" ]; then
|
|
||||||
echo "No fish installer for Linux yet. Try this instead:"
|
|
||||||
echo " sudo apt install -y fish"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
################
|
################
|
||||||
# Install fish #
|
# Install fish #
|
||||||
################
|
################
|
||||||
|
|
||||||
|
my_os=$(uname -s)
|
||||||
|
|
||||||
# Every package should define these 6 variables
|
# Every package should define these 6 variables
|
||||||
# shellcheck disable=2034
|
# shellcheck disable=2034
|
||||||
pkg_cmd_name="fish"
|
pkg_cmd_name="fish"
|
||||||
@@ -34,70 +30,109 @@ pkg_src_dir="$HOME/.local/opt/fish-v$WEBI_VERSION"
|
|||||||
# shellcheck disable=2034
|
# shellcheck disable=2034
|
||||||
pkg_src="$pkg_src_cmd"
|
pkg_src="$pkg_src_cmd"
|
||||||
|
|
||||||
# pkg_install must be defined by every package
|
if test "Darwin" = "${my_os}"; then
|
||||||
|
pkg_src_cmd="/Applications/fish.app/Contents/Resources/base/usr/local/bin/fish"
|
||||||
|
# shellcheck disable=2034
|
||||||
|
pkg_src="${pkg_src_cmd}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
_linux_post_install() {
|
||||||
|
if ! test -e "$HOME/.local/bin/fish"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "To set fish as your default shell, run:"
|
||||||
|
echo " chsh -s $HOME/.local/bin/fish"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
_macos_post_install() {
|
_macos_post_install() {
|
||||||
if ! [ -e "$HOME/.local/bin/fish" ]; then
|
if ! test -e "$HOME/.local/bin/fish"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Trying to set fish as the default shell..."
|
echo "Trying to set fish as the default shell..."
|
||||||
echo ""
|
echo ""
|
||||||
# stop the caching of preferences
|
# stop the caching of preferences
|
||||||
killall cfprefsd
|
killall cfprefsd
|
||||||
|
|
||||||
# Set default Terminal.app shell to fish
|
# Set default Terminal.app shell to fish
|
||||||
defaults write com.apple.Terminal "Shell" -string "$HOME/.local/bin/fish"
|
defaults write com.apple.Terminal "Shell" -string "$HOME/.local/bin/fish"
|
||||||
echo "To set 'fish' as the default Terminal.app shell:"
|
echo "To set 'fish' as the default Terminal.app shell:"
|
||||||
echo " Terminal > Preferences > General > Shells open with:"
|
echo " Terminal > Preferences > General > Shells open with:"
|
||||||
echo " $HOME/.local/bin/fish"
|
echo " $HOME/.local/bin/fish"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Set default iTerm2 shell to fish
|
# Set default iTerm2 shell to fish
|
||||||
if [ -e "$HOME/Library/Preferences/com.googlecode.iterm2.plist" ]; then
|
if test -e "$HOME/Library/Preferences/com.googlecode.iterm2.plist"; then
|
||||||
/usr/libexec/PlistBuddy \
|
/usr/libexec/PlistBuddy \
|
||||||
-c "SET ':New Bookmarks:0:Custom Command' 'Custom Shell'" \
|
-c "SET ':New Bookmarks:0:Custom Command' 'Custom Shell'" \
|
||||||
"$HOME/Library/Preferences/com.googlecode.iterm2.plist"
|
"$HOME/Library/Preferences/com.googlecode.iterm2.plist"
|
||||||
/usr/libexec/PlistBuddy \
|
/usr/libexec/PlistBuddy \
|
||||||
-c "SET ':New Bookmarks:0:Command' $HOME/.local/bin/fish" \
|
-c "SET ':New Bookmarks:0:Command' $HOME/.local/bin/fish" \
|
||||||
"$HOME/Library/Preferences/com.googlecode.iterm2.plist"
|
"$HOME/Library/Preferences/com.googlecode.iterm2.plist"
|
||||||
echo "To set 'fish' as the default iTerm2 shell:"
|
echo "To set 'fish' as the default iTerm2 shell:"
|
||||||
echo " iTerm2 > Preferences > Profiles > General > Command >"
|
echo " iTerm2 > Preferences > Profiles > General > Command >"
|
||||||
echo " Custom Shell: $HOME/.local/bin/fish"
|
echo " Custom Shell: $HOME/.local/bin/fish"
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
killall cfprefsd
|
killall cfprefsd
|
||||||
}
|
}
|
||||||
|
|
||||||
# always try to reset the default shells
|
# always try to reset the default shells
|
||||||
_macos_post_install
|
if test "Darwin" = "${my_os}"; then
|
||||||
|
_macos_post_install
|
||||||
|
fi
|
||||||
|
|
||||||
pkg_install() {
|
pkg_install() {
|
||||||
mv fish.app/Contents/Resources/base/usr/local "$HOME/.local/opt/fish-v${WEBI_VERSION}"
|
if test "Darwin" = "${my_os}"; then
|
||||||
|
rm -rf "/Applications/fish-v${WEBI_VERSION}.app"
|
||||||
|
mv -f fish*.app "/Applications/fish-v${WEBI_VERSION}.app"
|
||||||
|
rm -rf /Applications/fish.app
|
||||||
|
mv "/Applications/fish-v${WEBI_VERSION}.app" "/Applications/fish.app"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$pkg_src_dir/bin"
|
||||||
|
mv fish "$pkg_src_dir/bin/"
|
||||||
|
}
|
||||||
|
|
||||||
|
pkg_link() {
|
||||||
|
if test "Darwin" = "${my_os}"; then
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
ln -sf /Applications/fish.app/Contents/Resources/base/usr/local/bin/fish "$HOME/.local/bin/fish"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$pkg_dst_cmd"
|
||||||
|
ln -s "$pkg_src_cmd" "$pkg_dst_cmd"
|
||||||
}
|
}
|
||||||
|
|
||||||
pkg_post_install() {
|
pkg_post_install() {
|
||||||
# don't skip what webi would do automatically
|
# don't skip what webi would do automatically
|
||||||
webi_post_install
|
webi_post_install
|
||||||
|
|
||||||
# try again to update default shells, now that all files should exist
|
# try again to update default shells, now that all files should exist
|
||||||
_macos_post_install
|
if test "Darwin" = "${my_os}"; then
|
||||||
|
_macos_post_install
|
||||||
if [ ! -e ~/.config/fish/config.fish ]; then
|
else
|
||||||
mkdir -p ~/.config/fish
|
_linux_post_install
|
||||||
touch ~/.config/fish/config.fish
|
fi
|
||||||
chmod 0600 ~/.config/fish/config.fish
|
if ! test -e ~/.config/fish/config.fish; then
|
||||||
fi
|
mkdir -p ~/.config/fish
|
||||||
|
touch ~/.config/fish/config.fish
|
||||||
|
chmod 0600 ~/.config/fish/config.fish
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# pkg_get_current_version is recommended, but (soon) not required
|
# pkg_get_current_version is recommended, but (soon) not required
|
||||||
pkg_get_current_version() {
|
pkg_get_current_version() {
|
||||||
# 'fish --version' has output in this format:
|
# 'fish --version' has output in this format:
|
||||||
# fish, version 3.1.2
|
# fish, version 4.3.3
|
||||||
# This trims it down to just the version number:
|
# This trims it down to just the version number:
|
||||||
# 3.1.2
|
# 4.3.3
|
||||||
fish --version 2> /dev/null | head -n 1 | cut -d ' ' -f 3
|
fish --version 2> /dev/null | head -n 1 | cut -d ' ' -f 3
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ __init_gh() {
|
|||||||
# ~/.local/opt/gh-v0.99.9/bin
|
# ~/.local/opt/gh-v0.99.9/bin
|
||||||
mkdir -p "$(dirname "$pkg_src_cmd")"
|
mkdir -p "$(dirname "$pkg_src_cmd")"
|
||||||
|
|
||||||
# mv ./gh-*/gh ~/.local/opt/gh-v0.99.9/bin/gh
|
# mv ./gh_*/bin/gh ~/.local/opt/gh-v0.99.9/bin/gh
|
||||||
mv ./"$pkg_cmd_name"*/bin/gh "$pkg_src_cmd"
|
mv ./"$pkg_cmd_name"*/bin/gh "$pkg_src_cmd"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
github_releases = go-gitea/gitea
|
github_releases = go-gitea/gitea
|
||||||
exclude = -src- -docs-
|
exclude = -src- -docs-
|
||||||
|
variants = gogit
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ __init_goreleaser() {
|
|||||||
# ~/.local/opt/goreleaser-v1.21.2/bin
|
# ~/.local/opt/goreleaser-v1.21.2/bin
|
||||||
mkdir -p "$(dirname "$pkg_src_cmd")"
|
mkdir -p "$(dirname "$pkg_src_cmd")"
|
||||||
|
|
||||||
# mv ./goreleaser-*/goreleaser ~/.local/opt/goreleaser-v1.21.2/bin/goreleaser
|
# mv ./goreleaser ~/.local/opt/goreleaser-v1.21.2/bin/goreleaser
|
||||||
mv ./goreleaser "$pkg_src_cmd"
|
mv ./goreleaser "$pkg_src_cmd"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ package classify
|
|||||||
import (
|
import (
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/webinstall/webi-installers/internal/buildmeta"
|
"github.com/webinstall/webi-installers/internal/buildmeta"
|
||||||
@@ -268,16 +269,11 @@ func IsMetaAsset(name string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, exact := range []string{
|
return slices.Contains([]string{
|
||||||
"install.sh",
|
"install.sh",
|
||||||
"install.ps1",
|
"install.ps1",
|
||||||
"compat.json",
|
"compat.json",
|
||||||
"b3sums",
|
"b3sums",
|
||||||
"dist-manifest.json",
|
"dist-manifest.json",
|
||||||
} {
|
}, lower)
|
||||||
if lower == exact {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import (
|
|||||||
"github.com/webinstall/webi-installers/internal/releases/chromedist"
|
"github.com/webinstall/webi-installers/internal/releases/chromedist"
|
||||||
"github.com/webinstall/webi-installers/internal/releases/cmake"
|
"github.com/webinstall/webi-installers/internal/releases/cmake"
|
||||||
"github.com/webinstall/webi-installers/internal/releases/fish"
|
"github.com/webinstall/webi-installers/internal/releases/fish"
|
||||||
"github.com/webinstall/webi-installers/internal/releases/gitea"
|
|
||||||
"github.com/webinstall/webi-installers/internal/releases/flutterdist"
|
"github.com/webinstall/webi-installers/internal/releases/flutterdist"
|
||||||
"github.com/webinstall/webi-installers/internal/releases/git"
|
"github.com/webinstall/webi-installers/internal/releases/git"
|
||||||
"github.com/webinstall/webi-installers/internal/releases/golang"
|
"github.com/webinstall/webi-installers/internal/releases/golang"
|
||||||
@@ -41,7 +40,7 @@ import (
|
|||||||
"github.com/webinstall/webi-installers/internal/releases/postgres"
|
"github.com/webinstall/webi-installers/internal/releases/postgres"
|
||||||
"github.com/webinstall/webi-installers/internal/releases/sass"
|
"github.com/webinstall/webi-installers/internal/releases/sass"
|
||||||
"github.com/webinstall/webi-installers/internal/releases/servicemandist"
|
"github.com/webinstall/webi-installers/internal/releases/servicemandist"
|
||||||
"github.com/webinstall/webi-installers/internal/releases/sttr"
|
sttrdist "github.com/webinstall/webi-installers/internal/releases/sttr"
|
||||||
"github.com/webinstall/webi-installers/internal/releases/uuidv7"
|
"github.com/webinstall/webi-installers/internal/releases/uuidv7"
|
||||||
"github.com/webinstall/webi-installers/internal/releases/watchexec"
|
"github.com/webinstall/webi-installers/internal/releases/watchexec"
|
||||||
"github.com/webinstall/webi-installers/internal/releases/xcaddy"
|
"github.com/webinstall/webi-installers/internal/releases/xcaddy"
|
||||||
@@ -98,7 +97,7 @@ func Package(pkg string, conf *installerconf.Conf, d *rawcache.Dir, gitTagDir *r
|
|||||||
assets = append(assets, gitAssets...)
|
assets = append(assets, gitAssets...)
|
||||||
}
|
}
|
||||||
|
|
||||||
TagVariants(pkg, assets)
|
TagVariants(pkg, conf.Variants, assets)
|
||||||
assets = expandUniversal(assets)
|
assets = expandUniversal(assets)
|
||||||
NormalizeVersions(pkg, assets)
|
NormalizeVersions(pkg, assets)
|
||||||
processGitTagHEAD(assets)
|
processGitTagHEAD(assets)
|
||||||
@@ -194,9 +193,19 @@ func NormalizeVersions(pkg string, assets []storage.Asset) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TagVariants applies package-specific variant tags to classified assets.
|
// TagVariants applies variant tags to classified assets.
|
||||||
// Each case delegates to a per-installer package under internal/releases/.
|
// conf variants (from releases.conf) are applied first: each variant is
|
||||||
func TagVariants(pkg string, assets []storage.Asset) {
|
// matched as a case-folded substring of the filename. Package-specific
|
||||||
|
// logic runs after for cases that require more than a substring check.
|
||||||
|
func TagVariants(pkg string, confVariants []string, assets []storage.Asset) {
|
||||||
|
for i := range assets {
|
||||||
|
lower := strings.ToLower(assets[i].Filename)
|
||||||
|
for _, v := range confVariants {
|
||||||
|
if strings.Contains(lower, strings.ToLower(v)) {
|
||||||
|
assets[i].Variants = append(assets[i].Variants, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
switch pkg {
|
switch pkg {
|
||||||
case "atomicparsley":
|
case "atomicparsley":
|
||||||
atomicparsleydist.TagVariants(assets)
|
atomicparsleydist.TagVariants(assets)
|
||||||
@@ -210,8 +219,6 @@ func TagVariants(pkg string, assets []storage.Asset) {
|
|||||||
flutterdist.TagVariants(assets)
|
flutterdist.TagVariants(assets)
|
||||||
case "git":
|
case "git":
|
||||||
gitdist.TagVariants(assets)
|
gitdist.TagVariants(assets)
|
||||||
case "gitea":
|
|
||||||
gitea.TagVariants(assets)
|
|
||||||
case "lsd":
|
case "lsd":
|
||||||
lsddist.TagVariants(assets)
|
lsddist.TagVariants(assets)
|
||||||
case "node":
|
case "node":
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ import (
|
|||||||
func TagVariants(assets []storage.Asset) {
|
func TagVariants(assets []storage.Asset) {
|
||||||
for i := range assets {
|
for i := range assets {
|
||||||
lower := strings.ToLower(assets[i].Filename)
|
lower := strings.ToLower(assets[i].Filename)
|
||||||
if strings.Contains(lower, "-profile") {
|
|
||||||
assets[i].Variants = append(assets[i].Variants, "profile")
|
|
||||||
}
|
|
||||||
if assets[i].Arch == "x86_64" {
|
if assets[i].Arch == "x86_64" {
|
||||||
if strings.Contains(lower, "-baseline") {
|
if strings.Contains(lower, "-baseline") {
|
||||||
// Baseline is plain x86_64 — strip the suffix from
|
// Baseline is plain x86_64 — strip the suffix from
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
// Package lsd provides variant tagging for lsd (LSDeluxe) releases.
|
// Package lsd provides variant tagging for lsd (LSDeluxe) releases.
|
||||||
//
|
//
|
||||||
// lsd publishes .deb packages and windows-msvc builds alongside
|
// lsd publishes .deb packages alongside the standard archives.
|
||||||
// the standard archives.
|
// msvc builds are excluded via releases.conf variants.
|
||||||
package lsddist
|
package lsddist
|
||||||
|
|
||||||
import (
|
import "github.com/webinstall/webi-installers/internal/storage"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/webinstall/webi-installers/internal/storage"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TagVariants tags lsd-specific build variants.
|
// TagVariants tags lsd-specific build variants.
|
||||||
func TagVariants(assets []storage.Asset) {
|
func TagVariants(assets []storage.Asset) {
|
||||||
@@ -16,8 +12,5 @@ func TagVariants(assets []storage.Asset) {
|
|||||||
if assets[i].Format == ".deb" {
|
if assets[i].Format == ".deb" {
|
||||||
assets[i].Variants = append(assets[i].Variants, "deb")
|
assets[i].Variants = append(assets[i].Variants, "deb")
|
||||||
}
|
}
|
||||||
if strings.Contains(strings.ToLower(assets[i].Filename), "-msvc") {
|
|
||||||
assets[i].Variants = append(assets[i].Variants, "msvc")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
// Package ollama provides variant tagging for Ollama releases.
|
// Package ollama provides variant tagging for Ollama releases.
|
||||||
//
|
|
||||||
// Ollama publishes GPU accelerator builds: -rocm (AMD), -jetpack5
|
|
||||||
// and -jetpack6 (NVIDIA Jetson).
|
|
||||||
package ollamadist
|
package ollamadist
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -11,14 +8,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// TagVariants tags ollama-specific build variants.
|
// TagVariants tags ollama-specific build variants.
|
||||||
|
// Suffix variants (mlx, rocm, jetpack5, jetpack6) are handled by the
|
||||||
|
// conf-driven loop in classifypkg.TagVariants; this handles the rest.
|
||||||
func TagVariants(assets []storage.Asset) {
|
func TagVariants(assets []storage.Asset) {
|
||||||
for i := range assets {
|
for i := range assets {
|
||||||
lower := strings.ToLower(assets[i].Filename)
|
|
||||||
for _, v := range []string{"rocm", "jetpack5", "jetpack6"} {
|
|
||||||
if strings.Contains(lower, "-"+v) {
|
|
||||||
assets[i].Variants = append(assets[i].Variants, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Ollama-darwin.zip (capital O) is the macOS .app bundle.
|
// Ollama-darwin.zip (capital O) is the macOS .app bundle.
|
||||||
// Installable by Go (extract .app), but not in legacy cache.
|
// Installable by Go (extract .app), but not in legacy cache.
|
||||||
if strings.HasPrefix(assets[i].Filename, "Ollama-") {
|
if strings.HasPrefix(assets[i].Filename, "Ollama-") {
|
||||||
|
|||||||
@@ -23,15 +23,8 @@ var winVersionRe = regexp.MustCompile(`(?i)-win(?:7|8|81|10|2008|2012|2016)`)
|
|||||||
func TagVariants(assets []storage.Asset) {
|
func TagVariants(assets []storage.Asset) {
|
||||||
for i := range assets {
|
for i := range assets {
|
||||||
lower := strings.ToLower(assets[i].Filename)
|
lower := strings.ToLower(assets[i].Filename)
|
||||||
switch {
|
if winVersionRe.MatchString(lower) {
|
||||||
case strings.Contains(lower, "-fxdependentwindesktop"):
|
|
||||||
assets[i].Variants = append(assets[i].Variants, "fxdependentWinDesktop")
|
|
||||||
case strings.Contains(lower, "-fxdependent"):
|
|
||||||
assets[i].Variants = append(assets[i].Variants, "fxdependent")
|
|
||||||
case winVersionRe.MatchString(lower):
|
|
||||||
assets[i].Variants = append(assets[i].Variants, "win-version-specific")
|
assets[i].Variants = append(assets[i].Variants, "win-version-specific")
|
||||||
case strings.HasSuffix(lower, ".appimage"):
|
|
||||||
assets[i].Variants = append(assets[i].Variants, "appimage")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
// Package sttr provides variant tagging for sttr releases.
|
// Package sttr provides variant tagging for sttr releases.
|
||||||
//
|
//
|
||||||
// sttr ships a darwin_all (universal macOS) archive alongside per-arch builds.
|
// sttr_Darwin_all.tar.gz is the only macOS release — a universal binary
|
||||||
// These universal archives have no arch in the filename — Go classifies them as
|
// with no arch token. Mark it universal2 so expandUniversal serves it
|
||||||
// os="darwin", arch="" which the Node builds-cacher rejects with FORMAT CHANGE
|
// to both arm64 and amd64 Mac users.
|
||||||
// (Node's classifier extracts a different arch from "all"). Production Node
|
|
||||||
// also stores these as os="", arch="" (unroutable).
|
|
||||||
//
|
|
||||||
// .sbom.json files are software bill-of-materials metadata — not installable
|
|
||||||
// archives. They pass through the format filter (ext="") but should not be
|
|
||||||
// served.
|
|
||||||
package sttrdist
|
package sttrdist
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -17,20 +11,11 @@ import (
|
|||||||
"github.com/webinstall/webi-installers/internal/storage"
|
"github.com/webinstall/webi-installers/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TagVariants tags sttr-specific build variants for exclusion from legacy export.
|
// TagVariants tags sttr-specific build variants.
|
||||||
func TagVariants(assets []storage.Asset) {
|
func TagVariants(assets []storage.Asset) {
|
||||||
for i := range assets {
|
for i := range assets {
|
||||||
lower := strings.ToLower(assets[i].Filename)
|
if strings.Contains(strings.ToLower(assets[i].Filename), "darwin_all") {
|
||||||
// darwin_all / Darwin_all: universal macOS archive with no arch info.
|
assets[i].Arch = "universal2"
|
||||||
// Node's classifier extracts a different result → FORMAT CHANGE.
|
|
||||||
// Production LIVE_cache has these as os="", arch="" (unroutable).
|
|
||||||
if strings.Contains(lower, "darwin_all") {
|
|
||||||
assets[i].Variants = append(assets[i].Variants, "universal-all")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// .sbom.json: software bill-of-materials, not an installable archive.
|
|
||||||
if strings.HasSuffix(lower, ".sbom.json") {
|
|
||||||
assets[i].Variants = append(assets[i].Variants, "metadata")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
github_releases = lsd-rs/lsd
|
github_releases = lsd-rs/lsd
|
||||||
|
variants = msvc
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
source = mariadbdist
|
|
||||||
asset_filter = galera
|
|
||||||
@@ -2,62 +2,67 @@
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
|
|
||||||
__init_ollama() {
|
__init_ollama() {
|
||||||
set -e
|
set -e
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
##################
|
##################
|
||||||
# Install ollama #
|
# Install ollama #
|
||||||
##################
|
##################
|
||||||
|
|
||||||
# Every package should define these 6 variables
|
# Every package should define these 6 variables
|
||||||
pkg_cmd_name="ollama"
|
pkg_cmd_name="ollama"
|
||||||
|
|
||||||
pkg_dst_cmd="${HOME}/.local/opt/ollama/bin/ollama"
|
pkg_dst_dir="${HOME}/.local/opt/ollama"
|
||||||
pkg_dst_dir="${HOME}/.local/opt/ollama"
|
pkg_dst_cmd="${HOME}/.local/bin/ollama"
|
||||||
pkg_dst="${pkg_dst_dir}"
|
|
||||||
|
|
||||||
pkg_src_cmd="${HOME}/.local/opt/ollama-v${WEBI_VERSION}/bin/ollama"
|
pkg_src_dir="${HOME}/.local/opt/ollama-v${WEBI_VERSION}"
|
||||||
pkg_src_dir="${HOME}/.local/opt/ollama-v${WEBI_VERSION}"
|
pkg_src_cmd="${HOME}/.local/opt/ollama-v${WEBI_VERSION}/bin/ollama"
|
||||||
pkg_src="${pkg_src_dir}"
|
|
||||||
|
|
||||||
# pkg_install must be defined by every package
|
my_os=$(uname -s)
|
||||||
pkg_install() {
|
if test "Darwin" = "${my_os}"; then
|
||||||
# ~/.local/opt/
|
pkg_dst_cmd="${HOME}/.local/bin/ollama"
|
||||||
mkdir -p "$(dirname "${pkg_src_dir}")"
|
pkg_src_cmd="${HOME}/.local/opt/ollama-v${WEBI_VERSION}/ollama"
|
||||||
|
fi
|
||||||
|
|
||||||
if test -d ./ollama-*/; then
|
pkg_dst="${pkg_dst_cmd}"
|
||||||
# the de facto way (in case it's supported in the future)
|
pkg_src="${pkg_src_cmd}"
|
||||||
# mv ./ollama-*/ ~/.local/opt/ollama-v3.27.0/
|
|
||||||
mv ./ollama-*/ "${pkg_src}"
|
|
||||||
elif test -d ./bin; then
|
|
||||||
# how linux is presently done
|
|
||||||
mkdir -p "${pkg_src_dir}"
|
|
||||||
mv ./bin "${pkg_src_dir}"
|
|
||||||
if test -f ./lib; then
|
|
||||||
mv ./lib "${pkg_src_dir}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# how macOS is presently done
|
|
||||||
mkdir -p "$(dirname "${pkg_src_cmd}")"
|
|
||||||
mv ./ollama-* "${pkg_src_cmd}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# remove previous location
|
# pkg_install must be defined by every package
|
||||||
if test -f ~/.local/bin/ollama; then
|
pkg_install() {
|
||||||
rm ~/.local/bin/ollama
|
if test -d ./bin; then
|
||||||
fi
|
# linux tar.zst: bin/ollama + lib/ollama/
|
||||||
}
|
mkdir -p "${pkg_src_dir}"
|
||||||
|
mv ./bin "${pkg_src_dir}/bin"
|
||||||
|
if test -d ./lib; then
|
||||||
|
mv ./lib "${pkg_src_dir}/lib"
|
||||||
|
fi
|
||||||
|
elif test -f ./ollama; then
|
||||||
|
# macOS tgz: flat — bare binary + dylibs/mlx backends in root
|
||||||
|
mkdir -p "${pkg_src_dir}"
|
||||||
|
mv ./* "${pkg_src_dir}/"
|
||||||
|
elif test -d ./Ollama.app; then
|
||||||
|
# macOS zip: install app bundle to /Applications
|
||||||
|
mv -f ./Ollama.app /Applications/Ollama.app
|
||||||
|
elif test -f ./ollama-*; then
|
||||||
|
# older bare binary format
|
||||||
|
mkdir -p "$(dirname "${pkg_src_cmd}")"
|
||||||
|
mv ./ollama-* "${pkg_src_cmd}"
|
||||||
|
else
|
||||||
|
echo "error: unrecognized ollama archive format" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
pkg_get_current_version() {
|
pkg_get_current_version() {
|
||||||
# 'ollama --version' has output in this format:
|
# 'ollama --version' has output in this format:
|
||||||
# ollama version is 0.3.10
|
# ollama version is 0.3.10
|
||||||
# This trims it down to just the version number:
|
# This trims it down to just the version number:
|
||||||
# 0.3.10
|
# 0.3.10
|
||||||
ollama --version 2> /dev/null |
|
ollama --version 2> /dev/null |
|
||||||
head -n 1 |
|
head -n 1 |
|
||||||
cut -d' ' -f4 |
|
cut -d' ' -f4 |
|
||||||
sed 's:^v::'
|
sed 's:^v::'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
__init_ollama
|
__init_ollama
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
github_releases = jmorganca/ollama
|
github_releases = jmorganca/ollama
|
||||||
variants = rocm jetpack5 jetpack6
|
variants = mlx rocm jetpack5 jetpack6
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
github_releases = powershell/powershell
|
github_releases = powershell/powershell
|
||||||
variants = fxdependent fxdependentWinDesktop
|
variants = fxdependent fxdependentWinDesktop appimage
|
||||||
|
|||||||
@@ -11,14 +11,22 @@ g_out="agents/tmp/${g_bin}"
|
|||||||
g_remote_bin="~/bin/${g_bin}"
|
g_remote_bin="~/bin/${g_bin}"
|
||||||
|
|
||||||
case "${g_host}" in
|
case "${g_host}" in
|
||||||
beta.webi.sh) g_remote_conf="~/srv/beta.webinstall.dev/installers/" ;;
|
beta.webi.sh) g_remote_conf="~/srv/beta.webinstall.dev/installers/" ;;
|
||||||
next.webi.sh) g_remote_conf="~/srv/next.webinstall.dev/installers/" ;;
|
next.webi.sh) g_remote_conf="~/srv/next.webinstall.dev/installers/" ;;
|
||||||
*) g_remote_conf="~/srv/webid/installers/" ;;
|
webi.sh) g_remote_conf="~/srv/webinstall.dev/installers/" ;;
|
||||||
|
*) g_remote_conf="~/srv/webinstall.dev/installers/" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
fn_build() {
|
fn_build() {
|
||||||
b_version="$(git describe --tags --always 2>/dev/null || echo '0.0.0-dev')"
|
b_tag="$(git describe --tags --abbrev=0 --match 'cmd/webicached/*' 2> /dev/null || echo 'cmd/webicached/v0.0.0')"
|
||||||
|
b_tag_ver="$(printf '%s' "${b_tag}" | sed 's:^cmd/webicached/::')"
|
||||||
|
b_count="$(git log --oneline "${b_tag}..HEAD" -- cmd/ internal/ 2> /dev/null | wc -l | tr -d ' \t')"
|
||||||
b_commit="$(git rev-parse --short HEAD)"
|
b_commit="$(git rev-parse --short HEAD)"
|
||||||
|
if test "${b_count}" -gt 0; then
|
||||||
|
b_version="${b_tag_ver}-${b_count}-g${b_commit}"
|
||||||
|
else
|
||||||
|
b_version="${b_tag_ver}"
|
||||||
|
fi
|
||||||
b_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
b_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
b_ldflags="-X main.version=${b_version} -X main.commit=${b_commit} -X main.date=${b_date}"
|
b_ldflags="-X main.version=${b_version} -X main.commit=${b_commit} -X main.date=${b_date}"
|
||||||
|
|
||||||
@@ -29,7 +37,7 @@ fn_build() {
|
|||||||
|
|
||||||
fn_deploy() {
|
fn_deploy() {
|
||||||
printf 'Stopping %s on %s...\n' "${g_bin}" "${g_host}"
|
printf 'Stopping %s on %s...\n' "${g_bin}" "${g_host}"
|
||||||
ssh "${g_host}" "~/.local/bin/serviceman stop ${g_bin}" 2>/dev/null || true
|
ssh "${g_host}" "~/.local/bin/serviceman stop ${g_bin}" 2> /dev/null || true
|
||||||
|
|
||||||
printf 'Uploading binary...\n'
|
printf 'Uploading binary...\n'
|
||||||
scp "${g_out}" "${g_host}:${g_remote_bin}"
|
scp "${g_out}" "${g_host}:${g_remote_bin}"
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ __init_sd() {
|
|||||||
pkg_install() {
|
pkg_install() {
|
||||||
# mv ./sd-*/sd "$pkg_src_cmd"
|
# mv ./sd-*/sd "$pkg_src_cmd"
|
||||||
if test -f sd-*; then
|
if test -f sd-*; then
|
||||||
# ~/.local/opt/sd-v0.99.9/bin
|
# old format: bare binary named sd-{triplet}
|
||||||
mkdir -p "$(dirname "$pkg_src_cmd")"
|
mkdir -p "$(dirname "$pkg_src_cmd")"
|
||||||
mv sd-* "$pkg_src_cmd"
|
mv sd-* "$pkg_src_cmd"
|
||||||
elif test -f sd-*/sd; then
|
elif test -f sd-*/sd; then
|
||||||
# ~/.local/opt/sd-v0.99.9/bin
|
# current format: sd-v{ver}-{triplet}/ directory
|
||||||
mkdir -p "$(dirname "$pkg_src_cmd")"
|
mkdir -p "$(dirname "$pkg_src_cmd")"
|
||||||
mv sd-*/sd "$pkg_src_cmd"
|
mv sd-*/sd "$pkg_src_cmd"
|
||||||
if test -f sd-*/sd.1; then
|
if test -f sd-*/sd.1; then
|
||||||
|
|||||||
118
sql-migrate/README.md
Normal file
118
sql-migrate/README.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
---
|
||||||
|
title: sql-migrate
|
||||||
|
homepage: https://github.com/therootcompany/golib/tree/main/cmd/sql-migrate
|
||||||
|
tagline: |
|
||||||
|
sql-migrate: A lightweight, feature-branch-friendly SQL migrator.
|
||||||
|
---
|
||||||
|
|
||||||
|
To update or switch versions, run `webi sql-migrate@stable` (or `@v2`, `@beta`,
|
||||||
|
etc).
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
These are the files that are created and/or modified with this installer:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.config/envman/PATH.env
|
||||||
|
~/.local/bin/sql-migrate
|
||||||
|
~/.local/opt/sql-migrate-VERSION/bin/sql-migrate
|
||||||
|
|
||||||
|
<PROJECT-DIR>/migrations.log
|
||||||
|
<PROJECT-DIR>/sql/migrations/<yyyy-mm-dd>-<number>_<name>.<up|down>.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cheat Sheet
|
||||||
|
|
||||||
|
> `sql-migrate` is a lightweight migration tool that gets out of your way - it
|
||||||
|
> works with your existing SQL tools and allows working with distinct sets of
|
||||||
|
> migrations, such as is typical in feature branches.
|
||||||
|
|
||||||
|
### How to use sql-migrate
|
||||||
|
|
||||||
|
Migration commands output a POSIX shell script which should be run with `sh`.
|
||||||
|
|
||||||
|
`init`, `sync`, `up`, `down`, `list`, `status`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Initialize a migration directory
|
||||||
|
sql-migrate -d ./sql/migrations/ init --sql-command psql
|
||||||
|
|
||||||
|
# Export ENVs for your database
|
||||||
|
export PG_URL='postgres://user:pass@example.com/dbname?sslmode=require&sslnegotiation=direct'
|
||||||
|
|
||||||
|
# Create a new migration (auto-generates up/down pair with incrementing number)
|
||||||
|
sql-migrate -d ./sql/migrations/ create add-users-table
|
||||||
|
|
||||||
|
# SELECT and log existing migrations
|
||||||
|
sql-migrate -d ./sql/migrations/ sync | sh
|
||||||
|
|
||||||
|
# Run all pending migrations up
|
||||||
|
sql-migrate -d ./sql/migrations/ up | sh
|
||||||
|
|
||||||
|
# Run 3 migrations up
|
||||||
|
sql-migrate -d ./sql/migrations/ up 3 | sh
|
||||||
|
|
||||||
|
# Run 1 migration down (roll back)
|
||||||
|
sql-migrate -d ./sql/migrations/ down | sh
|
||||||
|
|
||||||
|
# Run 2 migrations down
|
||||||
|
sql-migrate -d ./sql/migrations/ down 2 | sh
|
||||||
|
|
||||||
|
# List all migrations
|
||||||
|
sql-migrate -d ./sql/migrations/ list
|
||||||
|
|
||||||
|
# See which migrations have been applied
|
||||||
|
sql-migrate -d ./sql/migrations/ status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration directory layout
|
||||||
|
|
||||||
|
Migrations follow the naming format
|
||||||
|
`<yyyy-mm-dd>-<number>_<name>.<up|down>.sql`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sql/
|
||||||
|
├── migrations.log # transaction log (auto-managed)
|
||||||
|
└── migrations/
|
||||||
|
├── 0001-01-01-001000_init-migrations.up.sql # generated by 'init' (has config vars)
|
||||||
|
├── 2021-02-03-001000_init-app.up.sql
|
||||||
|
├── 2021-02-03-001000_init-app.down.sql
|
||||||
|
├── 2021-02-03-002000_add-products.up.sql
|
||||||
|
├── 2021-02-03-002000_add-products.down.sql
|
||||||
|
└── 2021-02-03-003000_add-customers.up.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
The initial `0001-01-01-001000_init-migrations.up.sql` migration contains
|
||||||
|
configuration variables:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- migrations_log: ./sql/migrations.log
|
||||||
|
-- sql_command: psql "$PG_URL" -v ON_ERROR_STOP=on --no-align --tuples-only --file %s
|
||||||
|
```
|
||||||
|
|
||||||
|
Environment variables by database:
|
||||||
|
|
||||||
|
- PostgreSQL: `PG_URL` (auth url), `PGOPTIONS` (to set `schema` and other
|
||||||
|
specific options)
|
||||||
|
- SQLite3: `SQLITE_PATH`
|
||||||
|
- SQL Server: `SQLCMDSERVER`, `SQLCMDDATABASE`, `SQLCMDUSER`, `SQLCMDPASSWORD`
|
||||||
|
- MySQL / MariaDB: `MY_CNF` (path to `my.cnf`, containing credentials)
|
||||||
|
|
||||||
|
### Database client compatibility
|
||||||
|
|
||||||
|
The `--sql-command` flag tells sql-migrate how to talk to your database:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sql-migrate -d ./sql/migrations/ init --sql-command psql
|
||||||
|
```
|
||||||
|
|
||||||
|
The following clients are known and will have the correct options applied:
|
||||||
|
|
||||||
|
- psql (PostgreSQL)
|
||||||
|
- sqlite3
|
||||||
|
- sqlcmd (mssql / Microsoft SQL Server)
|
||||||
|
- mariadb / mysql
|
||||||
|
|
||||||
|
Since the migrations run via shell commands, you can make `sql-migrate`
|
||||||
|
compatible with any SQL client by setting `sql_command` in
|
||||||
|
`migrations/0001-01-01-001000_init-migrations.up.sql`.
|
||||||
78
sql-migrate/SKILL.md
Normal file
78
sql-migrate/SKILL.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
name: sql-migrate
|
||||||
|
description:
|
||||||
|
Manage SQL database migrations as plain .sql files with a transaction log. Use
|
||||||
|
when asked about sql-migrate, database migrations, or how to set up migration
|
||||||
|
tooling.
|
||||||
|
---
|
||||||
|
|
||||||
|
# sql-migrate (CLI)
|
||||||
|
|
||||||
|
The agent skill is embedded in the help output.
|
||||||
|
|
||||||
|
Run `sql-migrate --help` use its output to guide usage decisions.
|
||||||
|
|
||||||
|
The `up`, `down`, and `sync` subcommands produce POSIX shell scripts - pipe them
|
||||||
|
to `sh` to run.
|
||||||
|
|
||||||
|
## Migration layout
|
||||||
|
|
||||||
|
Migrations follow the naming format
|
||||||
|
`<yyyy-mm-dd>-<number>_<name>.<up|down>.sql`:
|
||||||
|
|
||||||
|
```
|
||||||
|
sql/
|
||||||
|
├── migrations.log # transaction log (auto-managed)
|
||||||
|
└── migrations/
|
||||||
|
├── 0001-01-01-001000_init-migrations.up.sql # generated by 'init'
|
||||||
|
├── 2021-02-03-001000_init-app.up.sql
|
||||||
|
├── 2021-02-03-001000_init-app.down.sql
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
The initial migration file contains configuration variables:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- migrations_log: ./sql/migrations.log
|
||||||
|
-- sql_command: psql "$PG_URL" -v ON_ERROR_STOP=on --no-align --tuples-only --file %s
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration file structure
|
||||||
|
|
||||||
|
The migration files contain their own management, including a randomly-generated
|
||||||
|
id:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- change_me (up)
|
||||||
|
SELECT 'place your UP migration here';
|
||||||
|
|
||||||
|
-- leave this as the last line
|
||||||
|
INSERT INTO _migrations (name, id) VALUES ('2026-05-27-002000_change_me', 'e22295e5');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment variables by database
|
||||||
|
|
||||||
|
- PostgreSQL: `PG_URL` (auth URL), `PGOPTIONS` (set `schema` and other options)
|
||||||
|
- SQLite3: `SQLITE_PATH`
|
||||||
|
- SQL Server: `SQLCMDSERVER`, `SQLCMDDATABASE`, `SQLCMDUSER`, `SQLCMDPASSWORD`
|
||||||
|
- MySQL / MariaDB: `MY_CNF` (path to `my.cnf` containing credentials)
|
||||||
|
|
||||||
|
## SQL client extensibility
|
||||||
|
|
||||||
|
The `--sql-command` flag tells sql-migrate how to talk to your database. Known
|
||||||
|
clients (`psql`, `sqlite3`, `sqlcmd`, `mariadb`/`mysql`) have correct options
|
||||||
|
applied automatically. Since migrations run via shell commands, you can make
|
||||||
|
sql-migrate compatible with any SQL client by setting `sql_command` in the init
|
||||||
|
migration file.
|
||||||
|
|
||||||
|
# sqlmigrate (Go module)
|
||||||
|
|
||||||
|
See `go doc` for each independent module (golib is a monorepo):
|
||||||
|
|
||||||
|
- `github.com/therootcompany/golib/database/sqlmigrate/v2`
|
||||||
|
- `github.com/therootcompany/golib/database/sqlmigrate/pgmigrate`
|
||||||
|
- `github.com/therootcompany/golib/database/sqlmigrate/litemigrate`
|
||||||
|
- `github.com/therootcompany/golib/database/sqlmigrate/msmigrate`
|
||||||
|
- `github.com/therootcompany/golib/database/sqlmigrate/mymigrate`
|
||||||
|
|
||||||
|
DO NOT search the parent golib module.
|
||||||
47
sql-migrate/install.ps1
Normal file
47
sql-migrate/install.ps1
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
|
||||||
|
######################
|
||||||
|
# Install sql-migrate #
|
||||||
|
######################
|
||||||
|
|
||||||
|
$pkg_cmd_name = "sql-migrate"
|
||||||
|
|
||||||
|
$pkg_dst_cmd = "$Env:USERPROFILE\.local\bin\sql-migrate.exe"
|
||||||
|
$pkg_dst = "$pkg_dst_cmd"
|
||||||
|
|
||||||
|
$pkg_src_cmd = "$Env:USERPROFILE\.local\opt\sql-migrate-v$Env:WEBI_VERSION\bin\sql-migrate.exe"
|
||||||
|
$pkg_src_bin = "$Env:USERPROFILE\.local\opt\sql-migrate-v$Env:WEBI_VERSION\bin"
|
||||||
|
$pkg_src_dir = "$Env:USERPROFILE\.local\opt\sql-migrate-v$Env:WEBI_VERSION"
|
||||||
|
$pkg_src = "$pkg_src_cmd"
|
||||||
|
|
||||||
|
New-Item "$Env:USERPROFILE\Downloads\webi" -ItemType Directory -Force | Out-Null
|
||||||
|
$pkg_download = "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE"
|
||||||
|
|
||||||
|
# Fetch archive
|
||||||
|
if (!(Test-Path -Path "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE")) {
|
||||||
|
Write-Output "Downloading sql-migrate from $Env:WEBI_PKG_URL to $pkg_download"
|
||||||
|
& curl.exe -A "$Env:WEBI_UA" -fsSL "$Env:WEBI_PKG_URL" -o "$pkg_download.part"
|
||||||
|
& Move-Item "$pkg_download.part" "$pkg_download"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(Test-Path -Path "$pkg_src_cmd")) {
|
||||||
|
Write-Output "Installing sql-migrate"
|
||||||
|
|
||||||
|
Push-Location .local\tmp
|
||||||
|
|
||||||
|
Remove-Item -Path ".\sql-migrate-v*" -Recurse -ErrorAction Ignore
|
||||||
|
Remove-Item -Path ".\sql-migrate.exe" -Recurse -ErrorAction Ignore
|
||||||
|
|
||||||
|
Write-Output "Unpacking $pkg_download"
|
||||||
|
& tar xf "$pkg_download"
|
||||||
|
|
||||||
|
Write-Output "Install Location: $pkg_src_cmd"
|
||||||
|
New-Item "$pkg_src_bin" -ItemType Directory -Force | Out-Null
|
||||||
|
Move-Item -Path ".\sql-migrate.exe" -Destination "$pkg_src_bin"
|
||||||
|
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "Copying into '$pkg_dst_cmd' from '$pkg_src_cmd'"
|
||||||
|
Remove-Item -Path "$pkg_dst_cmd" -Recurse -ErrorAction Ignore | Out-Null
|
||||||
|
Copy-Item -Path "$pkg_src" -Destination "$pkg_dst" -Recurse
|
||||||
33
sql-migrate/install.sh
Normal file
33
sql-migrate/install.sh
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
|
||||||
|
set -e
|
||||||
|
set -u
|
||||||
|
|
||||||
|
__init_sql_migrate() {
|
||||||
|
pkg_cmd_name="sql-migrate"
|
||||||
|
|
||||||
|
pkg_dst_cmd="${HOME}/.local/bin/sql-migrate"
|
||||||
|
pkg_dst="${pkg_dst_cmd}"
|
||||||
|
|
||||||
|
pkg_src_cmd="${HOME}/.local/opt/sql-migrate-v${WEBI_VERSION}/bin/sql-migrate"
|
||||||
|
pkg_src_dir="${HOME}/.local/opt/sql-migrate-v${WEBI_VERSION}"
|
||||||
|
pkg_src="${pkg_src_cmd}"
|
||||||
|
|
||||||
|
pkg_install() {
|
||||||
|
pkg_src_bin=$(dirname "${pkg_src_cmd}")
|
||||||
|
mkdir -p "${pkg_src_bin}"
|
||||||
|
mv ./sql-migrate "${pkg_src_cmd}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# pkg_get_current_version is recommended, but (soon) not required
|
||||||
|
pkg_get_current_version() {
|
||||||
|
# 'sql-migrate version' has output in this format:
|
||||||
|
# sql-migrate v0.0.0-dev 0000000 (0001-01-01)
|
||||||
|
# This trims it down to just the version number:
|
||||||
|
# v0.0.0-dev
|
||||||
|
sql-migrate version 2> /dev/null | head -n 1 | cut -d ' ' -f 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__init_sql_migrate
|
||||||
2
sql-migrate/releases.conf
Normal file
2
sql-migrate/releases.conf
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github_releases = therootcompany/golib
|
||||||
|
tag_prefix = cmd/sql-migrate/v
|
||||||
@@ -1 +1,3 @@
|
|||||||
github_releases = abhimanyu003/sttr
|
github_releases = abhimanyu003/sttr
|
||||||
|
exclude = .sbom.json
|
||||||
|
variants = .pkg.tar.
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ __rmrf_local() {
|
|||||||
arc \
|
arc \
|
||||||
archiver \
|
archiver \
|
||||||
awless \
|
awless \
|
||||||
|
basecamp \
|
||||||
bat \
|
bat \
|
||||||
|
btop \
|
||||||
caddy \
|
caddy \
|
||||||
chromedriver \
|
chromedriver \
|
||||||
cmake \
|
cmake \
|
||||||
@@ -106,6 +108,7 @@ __rmrf_local() {
|
|||||||
arc \
|
arc \
|
||||||
archiver \
|
archiver \
|
||||||
awless \
|
awless \
|
||||||
|
basecamp \
|
||||||
bat \
|
bat \
|
||||||
caddy \
|
caddy \
|
||||||
chromedriver \
|
chromedriver \
|
||||||
@@ -205,6 +208,7 @@ __test() {
|
|||||||
arc \
|
arc \
|
||||||
archiver \
|
archiver \
|
||||||
awless \
|
awless \
|
||||||
|
basecamp \
|
||||||
bat \
|
bat \
|
||||||
caddy \
|
caddy \
|
||||||
chromedriver \
|
chromedriver \
|
||||||
|
|||||||
@@ -4,34 +4,34 @@ set -u
|
|||||||
|
|
||||||
__init_yq() {
|
__init_yq() {
|
||||||
|
|
||||||
pkg_cmd_name="yq"
|
pkg_cmd_name="yq"
|
||||||
|
|
||||||
pkg_dst_cmd="$HOME/.local/bin/yq"
|
pkg_dst_cmd="$HOME/.local/bin/yq"
|
||||||
pkg_dst="$pkg_dst_cmd"
|
pkg_dst="$pkg_dst_cmd"
|
||||||
|
|
||||||
pkg_src_cmd="$HOME/.local/opt/yq-v$WEBI_VERSION/bin/yq"
|
pkg_src_cmd="$HOME/.local/opt/yq-v$WEBI_VERSION/bin/yq"
|
||||||
pkg_src_dir="$HOME/.local/opt/yq-v$WEBI_VERSION"
|
pkg_src_dir="$HOME/.local/opt/yq-v$WEBI_VERSION"
|
||||||
pkg_src="$pkg_src_cmd"
|
pkg_src="$pkg_src_cmd"
|
||||||
|
|
||||||
pkg_install() {
|
pkg_install() {
|
||||||
mkdir -p "$(dirname "$pkg_src_cmd")"
|
mkdir -p "$(dirname "$pkg_src_cmd")"
|
||||||
# yq_linux_amd64.tar.gz contains:
|
# yq_linux_amd64.tar.gz contains:
|
||||||
# - yq_linux_amd64
|
# - yq_linux_amd64
|
||||||
# - yq.1
|
# - yq.1
|
||||||
# - install-man-page.sh
|
# - install-man-page.sh
|
||||||
if [ -e ./yq.1 ]; then
|
if [ -e ./yq.1 ]; then
|
||||||
mkdir -p ~/.local/share/man/man1
|
mkdir -p ~/.local/share/man/man1
|
||||||
mv ./yq.1 ~/.local/share/man/man1/
|
mv ./yq.1 ~/.local/share/man/man1/
|
||||||
fi
|
fi
|
||||||
mv ./"$pkg_cmd_name"* "$pkg_src_cmd"
|
mv ./"$pkg_cmd_name"* "$pkg_src_cmd"
|
||||||
chmod a+x "$pkg_src_cmd"
|
chmod a+x "$pkg_src_cmd"
|
||||||
}
|
}
|
||||||
|
|
||||||
pkg_get_current_version() {
|
pkg_get_current_version() {
|
||||||
yq --version 2> /dev/null |
|
yq --version 2> /dev/null |
|
||||||
head -n 1 |
|
head -n 1 |
|
||||||
cut -d ' ' -f 2
|
cut -d ' ' -f 2
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user