Compare commits

..

4 Commits

Author SHA1 Message Date
AJ ONeal
ab12d4fec3 ref: swap 'go' is canonical, 'golang' is alias 2023-08-09 23:04:39 +00:00
AJ ONeal
e7fef765b1 ref: make 'go' canonical installer, not just alias 2023-08-09 23:04:07 +00:00
AJ ONeal
d30860c163 feat: add golang-essentials alias 2023-08-09 22:57:38 +00:00
AJ ONeal
98d2cd3af0 ref!(go): separate go from go-essentials for 1.21 2023-08-09 22:57:38 +00:00
668 changed files with 7270 additions and 29754 deletions

View File

@@ -35,18 +35,15 @@ info, and doing a find and replace on a few file system path names.
```
2. Copy the example template and update with info from Official Releases:
<https://github.com/___CHANGE/ME___/releases>
```bash
rsync -av _example/ CHANGE-ME/
```
- [ ] update `CHANGE-ME/release.js` to use the official repo
- [ ] Learn how `CHANGE-ME` unpacks (i.e. as a single file? as a .tar.gz? as
a .tar.gz with a folder named CHANGE-ME?)
- [ ] find and replace to change the name
- [ ] update `CHANGE-ME/install.sh` (see `bat` and `jq` as examples)
- [ ] update `CHANGE-ME/install.ps1` (see `bat` and `jq` as examples)
3. Needs an updated tagline and cheat sheet
- [ ] update `CHANGE-ME/README.md`
- [ ] official URL

View File

@@ -1,37 +0,0 @@
name: Node.js CI
on:
push:
branches: ['main']
pull_request:
jobs:
build:
name: "Fmt, Lint, & Test"
runs-on: ubuntu-latest
strategy:
matrix:
node-version:
#- 20.x
- latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: "Webi: Install 'shfmt' and 'shellcheck', and update PATH"
run: |
sh ./_scripts/install-ci-deps
echo "${HOME}/.local/bin" >> $GITHUB_PATH
echo "${HOME}/.local/opt/pwsh" >> $GITHUB_PATH
- run: |
git submodule init
git submodule update
- run: shfmt --version
- run: shellcheck -V
- run: node --version
- run: npm run fmt
- run: npm ci
- run: npm run lint
- env:
GITHUB_TOKEN: ${{ github.token }}
run: npm run test

37
.gitignore vendored
View File

@@ -1,40 +1,7 @@
# generated artifacts
.env
node_modules
install-*.sh
install-*.bat
install-*.ps1
# Go build outputs (from go run/build in repo root)
/classify
/e2etest
/fetchraw
/inspect
/uaparse
/webicached
/zigtest
/distributables.csv
# local config
.env.*
*.env
.env
!example.env
# caches
_cache/
node_modules/
# temporary & backup files
.*.sw*
*.bak
*.bak.*
# agent session files
agents/
# other
.DS_Store
desktop.ini
.directory
LIVE_cache
/webid
bin/

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "_webi/build-classifier"]
path = _webi/build-classifier
url = https://github.com/webinstall/webi-build-classifier.git

View File

@@ -1 +0,0 @@
DELETEMEnode_modules/**/*

View File

@@ -1,7 +1,4 @@
{
"globals": {
"AbortController": false
},
"browser": true,
"node": true,
"esversion": 11,

View File

@@ -1,4 +0,0 @@
node_modules
jsconfig.json
package.json
package-lock.json

View File

@@ -1,9 +1,6 @@
{
"bracketSpacing": true,
"printWidth": 80,
"proseWrap": "always",
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"semi": true
"tabWidth": 2,
"singleQuote": true,
"proseWrap": "always"
}

6
.vimrc
View File

@@ -1,6 +0,0 @@
" Note: You must enable per-project .vimrc in your ~/.vimrc
" set secure
" set exrc
" PowerShell settings
let g:ale_powershell_psscriptanalyzer_exclusions = "PSAvoidUsingWriteHost,PSUseDeclaredVarsMoreThanAssignments"

492
AGENTS.md
View File

@@ -1,492 +0,0 @@
# Webi Installers — Agent Guide
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.
## Why Webi Exists
Webi makes tool installation trivially repeatable for people who aren't
sysadmins — freelance clients, junior devs, anyone who shouldn't have to care
about PATH, permissions, or platform differences. Three things matter:
1. **Install without friction.** No sudo, no manual PATH edits, no "necessary
but unimportant" steps leaking into the experience.
2. **Know where things are.** The Files section tells you exactly what got
created or modified. Nothing should be mysterious.
3. **Copy-paste recipes.** The cheat sheet is what you'd send someone less
experienced than yourself instead of a project's full README — scannable,
concrete, easy to reference by name.
## Quick Start: Adding a New Installer
1. Identify the **package type** (see [Categories](#categories) below)
2. Find an existing installer of the same type to use as a template
3. Create `<name>/releases.js`, `install.sh`, `install.ps1`, `README.md`
4. Test with the command in [Testing releases.js](#testing-releasesjs)
5. Run formatters before committing (see [Code Style](#code-style))
## Directory Layout
```
<package-name>/
README.md # YAML frontmatter + docs
releases.js # Fetches release metadata (Node.js)
install.sh # POSIX shell installer (macOS/Linux)
install.ps1 # PowerShell installer (Windows) — optional
```
Key infrastructure directories (do not modify without good reason):
- `_webi/` — bootstrap templates, `normalize.js` (auto-detects OS/arch/ext from
filenames)
- `_common/` — shared JS: `github.js`, `githubish.js`, `gitea.js`, `fetcher.js`
- `_example/` — canonical template for new packages
- `_examples/` — specialized templates (goreleaser, xz-compressed)
## Categories
Ref: <https://github.com/webinstall/webi-installers/issues/412>
| Type | Description | Template to copy |
| ----- | -------------------------------------- | ---------------- |
| `bin` | Single binary in tar/zip | `koji`, `delta` |
| `bin` | Single bare binary (no archive) | `arc`, `shfmt` |
| `bin` | Goreleaser-style archives | `keypairs` |
| 📦 | Self-contained package (bin/man/share) | `node`, `go` |
| 📂 | Multiple binaries/scripts | `pg` |
| 🔗 | Alias/redirect to another package | `ripgrep``rg` |
| 📝 | Bespoke / custom install | `rustlang` |
## releases.js
Fetches release metadata and returns a normalized object. Most packages use
GitHub releases:
```js
'use strict';
var github = require('../_common/github.js');
var owner = 'OWNER';
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));
})();
}
```
### Common release transformations
**Strip version prefix** (monorepo or tool-prefixed tags):
```js
// e.g. "tools/monorel/v0.6.5" → "v0.6.5"
rel.version = rel.version.replace(/^tools\/monorel\//, '');
// e.g. "cli-v1.2.3" → "v1.2.3"
rel.version = rel.version.replace(/^cli-/, '');
```
**Filter releases** (monorepo with multiple tools, or unwanted assets):
```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
node -e "
let Releases = require('./<name>/releases.js');
Releases.sample().then(function (all) {
console.log(JSON.stringify(all, null, 2));
});
"
```
Verify: versions are clean semver (`0.6.5` not `tools/monorel/v0.6.5`), OS/arch
detected correctly, download URLs resolve.
## install.sh
POSIX shell (`sh`, not bash). Always wrapped in a function:
```sh
#!/bin/sh
# shellcheck disable=SC2034
set -e
set -u
__init_pkgname() {
# These 6 variables are required
pkg_cmd_name="cmd"
pkg_dst_cmd="$HOME/.local/bin/cmd"
pkg_dst="$pkg_dst_cmd"
pkg_src_cmd="$HOME/.local/opt/cmd-v$WEBI_VERSION/bin/cmd"
pkg_src_dir="$HOME/.local/opt/cmd-v$WEBI_VERSION"
pkg_src="$pkg_src_cmd"
pkg_install() {
mkdir -p "$(dirname "$pkg_src_cmd")"
mv ./cmd "$pkg_src_cmd"
}
pkg_get_current_version() {
cmd --version 2> /dev/null | head -n 1 | cut -d' ' -f2
}
}
__init_pkgname
```
### Framework variables available in install.sh
Set by the webi bootstrap (`_webi/package-install.tpl.sh`):
| Variable | Example | Description |
| --------------- | ------------------- | --------------------- |
| `WEBI_VERSION` | `1.2.3` | Selected version |
| `WEBI_PKG_URL` | `https://...` | Download URL |
| `WEBI_PKG_FILE` | `foo-v1.2.3.tar.gz` | Download filename |
| `WEBI_OS` | `linux` | Detected OS |
| `WEBI_ARCH` | `amd64` | Detected architecture |
| `WEBI_EXT` | `tar.gz` | Archive extension |
| `WEBI_CHANNEL` | `stable` | Release channel |
| `PKG_NAME` | `foo` | Package name |
### Override functions
| Function | Purpose |
| --------------------------- | --------------------------------------------- |
| `pkg_install()` | **Required.** Move files to `$pkg_src` |
| `pkg_get_current_version()` | Parse installed version from command output |
| `pkg_post_install()` | Post-install setup (git config, shell config) |
| `pkg_done_message()` | Custom completion message |
| `pkg_link()` | Override default symlink behavior |
| `pkg_pre_install()` | Custom pre-install logic |
### Framework helper functions
| Function | Purpose |
| ------------------------ | ---------------------------------- |
| `webi_download()` | Download package if not cached |
| `webi_extract()` | Extract archive by extension |
| `webi_path_add <dir>` | Add to PATH via envman |
| `webi_link()` | Create versioned symlinks |
| `webi_check_installed()` | Check if version already installed |
### pkg_install patterns
**Bare binary in archive root:**
```sh
mv ./cmd "$pkg_src_cmd"
```
**Binary in a subdirectory (goreleaser-style `cmd-OS-arch/cmd`):**
```sh
mv ./cmd-*/cmd "$pkg_src_cmd"
```
**Flexible detection (handles multiple archive layouts):**
```sh
if test -f ./cmd; then
mv ./cmd "$pkg_src_cmd"
elif test -e ./cmd-*/cmd; then
mv ./cmd-*/cmd "$pkg_src_cmd"
elif test -e ./cmd-*; then
mv ./cmd-* "$pkg_src_cmd"
fi
```
## install.ps1
PowerShell for Windows. Uses `$Env:` variables. See `_example/install.ps1` for
the full template. Key differences from install.sh:
- Paths use backslashes, commands end in `.exe`
- `$Env:USERPROFILE` instead of `$HOME`
- `Test-Path`, `Move-Item`, `Copy-Item` instead of shell equivalents
- Downloads go to `$Env:USERPROFILE\Downloads\webi\`
- Temp work in `.local\tmp`, use `Push-Location`/`Pop-Location`
- Symlinks done via `Copy-Item` (not actual symlinks)
## README.md
````markdown
---
title: toolname
homepage: https://github.com/owner/repo
tagline: |
toolname: A short one-line description.
---
To update or switch versions, run `webi toolname@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/toolname
~/.local/opt/toolname-VERSION/bin/toolname
```
## Cheat Sheet
> `toolname` does X. Brief description.
### How to use toolname
```sh
toolname --example
```
````
Note: **Files goes above Cheat Sheet**, not inside it.
### Cheat Sheet tone and style
Webi cheat sheets are **opinionated quick-reference guides**, not comprehensive
documentation. Think "colleague's sticky note" — not the project's official
README.
The tool is the topic, but **the problem is the reason**. Cheat sheets are
organized around tasks the reader already wants to do — the tool is how they get
there. Headings reference the tool (the reader came to this page on purpose),
but the content solves the underlying problem completely:
- "How to reverse proxy to Node" (caddy knowledge, not just node)
- "How to run a Node app as a System Service" (serviceman knowledge)
- "How to Enable Secure Remote Postgres Access" (openssl, pg_hba.conf, systemd)
- "How to manually configure git to use delta" (gitconfig, not delta flags)
- "How to make fish the default shell in iTerm2" (iTerm2 knowledge, not fish)
The reader's question is "how do I do X?" and the cheat sheet answers it
completely — including configs, adjacent tools, and platform-specific
variations. A goreleaser cheat sheet teaches you goreleaser YAML. A postgres
cheat sheet teaches you pg_hba.conf, openssl certs, and systemd units.
Cheat sheets cross tool boundaries freely. Node's references caddy, serviceman,
setcap-netbind, GitHub Actions. Postgres references serviceman, openrc, launchd.
They link to each other's webi pages. The scope is "everything you need to
accomplish this task," not "everything this one binary does."
They show the actual files and configs that matter — not documentation _about_
configs, but the configs themselves, copy-pasteable, with inline comments
explaining the non-obvious parts.
**Guidelines:**
- **Show the 3-5 things someone will actually do**, with copy-pasteable
commands. Skip exhaustive flag lists and API docs.
- **Lead with practical integration.** Show the exact `git config` lines, the
exact hook script, the exact shell alias — don't just explain the feature and
leave wiring up to the reader.
- **Skip what they already know.** No need to re-explain what the tool is at
length — the tagline and one-liner blockquote handle that. Get to the
commands.
- **Prefer concrete over abstract.** Instead of "you can configure X via a
config file", show the config file contents.
## Shell Naming Conventions
**Variables:**
- `ALL_CAPS` — environment variables only (`PATH`, `HOME`, `WEBI_VERSION`)
- `b_varname` — block-scoped (inside a function, loop, or conditional)
- `g_varname` — global to the script (and sourced scripts)
- `a_varname` — function arguments
**Functions and commands:**
- `fn_name` — helper functions (anything other than the script's main/entry
function)
- `cmd_name` — command aliases, e.g. `cmd_curl='curl --fail-with-body -sSL'`
## Code Style
Requires `node`, `shfmt`, `pwsh`, and `pwsh-essentials` (install all via webi).
Run before committing:
```sh
npm run fmt # prettier (JS/MD) + shfmt (sh) + pwsh-fmt (ps1)
npm run lint # jshint + shellcheck + pwsh-lint
```
Commit messages: `feat(<pkg>): add installer`, `fix(<pkg>): update install.sh`,
`docs(<pkg>): add cheat sheet`.
## Naming Conventions
- The canonical package name is the **command name** you type: `go`, `node`,
`rg`
- The alternate/alias name is the project name: `golang`, `nodejs`, `ripgrep`
- Package directories are lowercase with hyphens
## Common Pitfalls
- **Monorepo releases**: The GitHub API returns ALL releases for the repo. You
must filter in `releases.js` and strip the tag prefix from the version.
- **No `--version` flag**: Some tools lack version introspection. Comment out
`pkg_get_current_version` — webi still works, it just can't skip reinstalls.
- **normalize.js auto-detection**: OS/arch/ext are guessed from download
filenames. If the tool uses non-standard naming, you may need to set `os`,
`arch`, or `ext` explicitly in `releases.js`.
- **Goreleaser archives**: Typically contain a bare binary at the archive root
(not nested in a directory). Use `mv ./cmd "$pkg_src_cmd"`.
---
## Go Cache Daemon
The Go pipeline (`cmd/webicached`) replaces the Node.js release-fetching code.
It reads `releases.conf` files, fetches upstream release metadata, classifies
build assets, and writes to `~/.cache/webi/legacy/` in the format the Node.js server expects.
### Canonical Vocabulary
The classifier MUST use exactly these strings. They match the production API.
**OS**: `macos` (NOT `darwin`), `linux`, `windows`, `freebsd`, `openbsd`,
`netbsd`, `dragonfly`, `aix`, `illumos`, `plan9`, `solaris`, `posix_2017`
**Arch** — exact equivalences:
- `amd64` (NOT `x86_64`), `x86` (NOT `i386`/`i686`/`386`)
- `arm64` (NOT `aarch64`)
- `armv7l` (NOT `armv7`), `armv6l` (NOT `armv6`)
- `mipsle` (NOT `mipsel`), `mips64le` (NOT `mips64el`)
**Arch** — compatibility downcasts:
- `armhf` → `armv7l`, `armv7a` → `armv7l`, `armel` → `arm`
**Arch** — other: `arm`, `ppc64le`, `ppc64`, `loong64`, `riscv64`, `s390x`,
`mips`, `mips64`
**Libc**: `none` (never empty), `gnu`, `musl`, `msvc`
**Ext**: `tar.gz`, `tar.xz`, `zip`, `exe`, `7z`, `pkg`, `msi`
(no leading dot; `exe` for bare binaries)
### releases.conf
Each package directory contains a `releases.conf` that tells the daemon where
to fetch releases. Format is `key = value`, one per line. `#` comments and
blank lines are ignored.
#### Source types (mutually exclusive — pick one)
```ini
# GitHub binary releases (most common)
github_releases = sharkdp/bat
# GitHub source tarballs (with optional git fallback)
github_sources = bnnanet/serviceman
git_url = https://github.com/bnnanet/serviceman.git
# Git tag enumeration (vim plugins, shell scripts — git_url alone)
git_url = https://github.com/tpope/vim-commentary.git
# Gitea (full URL required, or short form + base_url)
gitea_releases = https://git.rootprojects.org/root/pathman
# GitLab (defaults to gitlab.com)
gitlab_releases = owner/repo
# HashiCorp releases API
hashicorp_product = terraform
# Custom source (servicemandist, nodedist, zigdist, etc.)
source = nodedist
url = https://nodejs.org/download/release
```
#### Filtering, versioning, and platform
```ini
tag_prefix = bun- # monorepo: strip prefix from version
version_prefixes = jq- # strip from version string (space-separated)
asset_filter = MinGit # filename must contain this substring
exclude = busybox -src- -docs- # skip assets containing these (space-separated)
os = posix_2017 # restrict ALL versions to this OS (blanket)
alias_of = rg # mirrors another package's releases
```
#### Design rules
- `os` is a blanket tag on ALL versions. Only use for packages that are always
POSIX-only. For version-dependent OS tagging, use a custom `TagVariants` in
`internal/releases/{pkg}/variants.go`.
- `git_url` can be primary (gittag source when it's the only key) or secondary
fallback alongside `github_sources`/`gitea_sources`.
- Full URL forms accepted for github/gitea/gitlab (e.g.
`github_releases = https://github.com/sharkdp/bat`).
### Testing
Test tools: `cmd/e2etest` (pipeline comparison), `cmd/comparecache` (cache diff),
`cmd/inspect` (single-package debug). Run each with `--help` for usage.
### Classifier vs Per-Package Tagger
The general classifier (`internal/classify/`) handles patterns common across
many projects. It MUST NOT contain one-off logic for a single package.
Per-package taggers (`internal/releases/{pkg}/variants.go`) handle
project-specific knowledge. Read the existing taggers for conventions.
MUST: Derive arch/OS from concrete evidence — not blanket defaults.
MUST: New general classifier patterns must apply to 2-3+ packages.
### Deploying
```sh
./scripts/deploy-webicached.sh beta.webi.sh
./scripts/deploy-webicached.sh next.webi.sh
```
First-time setup on a new host uses `serviceman`:
```sh
serviceman add --name webicached \
--workdir ~/srv/webid/installers/ -- \
~/bin/webicached \
--envfile ~/srv/webid/.env.secret \
--conf ~/srv/webid/installers/ \
--raw ~/.cache/webi/raw
```

View File

@@ -10,6 +10,7 @@
- You'll be asked to make changes if you don't run the code formatters and
linters:
- Node / JavaScript:
- [prettier](https://webinstall.dev/prettier)
```sh
@@ -20,6 +21,7 @@
npm run lint
```
- Bash
- [shfmt](https://webinstall.dev/shfmt)
```sh
npm run shfmt

View File

@@ -98,10 +98,9 @@ You just fill in the blanks.
Just create an empty directory and run the tests until you get a good result.
```sh
git clone git@github.com:webinstall/webi-installers.git
pushd ./webi-installers/
git submodule update --init
npm clean-install
git clone git@github.com:webinstall/packages.git
pushd packages
npm install
```
```sh
@@ -145,8 +144,8 @@ It looks like this:
`releases.js`:
```js
module.exports = function () {
return github(null, owner, repo).then(function (all) {
module.exports = function (request) {
return github(request, owner, repo).then(function (all) {
// if you need to do something special, you can do it here
// ...
return all;

64
_common/brew.js Normal file
View File

@@ -0,0 +1,64 @@
'use strict';
/**
* Gets a releases from 'brew'.
*
* @param request
* @param {string} formula
* @returns {PromiseLike<any> | Promise<any>}
*/
function getAllReleases(request, formula) {
if (!formula) {
return Promise.reject('missing formula for brew');
}
return request({
url: 'https://formulae.brew.sh/api/formula/' + formula + '.json',
fail: true, // https://git.coolaj86.com/coolaj86/request.js/issues/2
json: true,
})
.then(failOnBadStatus)
.then(function (resp) {
var ver = resp.body.versions.stable;
var dl = (
resp.body.bottle.stable.files.high_sierra ||
resp.body.bottle.stable.files.catalina
).url.replace(new RegExp(ver.replace(/\./g, '\\.'), 'g'), '{{ v }}');
return [
{
version: ver,
download: dl.replace(/{{ v }}/g, ver),
},
].concat(
resp.body.versioned_formulae.map(function (f) {
var ver = f.replace(/.*@/, '');
return {
version: ver,
download: dl,
};
}),
);
})
.catch(function (err) {
console.error('Error fetching MariaDB versions (brew)');
console.error(err);
return [];
});
}
function failOnBadStatus(resp) {
if (resp.statusCode >= 400) {
var err = new Error('Non-successful status code: ' + resp.statusCode);
err.code = 'ESTATUS';
err.response = resp;
throw err;
}
return resp;
}
module.exports = getAllReleases;
if (module === require.main) {
getAllReleases(require('@root/request'), 'mariadb').then(function (all) {
console.info(JSON.stringify(all, null, 2));
});
}

39
_common/gitea.js Normal file
View File

@@ -0,0 +1,39 @@
'use strict';
var ghRelease = require('./github.js');
/**
* Gets the releases for 'ripgrep'. This function could be trimmed down and made
* for use with any github release.
*
* @param request
* @param {string} owner
* @param {string} repo
* @returns {PromiseLike<any> | Promise<any>}
*/
function getAllReleases(request, owner, repo, baseurl) {
if (!baseurl) {
return Promise.reject('missing baseurl');
}
return ghRelease(request, owner, repo, baseurl + '/api/v1').then(function (
all,
) {
return all;
});
}
module.exports = getAllReleases;
if (module === require.main) {
getAllReleases(
require('@root/request'),
'coolaj86',
'go-pathman',
'https://git.coolaj86.com',
).then(
//getAllReleases(require('@root/request'), 'root', 'serviceman', 'https://git.rootprojects.org').then(
function (all) {
console.info(JSON.stringify(all, null, 2));
},
);
}

133
_common/github-source.js Normal file
View File

@@ -0,0 +1,133 @@
'use strict';
require('dotenv').config();
/**
* Gets the releases for 'ripgrep'. This function could be trimmed down and made
* for use with any github release.
*
* @param request
* @param {string} owner
* @param {string} repo
* @returns {PromiseLike<any> | Promise<any>}
*/
async function getAllReleases(
request,
owner,
repo,
oses,
arches,
baseurl = 'https://api.github.com',
) {
if (!owner) {
return Promise.reject('missing owner for repo');
}
if (!repo) {
return Promise.reject('missing repo name');
}
let req = {
url: `${baseurl}/repos/${owner}/${repo}/releases`,
json: true,
};
// TODO I really don't like global config, find a way to do better
if (process.env.GITHUB_USERNAME) {
req.auth = {
user: process.env.GITHUB_USERNAME,
pass: process.env.GITHUB_TOKEN,
};
}
let resp = await request(req);
let gHubResp = resp.body;
let all = {
releases: [],
// TODO make this ':baseurl' + ':releasename'
download: '',
};
for (let release of gHubResp) {
// TODO tags aren't always semver / sensical
let tag = release['tag_name'];
let lts = /(\b|_)(lts)(\b|_)/.test(release['tag_name']);
let channel = 'stable';
if (release['prerelease']) {
channel = 'beta';
}
let date = release['published_at'] || '';
date = date.replace(/T.*/, '');
let urls = [release.tarball_url, release.zipball_url];
for (let url of urls) {
let resp = await request({
method: 'HEAD',
followRedirect: true,
followAllRedirects: true,
followOriginalHttpMethod: true,
url: url,
stream: true,
});
// Workaround for bug where method changes to GET
resp.destroy();
// content-disposition: attachment; filename=BeyondCodeBootcamp-DuckDNS.sh-v1.0.1-0-ga2f4bde.zip
let name = resp.headers['content-disposition'].replace(
/.*filename=([^;]+)(;|$)/,
'$1',
);
all.releases.push({
name: name,
version: tag,
lts: lts,
channel: channel,
date: date,
os: '', // will be guessed by download filename
arch: '', // will be guessed by download filename
ext: '', // will be normalized
download: resp.request.uri.href,
});
}
}
if (oses) {
return combinate(all, oses, arches);
}
return all;
}
function combinate(all, oses, arches) {
let releases = all.releases;
// ex: arches = ['amd64', 'arm64', 'armv7l', 'armv6l', 'x86'];
// ex: oses = ['macos', 'linux', 'bsd', 'posix'];
let combos = [];
for (let release of releases) {
for (let arch of arches) {
for (let os of oses) {
let combo = {
arch: arch,
os: os,
};
let rel = Object.assign({}, release, combo);
combos.push(rel);
}
}
}
all.releases = combos;
return all;
}
module.exports = getAllReleases;
if (module === require.main) {
getAllReleases(
require('@root/request'),
'BeyondCodeBootcamp',
'DuckDNS.sh',
).then(function (all) {
console.info(JSON.stringify(all, null, 2));
});
}

91
_common/github.js Normal file
View File

@@ -0,0 +1,91 @@
'use strict';
require('dotenv').config();
/**
* Gets the releases for 'ripgrep'. This function could be trimmed down and made
* for use with any github release.
*
* @param request
* @param {string} owner
* @param {string} repo
* @returns {PromiseLike<any> | Promise<any>}
*/
async function getAllReleases(
request,
owner,
repo,
baseurl = 'https://api.github.com',
) {
if (!owner) {
throw new Error('missing owner for repo');
}
if (!repo) {
throw new Error('missing repo name');
}
var req = {
url: `${baseurl}/repos/${owner}/${repo}/releases`,
json: true,
};
// TODO I really don't like global config, find a way to do better
if (process.env.GITHUB_USERNAME) {
req.auth = {
user: process.env.GITHUB_USERNAME,
pass: process.env.GITHUB_TOKEN,
};
}
let resp = await request(req);
if (!resp.ok) {
console.error('Bad Resp Headers:', resp.headers);
console.error('Bad Resp Body:', resp.body);
throw new Error('the elusive releases BOOGEYMAN strikes again');
}
let gHubResp = resp.body;
let all = {
releases: [],
// todo make this ':baseurl' + ':releasename'
download: '',
};
try {
gHubResp.forEach(transformReleases);
} catch (e) {
console.error(e.message);
console.error('Error Headers:', resp.headers);
console.error('Error Body:', resp.body);
throw e;
}
function transformReleases(release) {
release['assets'].forEach(function (asset) {
let name = asset['name'];
all.releases.push({
name: name,
version: release['tag_name'], // TODO tags aren't always semver / sensical
lts: /(\b|_)(lts)(\b|_)/.test(release['tag_name']),
channel: !release['prerelease'] ? 'stable' : 'beta',
date: (release['published_at'] || '').replace(/T.*/, ''),
os: '', // will be guessed by download filename
arch: '', // will be guessed by download filename
ext: '', // will be normalized
download: asset['browser_download_url'],
});
});
}
return all;
}
module.exports = getAllReleases;
if (module === require.main) {
getAllReleases(require('@root/request'), 'BurntSushi', 'ripgrep').then(
function (all) {
console.info(JSON.stringify(all, null, 2));
},
);
}

View File

@@ -23,7 +23,7 @@ install:
```text
~/.config/envman/PATH.env
~/.local/bin/foo
~/.local/opt/foo/
~/.local/opt/foo
```
## Cheat Sheet

View File

@@ -15,45 +15,47 @@ $pkg_src_bin = "$Env:USERPROFILE\.local\opt\foobar-v$Env:WEBI_VERSION\bin"
$pkg_src_dir = "$Env:USERPROFILE\.local\opt\foobar-v$Env:WEBI_VERSION"
$pkg_src = "$pkg_src_cmd"
New-Item "$Env:USERPROFILE\Downloads\webi" -ItemType Directory -Force | Out-Null
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 foobar from $Env:WEBI_PKG_URL to $pkg_download"
IF (!(Test-Path -Path "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE"))
{
echo "Downloading foobar 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"
& move "$pkg_download.part" "$pkg_download"
}
if (!(Test-Path -Path "$pkg_src_cmd")) {
Write-Output "Installing foobar"
IF (!(Test-Path -Path "$pkg_src_cmd"))
{
echo "Installing foobar"
# TODO: create package-specific temp directory
# Enter tmp
Push-Location .local\tmp
pushd .local\tmp
# Remove any leftover tmp cruft
Remove-Item -Path ".\foobar-v*" -Recurse -ErrorAction Ignore
Remove-Item -Path ".\foo.exe" -Recurse -ErrorAction Ignore
# Remove any leftover tmp cruft
Remove-Item -Path ".\foobar-v*" -Recurse -ErrorAction Ignore
Remove-Item -Path ".\foo.exe" -Recurse -ErrorAction Ignore
# NOTE: DELETE THIS COMMENT IF NOT USED
# Move single binary into root of temporary folder
#& move "$pkg_download" "foo.exe"
# NOTE: DELETE THIS COMMENT IF NOT USED
# Move single binary into root of temporary folder
#& move "$pkg_download" "foo.exe"
# Unpack archive file into this temporary directory
# Windows BSD-tar handles zip. Imagine that.
Write-Output "Unpacking $pkg_download"
& tar xf "$pkg_download"
# Unpack archive file into this temporary directory
# Windows BSD-tar handles zip. Imagine that.
echo "Unpacking $pkg_download"
& tar xf "$pkg_download"
# Settle unpacked archive into place
Write-Output "Install Location: $pkg_src_cmd"
New-Item "$pkg_src_bin" -ItemType Directory -Force | Out-Null
Move-Item -Path ".\foobar-*\foo.exe" -Destination "$pkg_src_bin"
# Settle unpacked archive into place
echo "Install Location: $pkg_src_cmd"
New-Item "$pkg_src_bin" -ItemType Directory -Force | out-null
Move-Item -Path ".\foobar-*\foo.exe" -Destination "$pkg_src_bin"
# Exit tmp
Pop-Location
popd
}
Write-Output "Copying into '$pkg_dst_cmd' from '$pkg_src_cmd'"
Remove-Item -Path "$pkg_dst_cmd" -Recurse -ErrorAction Ignore | Out-Null
echo "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

View File

@@ -1,3 +0,0 @@
# Example releases.conf — uses ripgrep as a sample project.
# Copy this file into your package directory and adjust.
github_releases = BurntSushi/ripgrep

28
_example/releases.js Normal file
View File

@@ -0,0 +1,28 @@
'use strict';
var github = require('../_common/github.js');
var owner = 'BurntSushi';
var repo = 'ripgrep';
/******************************************************************************/
/** Note: Delete this Comment! **/
/** **/
/** Need a an example that filters out miscellaneous release files? **/
/** See `deno`, `gitea`, or `caddy` **/
/** **/
/******************************************************************************/
module.exports = function (request) {
return github(request, owner, repo).then(function (all) {
return all;
});
};
if (module === require.main) {
module.exports(require('@root/request')).then(function (all) {
all = require('../_webi/normalize.js')(all);
// just select the first 5 for demonstration
all.releases = all.releases.slice(0, 5);
console.info(JSON.stringify(all, null, 2));
});
}

View File

@@ -1,6 +1,9 @@
'use strict';
var pkg = require('../package.json');
var spawn = require('child_process').spawn;
var os = require('os');
var path = require('path');
function spawner(args) {
return new Promise(function (resolve, reject) {

View File

@@ -4,6 +4,7 @@
//var pkg = require('../package.json');
var os = require('os');
//var request = require('@root/request');
//var promisify = require('util').promisify;
//var exec = promisify(require('child_process').exec);
var exec = require('child_process').exec;

View File

@@ -1,57 +0,0 @@
#!/usr/bin/env node
'use strict';
var Fs = require('node:fs/promises');
var Path = require('node:path');
async function copyScripts() {
var pkgDir = Path.join(__dirname, '..');
var scriptsDir = Path.join(pkgDir, '_scripts');
var gitFile = Path.join(pkgDir, '.git');
// if this is a submodule, '.git' will be a file with a
// path to the actual git module directory
var gitDir = await Fs.readFile(gitFile, 'utf8')
.catch(function (e) {
// console.error(e);
//return 'gitdir: ../.git/modules/installers';
return 'gitdir: ./.git';
})
.then(function (str) {
var parts = str.split(': ');
str = parts[1];
str = str.trim();
return Path.resolve(pkgDir, str);
});
var gitHooksDir = Path.join(gitDir, 'hooks');
var src = Path.join(scriptsDir, 'git-hooks-pre-commit.js');
var dst = Path.join(gitHooksDir, 'pre-commit');
console.info(`[git-hooks] Checking for pre-commit hooks...`);
var relSrc = Path.relative(pkgDir, src);
var relDst = Path.relative(pkgDir, dst);
await Fs.access(dst)
.then(function () {
console.info(`[git-hooks] Found ${relDst}`);
})
.catch(async function (e) {
// ignore e
await Fs.mkdir(gitHooksDir, { recursive: true });
await Fs.copyFile(src, dst);
await Fs.chmod(dst, 0o755);
console.info(`[git-hooks] Found template ${relSrc}`);
console.info(`[git-hooks] Initialized ${relDst}`);
});
}
copyScripts()
.then(function () {
process.exit(0);
})
.catch(function (e) {
console.error(e);
process.exit(1);
});

View File

@@ -1,7 +0,0 @@
#!/bin/sh
set -e
set -u
npm run fmt
npm run lint
npm run test

View File

@@ -1,12 +0,0 @@
#!/usr/bin/env node
'use strict';
var Process = require('child_process');
var procOpts = { stdio: 'inherit' };
var commands = ['npm run fmt', 'npm run lint', 'npm run test'];
for (let cmd of commands) {
console.info(`[pre-commit] exec: ${cmd}`);
Process.execSync(cmd, procOpts);
}

View File

@@ -1,15 +0,0 @@
#!/bin/sh
set -e
set -u
# Install 'shfmt 3.7.x'
curl -fsS https://webi.sh/shfmt@3.7 | sh
# Install 'shellcheck v0.9.x'
curl -fsS https://webi.sh/shellcheck@0.9 | sh
# Install 'pwsh (PowerShell Core) v7.x'
curl -fsS https://webi.sh/pwsh@7 | sh
# shellcheck disable=SC1090
. ~/.config/envman/PATH.env
pwsh -Command "Install-Module -Name PSScriptAnalyzer -Scope CurrentUser -AllowClobber"

View File

@@ -1,27 +0,0 @@
#!/bin/sh
set -e
set -u
echo "Formatting */*.ps1 ... "
for my_ps1 in */*.ps1; do
my_dir="$(
dirname "${my_ps1}"
)"
if test -L "${my_ps1}" ||
test -L "${my_dir}" ||
! test -f "${my_ps1}" ||
! test -d "${my_dir}"; then
printf ' SKIP %s (non-regular file or parent directory)\n' "${my_ps1}"
continue
fi
printf " %s" "${my_ps1}"
# -Settings ./Settings/CodeFormatting.psd1
my_new_file="$(
pwsh -Command "Invoke-Formatter -ScriptDefinition (Get-Content -Path '${my_ps1}' -Raw)"
)"
printf '%s\n' "${my_new_file}" > "${my_ps1}"
printf '\n'
done

View File

@@ -1,40 +0,0 @@
#!/usr/bin/env pwsh
$ErrorActionPreference = "Stop"
Write-Host "Formatting */*.ps1 ... "
$my_cwd = Get-Location
$my_dirs = Get-ChildItem -Directory
$my_status = 0
foreach ($my_dir in $my_dirs) {
$my_files = Get-ChildItem -Path $my_dir.FullName -File -Filter *.ps1
foreach ($my_file in $my_files) {
$my_ps1 = [System.IO.Path]::GetRelativePath($my_cwd, $my_file.FullName)
$my_dir = [System.IO.Path]::GetDirectoryName($my_file.FullName)
if (-not (Test-Path -PathType Leaf -Path $my_ps1) -or
-not (Test-Path -PathType Container -Path $my_dir)) {
Write-Host (" SKIP {0} (non-regular file or parent directory)" -f $my_ps1)
continue
}
Write-Host (" {0}" -f $my_ps1)
$text = Get-Content -Path $my_ps1 -Raw
$my_new_file = Invoke-Formatter -ScriptDefinition $text
$my_new_file = $my_new_file.Trim()
# note: trailing newline is added back on write
$my_new_file | Set-Content -Path $my_ps1
$my_new_file = $my_new_file + "`n"
if ($text -ne $my_new_file) {
$my_status = 1
}
}
}
exit $my_status

View File

@@ -1,36 +0,0 @@
#!/bin/sh
set -e
set -u
echo "Formatting */*.ps1 ..."
for my_ps1 in */*.ps1; do
my_dir="$(
dirname "${my_ps1}"
)"
if test -L "${my_ps1}" ||
test -L "${my_dir}" ||
! test -f "${my_ps1}" ||
! test -d "${my_dir}"; then
printf ' SKIP %s (non-regular file or parent directory)\n' "${my_ps1}"
continue
fi
printf " %s" "${my_ps1}"
# -Settings ./PSScriptAnalyzerSettings.psd1
pwsh -Command "Invoke-ScriptAnalyzer -Fix -ExcludeRule PSAvoidUsingWriteHost,PSUseDeclaredVarsMoreThanAssignment -Path '$my_ps1'"
#
# fmt MUST happen after lint due to Byte-Order Marker (BOM) issues
# See https://github.com/PowerShell/PSScriptAnalyzer/issues/1743
#
# -Settings ./Settings/CodeFormatting.psd1
my_new_file="$(
pwsh -Command "Invoke-Formatter -ScriptDefinition (Get-Content -Path '${my_ps1}' -Raw)"
)"
printf '%s\n' "${my_new_file}" > "${my_ps1}"
printf '\n'
done

View File

@@ -1,48 +0,0 @@
#!/usr/bin/env pwsh
$ErrorActionPreference = "Stop"
Write-Host "Linting */*.ps1 ... "
$my_cwd = Get-Location
$my_dirs = Get-ChildItem -Directory
$my_status = 0
foreach ($my_dir in $my_dirs) {
$my_files = Get-ChildItem -Path $my_dir.FullName -File -Filter *.ps1
foreach ($my_file in $my_files) {
$my_ps1 = [System.IO.Path]::GetRelativePath($my_cwd, $my_file.FullName)
$my_dir = [System.IO.Path]::GetDirectoryName($my_file.FullName)
if (-not (Test-Path -PathType Leaf -Path $my_ps1) -or
-not (Test-Path -PathType Container -Path $my_dir)) {
Write-Host (" SKIP {0} (non-regular file or parent directory)" -f $my_ps1)
continue
}
Write-Host (" {0}" -f $my_ps1)
$my_old_file = (Get-Content -Path $my_ps1 -Raw)
Invoke-ScriptAnalyzer -Fix -ExcludeRule PSAvoidUsingWriteHost, PSUseDeclaredVarsMoreThanAssignment -Path $my_ps1
#
# fmt MUST happen after lint due to Byte-Order Marker (BOM) issues
# See https://github.com/PowerShell/PSScriptAnalyzer/issues/1743
#
$my_fixed_file = (Get-Content -Path $my_ps1 -Raw)
$my_new_file = Invoke-Formatter -ScriptDefinition $my_fixed_file
$my_new_file = $my_new_file.Trim()
# note: trailing newline is added back on write
$my_new_file | Set-Content -Path $my_ps1
$my_new_file = $my_new_file + "`n"
if ($my_old_file -ne $my_new_file) {
$my_status = 1
}
}
}
exit $my_status

View File

@@ -1,32 +0,0 @@
#!/usr/bin/env node
'use strict';
var Cmd = require('node:child_process');
var Os = require('node:os');
var procOpts = { stdio: 'inherit' };
var commands = ['shfmt --version', 'shellcheck --version'];
for (let cmd of commands) {
console.info(`[tooling-init] checking for '${cmd}':`);
try {
Cmd.execSync(cmd, procOpts);
} catch (e) {
// ignore e
printInstallHelp(cmd);
process.exit(1);
}
}
function printInstallHelp(cmd) {
console.error('');
console.error('ERROR');
console.error(` could not run '${cmd}'`);
console.error('POSSIBLE FIX');
if (/^win/i.test(Os.platform())) {
console.error(` curl.exe https://webi.ms/${cmd} | powershell`);
} else {
console.error(` curl -fsS https://webi.sh/${cmd} | sh`);
}
console.error('');
}

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env pwsh
if (!(Test-Path -Path "$Env:USERPROFILE\.vim\pack\plugins\start")) {
New-Item -Path "$Env:USERPROFILE\.vim\pack\plugins\start" -ItemType Directory -Force | Out-Null
IF (!(Test-Path -Path "$Env:USERPROFILE\.vim\pack\plugins\start")) {
New-Item -Path "$Env:USERPROFILE\.vim\pack\plugins\start" -ItemType Directory -Force | out-null
}
Remove-Item -Path "$Env:USERPROFILE\.vim\pack\plugins\start\example" -Recurse -ErrorAction Ignore
& git clone --depth=1 https://github.com/CHANGEME/example.git "$Env:USERPROFILE\.vim\pack\plugins\start\example"

28
_webi/bootstrap.bat Normal file
View File

@@ -0,0 +1,28 @@
setlocal
@echo off
pushd "%userprofile%" || goto :error
IF NOT EXIST .local (
mkdir .local || goto :error
)
IF NOT EXIST .local\bin (
mkdir .local\bin || goto :error
)
rem {{ baseurl }}
rem {{ version }}
pushd .local\bin || goto :error
if NOT EXIST webi.bat (
rem without SilentlyContinue this is SLOOOOOOOOOOOOOOOW!
powershell $ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest https://webinstall.dev/packages/_webi/webi.bat -OutFile webi.bat || goto :error
)
call .\webi {{ exename }} || goto :error
rem pathman add "%userprofile%\.local\bin" >nul 2>&1 || goto :error
pathman add "%userprofile%\.local\bin" || goto :error
popd || goto :error
popd
goto :EOF
:error
echo Failed with error #%errorlevel%.
exit /b %errorlevel%

View File

@@ -4,9 +4,9 @@
# <h1>Cheat Sheet at CHEATSHEET_URL</h1>
# <meta http-equiv="refresh" content="3; URL='CHEATSHEET_URL'" />
############################################################
New-Item -Path "$Env:USERPROFILE\Downloads\webi" -ItemType Directory -Force | Out-Null
New-Item -Path "$Env:USERPROFILE\.local\bin" -ItemType Directory -Force | Out-Null
if ($null -eq $Env:WEBI_HOST -or $Env:WEBI_HOST -eq "") { $Env:WEBI_HOST = "https://webinstall.dev" }
curl.exe -s -A "windows" "$Env:WEBI_HOST/packages/webi/webi-pwsh.ps1" -o "$Env:USERPROFILE\.local\bin\webi-pwsh.ps1"
New-Item -Path "$Env:USERPROFILE\Downloads\webi" -ItemType Directory -Force | out-null
New-Item -Path "$Env:USERPROFILE\.local\bin" -ItemType Directory -Force | out-null
IF ($Env:WEBI_HOST -eq $null -or $Env:WEBI_HOST -eq "") { $Env:WEBI_HOST = "https://webinstall.dev" }
curl.exe -s -A "windows" "$Env:WEBI_HOST/packages/_webi/webi-pwsh.ps1" -o "$Env:USERPROFILE\.local\bin\webi-pwsh.ps1"
Set-ExecutionPolicy -Scope Process Bypass
& "$Env:USERPROFILE\.local\bin\webi-pwsh.ps1" "{{ exename }}"

234
_webi/bootstrap.sh Normal file
View File

@@ -0,0 +1,234 @@
#!/bin/sh
#<pre>
############################################################
# <h1>Cheat Sheet at CHEATSHEET_URL</h1>
# <meta http-equiv="refresh" content="3; URL='CHEATSHEET_URL'" />
############################################################
#set -x
__install_webi() {
#WEBI_PKG=
#WEBI_HOST=https://webinstall.dev
export WEBI_HOST
echo ""
printf "Thanks for using webi to install '\e[32m%s\e[0m' on '\e[31m%s/%s\e[0m'.\n" "${WEBI_PKG-}" "$(uname -s)/$(uname -r)" "$(uname -m)"
echo "Have a problem? Experience a bug? Please let us know:"
echo " https://github.com/webinstall/webi-installers/issues"
echo ""
printf "\e[31mLovin'\e[0m it? Say thanks with a \e[34mStar on GitHub\e[0m:\n"
printf " \e[32mhttps://github.com/webinstall/webi-installers\e[0m\n"
echo ""
WEBI_WELCOME=true
export WEBI_WELCOME
set -e
set -u
mkdir -p "$HOME/.local/bin"
cat << EOF > "$HOME/.local/bin/webi"
#!/bin/sh
set -e
set -u
#set -x
__webi_main() {
export WEBI_TIMESTAMP="\$(date +%F_%H-%M-%S)"
export _webi_tmp="\${_webi_tmp:-\$(mktemp -d -t webi-\$WEBI_TIMESTAMP.XXXXXXXX)}"
if [ -n "\${_WEBI_PARENT:-}" ]; then
export _WEBI_CHILD=true
else
export _WEBI_CHILD=
fi
export _WEBI_PARENT=true
##
## Detect acceptable package formats
##
my_ext=""
set +e
# NOTE: the order here is least favorable to most favorable
if [ -n "\$(command -v pkgutil)" ]; then
my_ext="pkg,\$my_ext"
fi
# disable this check for the sake of building the macOS installer on Linux
#if [ -n "\$(command -v diskutil)" ]; then
# note: could also detect via hdiutil
my_ext="dmg,\$my_ext"
#fi
if [ -n "\$(command -v git)" ]; then
my_ext="git,\$my_ext"
fi
if [ -n "\$(command -v unxz)" ]; then
my_ext="xz,\$my_ext"
fi
if [ -n "\$(command -v unzip)" ]; then
my_ext="zip,\$my_ext"
fi
# for mac/linux 'exe' refers to the uncompressed binary without extension
my_ext="exe,\$my_ext"
if [ -n "\$(command -v tar)" ]; then
my_ext="tar,\$my_ext"
fi
my_ext="\$(echo "\$my_ext" | sed 's/,$//')" # nix trailing comma
set -e
##
## Detect http client
##
set +e
export WEBI_CURL="\$(command -v curl)"
export WEBI_WGET="\$(command -v wget)"
set -e
my_libc=''
if ldd /bin/ls 2> /dev/null | grep -q 'musl' 2> /dev/null; then
my_libc=' musl-native'
fi
export WEBI_HOST="\${WEBI_HOST:-https://webinstall.dev}"
export WEBI_UA="\$(uname -s)/\$(uname -r) \$(uname -m)/unknown\${my_libc}"
webinstall() {
my_package="\${1:-}"
if [ -z "\$my_package" ]; then
echo >&2 "Usage: webi <package>@<version> ..."
echo >&2 "Example: webi node@lts rg"
exit 1
fi
export WEBI_BOOT="\$(mktemp -d -t "\$my_package-bootstrap.\$WEBI_TIMESTAMP.XXXXXXXX")"
my_installer_url="\$WEBI_HOST/api/installers/\$my_package.sh?formats=\$my_ext"
if [ -n "\$WEBI_CURL" ]; then
if ! curl -fsSL "\$my_installer_url" -H "User-Agent: curl \$WEBI_UA" \\
-o "\$WEBI_BOOT/\$my_package-bootstrap.sh"; then
echo >&2 "error fetching '\$my_installer_url'"
exit 1
fi
else
if ! wget -q "\$my_installer_url" --user-agent="wget \$WEBI_UA" \\
-O "\$WEBI_BOOT/\$my_package-bootstrap.sh"; then
echo >&2 "error fetching '\$my_installer_url'"
exit 1
fi
fi
(
cd "\$WEBI_BOOT"
sh "\$my_package-bootstrap.sh"
)
rm -rf "\$WEBI_BOOT"
}
show_path_updates() {
if test -z "\${_WEBI_CHILD}"; then
if test -f "\$_webi_tmp/.PATH.env"; then
my_paths=\$(sort -u < "\$_webi_tmp/.PATH.env")
if test -n "\$my_paths"; then
printf 'PATH.env updated with:\\n'
printf "%s\\n" "\$my_paths"
printf '\\n'
printf "\\e[31mTO FINISH\\e[0m: copy, paste & run the following command:\\n"
printf "\\n"
printf " \\e[34msource ~/.config/envman/PATH.env\\e[0m\\n"
printf " (newly opened terminal windows will update automatically)\\n"
fi
rm -f "\$_webi_tmp/.PATH.env"
fi
fi
}
version() {
my_version=v1.1.15
printf "\\e[31mwebi\\e[32m %s\\e[0m Copyright 2020+ AJ ONeal\\n" "\${my_version}"
printf " \\e[34mhttps://webinstall.dev/webi\\e[0m\\n"
}
# show help if no params given or help flags are used
usage() {
echo ""
version
echo ""
printf "\\e[1mSUMMARY\\e[0m\\n"
echo " Webi is the best way to install the modern developer tools you love."
echo " It's fast, easy-to-remember, and conflict free."
echo ""
printf "\\e[1mUSAGE\\e[0m\\n"
echo " webi <thing1>[@version] [thing2] ..."
echo ""
printf "\\e[1mUNINSTALL\\e[0m\\n"
echo " Almost everything that is installed with webi is scoped to"
echo " ~/.local/opt/<thing1>, so you can remove it like so:"
echo ""
echo " rm -rf ~/.local/opt/<thing1>"
echo " rm -f ~/.local/bin/<thing1>"
echo ""
echo " Some packages have special uninstall instructions, check"
echo " https://webinstall.dev/<thing1> to be sure."
echo ""
printf "\\e[1mFAQ\\e[0m\\n"
printf " See \\e[34mhttps://webinstall.dev/faq\\e[0m\\n"
echo ""
printf "\\e[1mALWAYS REMEMBER\\e[0m\\n"
echo " Friends don't let friends use brew for simple, modern tools that don't need it."
echo " (and certainly not apt either **shudder**)"
echo ""
}
if [ \$# -eq 0 ] || echo "\$1" | grep -q -E '^(-V|--version|version)$'; then
version
exit 0
fi
if echo "\$1" | grep -q -E '^(-h|--help|help)$'; then
usage "\$@"
exit 0
fi
for pkgname in "\$@"; do
webinstall "\$pkgname"
done
show_path_updates
}
__webi_main "\$@"
EOF
chmod a+x "$HOME/.local/bin/webi"
if [ -n "${WEBI_PKG-}" ]; then
"$HOME/.local/bin/webi" "${WEBI_PKG}"
else
echo ""
echo "Hmm... no WEBI_PKG was specified. This is probably an error in the script."
echo ""
echo "Please open an issue with this information: Package '${WEBI_PKG-}' on '$(uname -s)/$(uname -r) $(uname -m)'"
echo " https://github.com/webinstall/packages/issues"
echo ""
fi
}
__install_webi

View File

@@ -1,72 +0,0 @@
'use strict';
let Path = require('node:path');
let BuildsCacher = require('./builds-cacher.js');
// let Parallel = require('./parallel.js');
var INSTALLERS_DIR = Path.join(__dirname, '..');
var CACHE_DIR = Path.join(__dirname, '../_cache');
async function main() {
let bc = BuildsCacher.create({
caches: CACHE_DIR,
installers: INSTALLERS_DIR,
});
// let dirs = await bc.getProjectsByType();
// let projNames = Object.keys(dirs.valid);
let lastUpdate;
let projName = 'k9s';
{
let packages = await bc.getPackages({
//Releases: Releases,
name: projName,
date: new Date(),
});
lastUpdate = packages.updated;
console.info(
`Last update for '${projName}': ${packages.updated} (${packages.releases.length} assets)`,
);
}
console.info('Waiting 5s');
{
setTimeout(async function () {
let packages = await bc.getPackages({
//Releases: Releases,
name: projName,
date: new Date(),
});
console.info(
`Last update for '${projName}': ${packages.updated} (${packages.releases.length} assets)`,
);
if (lastUpdate < packages.updated) {
console.info(`PASS`);
} else {
console.info(`MAYBE fail`);
}
}, 5 * 1000);
}
//let parallel = 25;
//await Parallel.run(parallel, projNames, getAll);
//async function getAll(name) {
// void (await bc.getPackages({
// //Releases: Releases,
// name: name,
// date: new Date(),
// }));
//}
}
main()
.then(function () {
console.log('Done');
})
.catch(function (e) {
console.error(e.stack || e);
process.exit(1);
});

View File

@@ -1,907 +0,0 @@
'use strict';
var BuildsCacher = module.exports;
let Fs = require('node:fs/promises');
let Os = require('node:os');
let Path = require('node:path');
let LEGACY_CACHE_DIR = Path.join(Os.homedir(), '.cache/webi/legacy');
let HostTargets = require('./build-classifier/host-targets.js');
let Lexver = require('./build-classifier/lexver.js');
let Triplet = require('./build-classifier/triplet.js');
var ALIAS_RE = /^alias: ([\w.-]+)$/m;
var LEGACY_ARCH_MAP = {
'*': 'ANYARCH',
arm64: 'aarch64',
armv6l: 'armv6',
armv7l: 'armv7',
amd64: 'x86_64',
mipsle: 'mipsel',
mips64le: 'mips64el',
mipsr6le: 'mipsr6el',
mips64r6le: 'mips64r6el',
// yes... el for arm and mips, but le for ppc
// (perhaps the joke got old?)
ppc64el: 'ppc64le',
386: 'x86',
};
var LEGACY_OS_MAP = {
'*': 'ANYOS',
macos: 'darwin',
posix: 'posix_2017',
};
var TERMS_META = [
// pattern
'{ARCH}',
'{EXT}',
'{LIBC}',
'{NAME}',
'{OS}',
'{VENDOR}',
// // os-/arch-indepedent
// 'ANYARCH',
// 'ANYOS',
// // libc
// 'none',
// channel
'beta',
'dev',
'preview',
'stable',
];
/** @typedef {String} TripletString - {arch}-{vendor}-{os}-{libc} */
/** @typedef {String} VersionString */
/** @typedef {Object.<VersionString, Array<BuildAsset>>} PackagesByRelease */
/**
* @typedef ProjectInfo
* @prop {Array<BuildAsset>} releases
* @prop {Array<BuildAsset>} packages
* @prop {Object.<TripletString, PackagesByRelease>} releasesByTriplet
* @prop {Array<import('./build-classifier/types.js').ArchString>} arches
* @prop {Array<import('./build-classifier/types.js').OsString>} oses
* @prop {Array<import('./build-classifier/types.js').LibcString>} libcs
* @prop {Array<String>} channels
* @prop {Array<String>} formats
* @prop {Array<String>} triplets
* @prop {Array<String>} versions
* @prop {Array<String>} lexvers
* @prop {Object.<String, String>} lexversMap
*/
/**
* @typedef BuildAsset
* @prop {String} name
* @prop {String} version
* @prop {Boolean} lts
* @prop {String} date
* @prop {String} arch
* @prop {String} os
* @prop {String} libc
* @prop {String} ext
* @prop {String} download
*/
/**
* @typedef VersionTarget
* @prop {String} version
* @prop {Boolean} lts
* @prop {String} channel
*/
/** @typedef {TargetTriplet & HostTargetPartial} HostTarget */
/** @typedef {import('./build-classifier/types.js').TargetTriplet} TargetTriplet */
/**
* @typedef HostTargetPartial
* @prop {String} target.triplet - os-vendor-arch-libc
* @prop {Error} target.error
*/
async function getPartialHeader(path) {
let readme = `${path}/README.md`;
let head = await readFirstBytes(readme).catch(function (err) {
if (err.code !== 'ENOENT') {
console.warn(`warn: ${path}: ${err.message}`);
}
return null;
});
return head;
}
// let fsOpen = util.promisify(Fs.open);
// let fsRead = util.promisify(Fs.read);
async function readFirstBytes(path) {
let start = 0;
let n = 1024;
let fh = await Fs.open(path, 'r');
let buf = Buffer.alloc(n);
let result = await fh.read(buf, start, n);
let str = result.buffer.toString('utf8');
await fh.close();
return str;
}
BuildsCacher.create = function ({ ALL_TERMS, installers }) {
let installersDir = installers;
if (!ALL_TERMS) {
ALL_TERMS = Triplet.TERMS_PRIMARY_MAP;
}
let bc = {};
bc.ALL_TERMS = ALL_TERMS;
bc.orphanTerms = Object.assign({}, bc.ALL_TERMS);
bc.unknownTerms = {};
bc.usedTerms = {};
bc.formats = [];
bc._triplets = {};
bc._targetsByBuildIdCache = {};
bc._caches = {};
bc._allFormats = {};
bc._allTriplets = {};
// Per-name lock: serializes cold-cache getPackages so concurrent
// callers can't corrupt bc._caches[name] via a transformAndUpdate race.
bc._inflight = {};
for (let term of TERMS_META) {
delete bc.orphanTerms[term];
}
bc.getProjectsByType = async function () {
let dirs = {
hidden: {},
errors: {},
alias: {},
invalid: {},
selfhosted: {},
valid: {},
};
let entries = await Fs.readdir(installersDir, { withFileTypes: true });
for (let entry of entries) {
let meta = await bc.getProjectTypeByEntry(entry);
dirs[meta.type][entry.name] = meta.detail;
}
return dirs;
};
/**
* Get project type and detail - alias, selfhosted, valid (and the invalids)
* @param {String} name - filename
*/
bc.getProjectType = async function (name) {
let filepath = Path.join(installersDir, name);
let entry;
try {
entry = await Fs.lstat(filepath);
Object.assign(entry, { name: name });
} catch (e) {
return { type: 'errors', detail: 'not found' };
}
let info = await bc.getProjectTypeByEntry(entry);
return info;
};
/**
* Get project type and detail - alias, selfhosted, valid (and the invalids)
* @param {fs.Stats|fs.Dirent} entry
*/
bc.getProjectTypeByEntry = async function (entry) {
let path = Path.join(installersDir, entry.name);
// skip non-installer dirs
if (entry.isSymbolicLink()) {
let link = await Fs.readlink(path);
return { type: 'alias', detail: link };
}
if (!entry.isDirectory()) {
return { type: 'hidden', detail: '!directory' };
}
if (entry.name === 'node_modules') {
return { type: 'hidden', detail: 'node_modules' };
}
if (entry.name.startsWith('_')) {
return { type: 'hidden', detail: '_*' };
}
if (entry.name.startsWith('.')) {
return { type: 'hidden', detail: '.*' };
}
if (entry.name.startsWith('~')) {
return { type: 'hidden', detail: '~*' };
}
if (entry.name.endsWith('~')) {
return { type: 'hidden', detail: '*~' };
}
// skip invalid installers
let head = await getPartialHeader(path);
if (!head) {
return { type: 'invalid', detail: '!README.md' };
}
let alias = head.match(ALIAS_RE);
if (alias) {
let link = alias[1];
return { type: 'alias', detail: link };
}
let cacheFile = `${LEGACY_CACHE_DIR}/${entry.name}.json`;
let hasCacheFile = await Fs.access(cacheFile)
.then(function () {
return true;
})
.catch(function () {
return false;
});
if (!hasCacheFile) {
return { type: 'selfhosted', detail: true };
}
return { type: 'valid', detail: true };
};
// Typically a package is organized by release (ex: go has 1.20, 1.21, etc),
// but we will organize by the build (ex: go1.20-darwin-arm64.tar.gz, etc).
bc.getPackages = async function (args) {
let name = args.name;
let warm = bc._caches[name];
if (warm) {
return _doGetPackages(args);
}
let inflight = bc._inflight[name];
if (inflight) {
return inflight;
}
let p = _doGetPackages(args).finally(function () {
delete bc._inflight[name];
});
bc._inflight[name] = p;
return p;
};
async function _doGetPackages({ name }) {
let dataFile = `${LEGACY_CACHE_DIR}/${name}.json`;
let tsFile = `${LEGACY_CACHE_DIR}/${name}.updated.txt`;
let tsDate;
{
let secondsStr = await Fs.readFile(tsFile, 'ascii').catch(function (err) {
if (err.code !== 'ENOENT') {
throw err;
}
return '0';
});
secondsStr = secondsStr.trim();
let seconds = parseFloat(secondsStr) || 0;
let ms = seconds * 1000;
tsDate = new Date(ms);
}
let projInfo = bc._caches[name];
let meta = {
// version info
versions: projInfo?.versions || [],
lexvers: projInfo?.lexvers || [],
lexversMap: projInfo?.lexversMap || {},
// culled release assets
packages: projInfo?.packages || [],
releasesByTriplet: projInfo?.releasesByTriplet || {},
// target info
triplets: projInfo?.triplets || [],
oses: projInfo?.oses || [],
arches: projInfo?.arches || [],
libcs: projInfo?.libcs || [],
formats: projInfo?.formats || [],
// TODO channels: projInfo?.channels || [],
};
if (!projInfo) {
let json = await Fs.readFile(dataFile, 'ascii').catch(
async function (err) {
if (err.code !== 'ENOENT') {
throw err;
}
return null;
},
);
try {
projInfo = JSON.parse(json);
} catch (e) {
console.error(`error: ${dataFile}:\n\t${e.message}`);
projInfo = null;
}
}
if (!projInfo) {
return meta;
}
let latestProjInfo = await BuildsCacher.transformAndUpdate(
name,
projInfo,
meta,
tsDate,
bc,
);
bc._caches[name] = latestProjInfo;
return latestProjInfo;
}
/**
* Given a list of acceptable formats, get the sorted list of of formats.
* Actually used (as per node _webi/lint-builds.js):
* .7z
* .app.zip
* .dmg
* .exe
* .exe.xz
* .git
* .gz
* .msi
* .pkg
* .pkg.tar.zst
* .sh
* .tar.gz
* .tar.xz
* .xz
* .zip
*/
bc.getSortedFormats = function (formats) {
/* jshint maxcomplexity: 25 */
formats.sort();
let id = formats.join(',');
if (bc._allFormats[id]) {
return bc._allFormats[id];
}
// we don't know how to handle any of these yet
// let exclude = [];
// let isAndroid = false;
// if (!isAndroid) {
// exclude.push('.apk');
// }
// let isDebian = false;
// if (!isDebian) {
// exclude.push('.deb');
// }
// let isEnterpriseLinux = false;
// if (!isEnterpriseLinux) {
// exclude.push('.rpm');
// }
// let isArch = false;
// if (!isArch) {
// exclude.push('.pkg.tar.zst');
// }
let hasExe = formats.includes('exe') || formats.includes('.exe');
/** @type {Array<String>} */
let exts = [];
let hasXz = formats.includes('xz') || formats.includes('.xz');
if (hasXz) {
exts.push('.tar.xz');
if (hasExe) {
exts.push('.exe.xz');
}
exts.push('.xz');
}
let hasZst = formats.includes('zst') || formats.includes('.zst');
if (hasZst) {
exts.push('.tar.zst');
exts.push('.zst');
}
let hasZip = formats.includes('zip') || formats.includes('.zip');
if (hasZip) {
exts.push('.zip');
}
let has7z = false;
if (has7z) {
exts.push('.7z');
}
// let hasBz2 = formats.includes('bz2') || formats.includes('.bz2');
// if (hasBz2) {
// exts.push('.bz2');
// }
if (hasExe) {
if (!hasZip) {
exts.push('.zip');
}
exts.push('.tar.gz');
exts.push('.gz');
exts.push('.exe');
exts.push('.msi');
//exts.push('.msixbundle');
} else {
exts.push('.tar.gz');
exts.push('.gz');
exts.push('.sh');
}
let hasGit = formats.includes('git') || formats.includes('.git');
if (hasGit) {
exts.push('.git');
}
// Fallbacks
// (we include everything to bubble an extract error over not found)
exts.push('.app.zip');
exts.push('.dmg');
exts.push('.pkg');
if (!hasXz) {
exts.push('.tar.xz');
if (hasExe) {
exts.push('.exe.xz');
}
exts.push('.xz');
}
if (!hasZip) {
if (!hasExe) {
exts.push('.zip');
}
}
if (!hasZst) {
exts.push('.tar.zst');
exts.push('.zst');
}
if (!has7z) {
exts.push('.7z');
}
// if (!hasRar) {
// exts.push('.rar');
// }
// exts.push('.tar.bz2');
// exts.push('.bz2');
bc._allFormats[id] = exts;
return exts;
};
bc.selectPackage = function (packages, formats) {
if (packages.length === 1) {
return packages[0];
}
let exts = bc.getSortedFormats(formats);
for (let ext of exts) {
for (let build of packages) {
if (build.ext === ext) {
return build;
}
}
}
return packages[0];
};
/**
* @param {ProjectInfo} projInfo
*/
bc.enumerateLatestVersions = function (projInfo) {
let lexPrefix = '';
let matchInfo = Lexver.matchSorted(projInfo.lexvers, lexPrefix);
let verInfo = {
default: projInfo.lexversMap[matchInfo.default],
previous: projInfo.lexversMap[matchInfo.previous],
stable: projInfo.lexversMap[matchInfo.stable],
latest: projInfo.lexversMap[matchInfo.latest],
};
return verInfo;
};
/**
* @param {ProjectInfo} projInfo
* @param {HostTarget} hostTarget
* @param {VersionTarget} verTarget
*/
bc.findMatchingPackages = function (projInfo, hostTarget, verTarget) {
let matchInfo = bc._enumerateVersions(projInfo, verTarget.version);
let triplets = bc._enumerateTriplets(hostTarget);
//console.log('dbg: matchInfo', matchInfo);
if (matchInfo) {
for (let _triplet of triplets) {
let targetReleases = projInfo.releasesByTriplet[_triplet];
if (!targetReleases) {
continue;
}
// Make sure that these releases are the expected version
// (ex: jq1.7 => darwin-arm64-libc, jq1.6 => darwin-x86_64-libc)
for (let matchver of matchInfo.matches) {
let ver = projInfo.lexversMap[matchver] || matchver;
let packages = targetReleases[ver];
if (!packages) {
continue;
}
let match = {
triplet: _triplet,
packages: packages,
latest: projInfo.versions[0],
version: ver,
versions: matchInfo,
};
return match;
}
}
return null;
}
// Version-first iteration, not triplet-first: take the newest
// version even when its only build lives in a fallback triplet
// (e.g. serviceman v1.0.1 only exists at posix_2017-ANYARCH-none).
for (let lexver of projInfo.lexvers) {
let ver = projInfo.lexversMap[lexver] || lexver;
for (let _triplet of triplets) {
let targetReleases = projInfo.releasesByTriplet[_triplet];
if (!targetReleases) {
continue;
}
let packages = targetReleases[ver];
if (!packages) {
continue;
}
let pkg = packages[0];
if (verTarget.lts) {
if (!pkg.lts) {
continue;
}
let match = {
triplet: _triplet,
packages: packages,
latest: projInfo.versions[0],
version: ver,
versions: matchInfo,
};
return match;
}
let wantChannel = verTarget.channel || 'stable';
let isChannel = pkg.channel || 'stable';
if (wantChannel === 'stable') {
if (isChannel !== 'stable') {
continue;
}
}
// latest, beta, alpha, rc, preview
let match = {
triplet: _triplet,
packages: packages,
latest: projInfo.versions[0],
version: ver,
versions: matchInfo,
};
return match;
}
}
return null;
};
bc._enumerateTriplets = function (hostTarget) {
let id = [hostTarget.os, hostTarget.arch, hostTarget.libc].join(',');
let triplets = bc._allTriplets[id] || [];
if (triplets.length > 0) {
return triplets;
}
// Prefer platform-specific matches over ANYOS/ANYARCH fallbacks.
// This ensures e.g. darwin-aarch64-none matches before
// ANYOS-ANYARCH-none (.git source URLs from old releases).
let oses = [];
if (hostTarget.os === 'windows') {
oses = ['windows', 'ANYOS'];
} else if (hostTarget.os === 'android') {
oses = ['android', 'linux', 'posix_2017', 'posix_2024', 'ANYOS'];
} else {
oses = [hostTarget.os, 'posix_2017', 'posix_2024', 'ANYOS'];
}
let waterfall = HostTargets.WATERFALL[hostTarget.os] || {};
let arches = waterfall[hostTarget.arch] ||
HostTargets.WATERFALL.ANYOS[hostTarget.arch] || [hostTarget.arch];
arches = arches.concat(['ANYARCH']);
// termsToTarget omits libc for plain UAs; 'libc' → waterfall ['none','libc',...]
let libc = hostTarget.libc || 'libc';
let libcs = waterfall[libc] ||
HostTargets.WATERFALL.ANYOS[libc] || [libc];
// Extend the glibc-host waterfall: the table only lists [none, libc]
// but Rust projects (bat, rg) and node ship libc='gnu' builds, and
// static musl builds also run on glibc hosts.
if (libc === 'libc' && !libcs.includes('gnu')) {
libcs = ['none', 'gnu', 'musl', 'libc'];
}
for (let os of oses) {
for (let arch of arches) {
for (let libc of libcs) {
let triplet = `${os}-${arch}-${libc}`;
triplets.push(triplet);
}
}
}
bc._allTriplets[id] = triplets;
return triplets;
};
bc._enumerateVersions = function (projInfo, ver) {
if (!ver) {
return null;
}
let lexPrefix = Lexver.parsePrefix(ver);
let matchInfo = Lexver.matchSorted(projInfo.lexvers, lexPrefix);
return matchInfo;
};
return bc;
};
BuildsCacher._classify = function (bc, projInfo, build) {
/* jshint maxcomplexity: 30 */
// Cache entries arrive pre-classified (os/arch/libc/ext set). Skip
// maybeInstallable for those — it false-rejects names ending in a
// version tag (`serviceman-v1.0.1`, `v1.0.1.zip`).
let cacheClassified =
build.os && build.arch && build.libc && build.ext;
if (!cacheClassified) {
let maybeInstallable = Triplet.maybeInstallable(projInfo, build);
if (!maybeInstallable) {
return null;
}
}
if (LEGACY_OS_MAP[build.os]) {
build.os = LEGACY_OS_MAP[build.os];
}
if (LEGACY_ARCH_MAP[build.arch]) {
build.arch = LEGACY_ARCH_MAP[build.arch];
}
// because some packages are shimmed to match a single download against
let preTarget = Object.assign({ os: '', arch: '', libc: '' }, build);
let targetId = `${preTarget.os}:${preTarget.arch}:${preTarget.libc}`;
let buildId = `${projInfo.name}:${targetId}@${build.download}`;
//console.log(`dbg: buildId`, buildId);
let target = bc._targetsByBuildIdCache[buildId];
if (target) {
Object.assign(build, { target: target, triplet: target.triplet });
return target;
}
let pattern = Triplet.toPattern(projInfo, build);
//console.log(`dbg: pattern`, pattern);
if (!pattern) {
let err = new Error(`no pattern generated for ${projInfo.name}`);
err.code = 'E_BUILD_NO_PATTERN';
target = { error: err };
bc._targetsByBuildIdCache[buildId] = target;
return target;
}
let rawTerms = pattern.split(/[_\{\}\/\.\-]+/g);
//console.log(`dbg: rawTerms`, rawTerms);
for (let term of rawTerms) {
delete bc.orphanTerms[term];
bc.usedTerms[term] = true;
}
// {NAME}/{NAME}-{VER}-Windows-x86_64_v2-musl.exe =>
// {NAME}.windows.x86_64v2.musl.exe
let terms = Triplet.patternToTerms(pattern);
//console.log(`dbg: terms`, terms);
if (!terms.length) {
let err = new Error(`'${terms}' was trimmed to ''`);
target = { error: err };
bc._targetsByBuildIdCache[buildId] = target;
return target;
}
for (let term of terms) {
if (!term) {
continue;
}
if (bc.ALL_TERMS[term]) {
delete bc.orphanTerms[term];
bc.usedTerms[term] = true;
continue;
}
bc.unknownTerms[term] = true;
}
// Skip termsToTarget for cache-classified entries: it false-flags
// e.g. .git URLs as os=ANYOS while the cache says os=posix_2017,
// and the mismatch check throws.
target = { triplet: '' };
if (cacheClassified) {
target.os = build.os;
target.arch = build.arch;
target.libc = build.libc;
target.vendor = build.vendor || 'unknown';
target.android = false;
target.unknownTerms = [];
} else {
try {
void Triplet.termsToTarget(target, projInfo, build, terms);
} catch (e) {
console.error(`PACKAGE FORMAT CHANGE for '${projInfo.name}':`);
console.error(e.message);
console.error(build);
return null;
}
}
target.triplet = `${target.arch}-${target.vendor}-${target.os}-${target.libc}`;
{
// TODO I don't love this hidden behavior
// perhaps classify should just happen when the package is loaded
// (and the sanity error should be removed, or thrown after the loop is complete)
let hasTriplet = projInfo.triplets.includes(target.triplet);
if (!hasTriplet) {
projInfo.triplets.push(target.triplet);
}
let hasOs = projInfo.oses.includes(target.os);
if (!hasOs) {
projInfo.oses.push(target.os);
}
let hasArch = projInfo.arches.includes(target.arch);
if (!hasArch) {
projInfo.arches.push(target.arch);
}
let hasLibc = projInfo.libcs.includes(target.libc);
if (!hasLibc) {
projInfo.libcs.push(target.libc);
}
if (!build.ext) {
build.ext = Triplet.buildToPackageType(build);
}
if (build.ext) {
if (!build.ext.startsWith('.')) {
build.ext = `.${build.ext}`;
}
}
let hasExt = projInfo.formats.includes(build.ext);
if (!hasExt) {
projInfo.formats.push(build.ext);
}
let hasGlobalExt = bc.formats.includes(build.ext);
if (!hasGlobalExt) {
bc.formats.push(build.ext);
}
}
bc._triplets[target.triplet] = true;
bc._targetsByBuildIdCache[buildId] = target;
let triple = [target.arch, target.vendor, target.os, target.libc];
for (let term of triple) {
if (!bc.ALL_TERMS[term]) {
throw new Error(
`[SANITY FAIL] '${projInfo.name}' '${target.triplet}' generated unknown term '${term}'`,
);
}
delete bc.orphanTerms[term];
bc.usedTerms[term] = true;
}
return target;
};
BuildsCacher.transformAndUpdate = function (name, projInfo, meta, date, bc) {
meta.packages = [];
let updated = date.valueOf();
Object.assign(projInfo, { name, updated }, meta);
for (let build of projInfo.releases) {
let buildTarget = BuildsCacher._classify(bc, projInfo, build);
if (!buildTarget) {
// ignore known, non-package extensions
continue;
}
if (buildTarget.error) {
let err = buildTarget.error;
let code = err.code || '';
console.error(`[ERROR]: ${code} ${projInfo.name}: ${build.name}`);
console.error(`>>> ${err.message} <<<`);
console.error(projInfo);
console.error(build);
console.error(`^^^ ${err.message} ^^^`);
console.error(err.stack);
continue;
}
if (!build.name) {
build.name = build.download.replace(/.*\//, '');
}
build.target = buildTarget;
meta.packages.push(build);
}
BuildsCacher.updateReleasesByTriplet(meta);
BuildsCacher.updateAndSortVersions(projInfo, meta);
Object.assign(projInfo, { name, updated }, meta);
return projInfo;
};
// TODO
// - tag channels
BuildsCacher.updateAndSortVersions = function (projInfo, meta) {
for (let build of projInfo.packages) {
let hasVersion = meta.versions.includes(build.version);
if (!hasVersion) {
build.lexver = Lexver.parseVersion(build.version);
meta.lexversMap[build.lexver] = build.version;
}
}
meta.lexvers = Object.keys(meta.lexversMap);
meta.lexvers.sort();
meta.lexvers.reverse();
meta.versions = [];
for (let lexver of meta.lexvers) {
let version = meta.lexversMap[lexver];
meta.versions.push(version);
}
projInfo.packages.sort(function (a, b) {
if (a.lexver > b.lexver) {
return -1;
}
if (a.lexver < b.lexver) {
return 1;
}
return 0;
});
};
BuildsCacher.updateReleasesByTriplet = function (meta) {
for (let build of meta.packages) {
let target = build.target;
let triplet = `${target.os}-${target.arch}-${target.libc}`;
if (!meta.releasesByTriplet[triplet]) {
meta.releasesByTriplet[triplet] = {};
}
let buildsByRelease = meta.releasesByTriplet[triplet];
if (!buildsByRelease[build.version]) {
buildsByRelease[build.version] = [];
}
let packages = buildsByRelease[build.version];
packages.push(build);
}
};

View File

@@ -1,37 +0,0 @@
'use strict';
let Builds = module.exports;
let Path = require('node:path');
let BuildsCacher = require('./builds-cacher.js');
// let HostTargets = require('./build-classifier/host-targets.js');
let Parallel = require('./parallel.js');
var INSTALLERS_DIR = Path.join(__dirname, '..');
var CACHE_DIR = Path.join(__dirname, '../_cache');
let bc = BuildsCacher.create({
caches: CACHE_DIR,
installers: INSTALLERS_DIR,
});
Builds.init = async function () {
let dirs = await bc.getProjectsByType();
let projNames = Object.keys(dirs.valid);
let parallel = 25;
await Parallel.run(parallel, projNames, getAll);
async function getAll(name) {
void (await bc.getPackages({
name: name,
date: new Date(),
}));
}
};
Builds.enumerateLatestVersions = bc.enumerateLatestVersions;
Builds.findMatchingPackages = bc.findMatchingPackages;
Builds.getPackage = bc.getPackages;
Builds.getProjectType = bc.getProjectType;
Builds.selectPackage = bc.selectPackage;

View File

@@ -1,90 +0,0 @@
'use strict';
let Fs = require('node:fs/promises');
let Os = require('node:os');
let Path = require('node:path');
let BuildsCacher = require('./builds-cacher.js');
let Triplet = require('./build-classifier/triplet.js');
let LEGACY_CACHE_DIR = Path.join(Os.homedir(), '.cache/webi/legacy');
async function main() {
let projName = process.argv[2];
if (!projName) {
console.error(``);
console.error(`USAGE`);
console.error(``);
console.error(` classify-one <project-name>`);
console.error(``);
console.error(`EXAMPLE`);
console.error(``);
console.error(` classify-one caddy`);
console.error(``);
return;
}
let tsDate = new Date(0);
let meta = {
// version info
versions: [],
lexvers: [],
lexversMap: {},
// culled release assets
packages: [],
releasesByTriplet: {},
// target info
triplets: [],
oses: [],
arches: [],
libcs: [],
formats: [],
};
let dataFile = Path.join(LEGACY_CACHE_DIR, `${projName}.json`);
let json = await Fs.readFile(dataFile, 'utf8');
let projInfo = JSON.parse(json);
// let packages = await Builds.getPackage({ name: projName });
// console.log(packages);
let bc = {};
bc.ALL_TERMS = Triplet.TERMS_PRIMARY_MAP;
bc.orphanTerms = Object.assign({}, bc.ALL_TERMS);
bc.unknownTerms = {};
bc.usedTerms = {};
bc.formats = [];
bc._targetsByBuildIdCache = {};
bc._triplets = {};
let transformed = BuildsCacher.transformAndUpdate(
projName,
projInfo,
meta,
tsDate,
bc,
);
console.log(`[DEBUG] transformed`);
let sample = transformed.packages.slice(0, 20);
console.log('packages:', sample, ':packages');
let firstTriplet = Object.keys(transformed.releasesByTriplet)[0];
let firstVersion = transformed.versions[0];
console.log(
`releasesByTriplet[${firstTriplet}][${firstVersion}]:`,
transformed.releasesByTriplet[firstTriplet]?.[firstVersion],
':releasesByTriplet',
);
console.log('versions:', transformed.versions, ':versions');
console.log('triplets:', transformed.triplets, ':triplets');
console.log('oses:', transformed.oses, ':oses');
console.log('arches:', transformed.arches, ':arches');
console.log('libcs:', transformed.libcs, ':libcs');
console.log('formats:', transformed.formats, ':formats');
console.log(Object.keys(transformed));
}
main().catch(function (err) {
console.error('Error:');
console.error(err);
});

View File

@@ -1,349 +0,0 @@
#!/bin/sh
#<pre>
############################################################
# <h1>Cheat Sheet at CHEATSHEET_URL</h1>
# <meta http-equiv="refresh" content="3; URL='CHEATSHEET_URL'" />
############################################################
export WEBI_PKG=webi
export WEBI_HOST=https://webinstall.dev
export WEBI_CHECKSUM=06a7fb9f
#########################################
# #
# Display Debug Info in Case of Failure #
# #
#########################################
fn_show_welcome() { (
echo ""
echo ""
# invert t_task and t_pkg for top-level welcome message
printf -- ">>> %s %s <<<\n" \
"$(t_pkg 'Welcome to') $(t_task 'Webi')$(t_pkg '!')" \
"$(t_dim "- modern tools, instant installs.")"
echo " We expect your experience to be $(t_em 'absolutely perfect')!"
echo ""
echo " $(t_attn 'Success')? Star it! $(t_url 'https://github.com/webinstall/webi-installers')"
echo " $(t_attn 'Problem')? Report it: $(t_url 'https://github.com/webinstall/webi-installers/issues')"
echo " $(t_dim "(your system is") $(t_host "$(fn_get_os)")/$(t_host "$(uname -m)") $(t_dim "with") $(t_host "$(fn_get_libc)") $(t_dim "&") $(t_host "$(fn_get_http_client_name)")$(t_dim ")")"
sleep 0.2
); }
fn_get_os() { (
# Ex:
# GNU/Linux
# Android
# Linux (often Alpine, musl)
# Darwin
b_os="$(uname -o 2> /dev/null || echo '')"
b_sys="$(uname -s)"
if test -z "${b_os}" || test "${b_os}" = "${b_sys}"; then
# ex: 'Darwin' (and plain, non-GNU 'Linux')
echo "${b_sys}"
return 0
fi
if echo "${b_os}" | grep -q "${b_sys}"; then
# ex: 'GNU/Linux'
echo "${b_os}"
return 0
fi
# ex: 'Android/Linux'
echo "${b_os}/${b_sys}"
); }
fn_get_libc() { (
# Ex:
# musl
# libc
if ldd /bin/ls 2> /dev/null | grep -q 'musl' 2> /dev/null; then
echo 'musl'
elif fn_get_os | grep -q 'GNU|Linux'; then
echo 'gnu'
else
echo 'libc'
fi
); }
fn_get_http_client_name() { (
# Ex:
# curl
# curl+wget
b_client=""
if command -v curl > /dev/null; then
b_client="curl"
fi
if command -v wget > /dev/null; then
if test -z "${b_client}"; then
b_client="wget"
else
b_client="curl+wget"
fi
fi
echo "${b_client}"
); }
#########################################
# #
# For Making the Display Nice #
# #
#########################################
# Term Types
t_cmd() { (fn_printf '\e[2m\e[35m%s\e[39m\e[22m' "${1}"); }
t_host() { (fn_printf '\e[2m\e[33m%s\e[39m\e[22m' "${1}"); }
t_link() { (fn_printf '\e[1m\e[36m%s\e[39m\e[22m' "${1}"); }
t_path() { (fn_printf '\e[2m\e[32m%s\e[39m\e[22m' "${1}"); }
t_pkg() { (fn_printf '\e[1m\e[32m%s\e[39m\e[22m' "${1}"); }
t_task() { (fn_printf '\e[36m%s\e[39m' "${1}"); }
t_url() { (fn_printf '\e[2m%s\e[22m' "${1}"); }
# Levels
t_info() { (fn_printf '\e[1m\e[36m%s\e[39m\e[22m' "${1}"); }
t_attn() { (fn_printf '\e[1m\e[33m%s\e[39m\e[22m' "${1}"); }
t_warn() { (fn_printf '\e[1m\e[33m%s\e[39m\e[22m' "${1}"); }
t_err() { (fn_printf '\e[31m%s\e[39m' "${1}"); }
# Styles
t_bold() { (fn_printf '\e[1m%s\e[22m' "${1}"); }
t_dim() { (fn_printf '\e[2m%s\e[22m' "${1}"); }
t_em() { (fn_printf '\e[3m%s\e[23m' "${1}"); }
t_under() { (fn_printf '\e[4m%s\e[24m' "${1}"); }
# FG Colors
t_cyan() { (fn_printf '\e[36m%s\e[39m' "${1}"); }
t_green() { (fn_printf '\e[32m%s\e[39m' "${1}"); }
t_magenta() { (fn_printf '\e[35m%s\e[39m' "${1}"); }
t_yellow() { (fn_printf '\e[33m%s\e[39m' "${1}"); }
fn_printf() { (
a_style="${1}"
a_text="${2}"
if fn_is_tty; then
#shellcheck disable=SC2059
printf -- "${a_style}" "${a_text}"
else
printf -- '%s' "${a_text}"
fi
); }
fn_sub_home() { (
my_rel=${HOME}
my_abs=${1}
echo "${my_abs}" | sed "s:^${my_rel}:~:"
); }
###################################
# #
# Detect HTTP Client #
# #
###################################
fn_wget() { (
# Doc:
# Downloads the file at the given url to the given path
a_url="${1}"
a_path="${2}"
cmd_wget="wget -c -q --user-agent"
if fn_is_tty; then
cmd_wget="wget -c -q --show-progress --user-agent"
fi
# busybox wget doesn't support --show-progress
# See <https://github.com/webinstall/webi-installers/pull/772>
if readlink "$(command -v wget)" | grep -q busybox; then
cmd_wget="wget --user-agent"
fi
b_triple_ua="$(fn_get_target_triple_user_agent)"
b_agent="webi/wget ${b_triple_ua}"
if command -v curl > /dev/null; then
b_agent="webi/wget+curl ${b_triple_ua}"
fi
if ! $cmd_wget "${b_agent}" "${a_url}" -O "${a_path}"; then
echo >&2 " $(t_err "failed to download (wget)") '$(t_url "${a_url}")'"
echo >&2 " $cmd_wget '${b_agent}' '${a_url}' -O '${a_path}'"
echo >&2 " $(wget -V)"
return 1
fi
); }
fn_curl() { (
# Doc:
# Downloads the file at the given url to the given path
a_url="${1}"
a_path="${2}"
cmd_curl="curl -f -sSL -#"
if fn_is_tty; then
cmd_curl="curl -f -sSL"
fi
b_triple_ua="$(fn_get_target_triple_user_agent)"
b_agent="webi/curl ${b_triple_ua}"
if command -v wget > /dev/null; then
b_agent="webi/curl+wget ${b_triple_ua}"
fi
if ! $cmd_curl -A "${b_agent}" "${a_url}" -o "${a_path}"; then
echo >&2 " $(t_err "failed to download (curl)") '$(t_url "${a_url}")'"
echo >&2 " $cmd_curl -A '${b_agent}' '${a_url}' -o '${a_path}'"
echo >&2 " $(curl -V)"
return 1
fi
); }
fn_get_target_triple_user_agent() { (
# Ex:
# x86_64/unknown GNU/Linux/5.15.107-2-pve gnu
# arm64/unknown Darwin/22.6.0 libc
echo "$(uname -m)/unknown $(fn_get_os)/$(uname -r) $(fn_get_libc)"
); }
fn_download_to_path() { (
a_url="${1}"
a_path="${2}"
mkdir -p "$(dirname "${a_path}")"
if command -v curl > /dev/null; then
fn_curl "${a_url}" "${a_path}.part"
elif command -v wget > /dev/null; then
fn_wget "${a_url}" "${a_path}.part"
else
echo >&2 " $(t_err "failed to detect HTTP client (curl, wget)")"
return 1
fi
mv "${a_path}.part" "${a_path}"
); }
##############################################
# #
# Install or Update Webi and Install Package #
# #
##############################################
webi_bootstrap() { (
a_path="${1}"
echo ""
echo "$(t_task 'Bootstrapping') $(t_pkg 'Webi')"
b_path_rel="$(fn_sub_home "${a_path}")"
b_checksum=""
if test -r "${a_path}"; then
b_checksum="$(fn_checksum "${a_path}")"
fi
if test "$b_checksum" = "${WEBI_CHECKSUM}"; then
echo " $(t_dim 'Found') $(t_path "${b_path_rel}")"
sleep 0.1
return 0
fi
b_webi_file_url="${WEBI_HOST}/packages/webi/webi.sh"
b_tmp=''
if test -r "${a_path}"; then
b_ts="$(date -u '+%s')"
b_tmp="${a_path}.${b_ts}.bak"
mv "${a_path}" "${b_tmp}"
echo " Updating $(t_path "${b_path_rel}")"
fi
echo " Downloading $(t_url "${b_webi_file_url}")"
echo " to $(t_path "${b_path_rel}")"
fn_download_to_path "${b_webi_file_url}" "${a_path}"
chmod u+x "${a_path}"
if test -r "${b_tmp}"; then
rm -f "${b_tmp}"
fi
); }
fn_checksum() {
a_filepath="${1}"
if command -v sha1sum > /dev/null; then
sha1sum "${a_filepath}" | cut -d' ' -f1 | cut -c 1-8
return 0
fi
if command -v shasum > /dev/null; then
shasum "${a_filepath}" | cut -d' ' -f1 | cut -c 1-8
return 0
fi
if command -v sha1 > /dev/null; then
sha1 "${a_filepath}" | cut -d'=' -f2 | cut -c 2-9
return 0
fi
echo >&2 " warn: no sha1 sum program"
date '+%F %H:%M'
}
##############################################
# #
# Detect TTY and run main #
# #
##############################################
fn_is_tty() {
if test "${WEBI_TTY}" = 'tty'; then
return 0
fi
return 1
}
fn_detect_tty() { (
# stdin will NOT be a tty if it's being piped
# stdout & stderr WILL be a tty even when piped
# they are not a tty if being captured or redirected
# 'set -i' is NOT available in sh
if test -t 1 && test -t 2; then
return 0
fi
return 1
); }
main() { (
set -e
set -u
WEBI_TTY="${WEBI_TTY:-}"
if test -z "${WEBI_TTY}"; then
if fn_detect_tty; then
WEBI_TTY="tty"
fi
export WEBI_TTY
fi
if test -z "${WEBI_WELCOME:-}"; then
fn_show_welcome
fi
export WEBI_WELCOME='shown'
# note: we may support custom locations in the future
export WEBI_HOME="${HOME}/.local"
b_home="$(fn_sub_home "${WEBI_HOME}")"
b_webi_path="${WEBI_HOME}/bin/webi"
b_webi_path_rel="${b_home}/bin/webi"
WEBI_CURRENT="${WEBI_CURRENT:-}"
if test "${WEBI_CURRENT}" != "${WEBI_CHECKSUM}"; then
webi_bootstrap "${b_webi_path}"
export WEBI_CURRENT="${WEBI_CHECKSUM}"
fi
echo " Running $(t_cmd "${b_webi_path_rel} ${WEBI_PKG}")"
echo ""
"${b_webi_path}" "${WEBI_PKG}"
); }
main

View File

@@ -0,0 +1,19 @@
#!/bin/sh
set -e
set -u
# For installing from the extracted package tmp directory
pkg_install() {
(
cd "$WEBI_TMP"
if command -v rsync 2> /dev/null | grep -q rsync; then
rsync -Krl ./xmpl*/ "$pkg_src/" 2> /dev/null
else
cp -Hr ./xmpl*/* "$pkg_src/" 2> /dev/null
cp -Hr ./xmpl*/.* "$pkg_src/" 2> /dev/null
fi
rm -rf ./xmpl*
)
}

View File

@@ -1,233 +0,0 @@
'use strict';
var Installers = module.exports;
var Crypto = require('crypto');
var Fs = require('node:fs/promises');
var path = require('node:path');
var reInstallTpl = /\s*#?\s*{{ installer }}/;
function padScript(txt) {
return txt.replace(/^/g, ' ');
}
var BAD_SH_RE = /[<>'"`$\\]/;
Installers.renderBash = async function (
pkgdir,
rel,
{ baseurl, pkg, tag, ver, os = '', arch = '', libc = '', formats },
) {
if (!Array.isArray(formats)) {
formats = [];
}
if (!tag) {
tag = '';
}
let installTxt = await Fs.readFile(path.join(pkgdir, 'install.sh'), 'utf8');
installTxt = padScript(installTxt);
var vers = rel.version.split('.');
var v = {
major: vers.shift() || '',
minor: vers.shift() || '',
patch: vers.join('.').replace(/[+\-].*/, ''),
build: vers
.join('.')
.replace(/[^+\-]*/, '')
.replace(/^-/, ''),
};
var pkgFile = rel.filename || rel.name;
let tplTxt = await Fs.readFile(
path.join(__dirname, 'package-install.tpl.sh'),
'utf8',
);
// ex: 'node@lts' or 'node'
var webiPkg = pkg;
if (ver) {
webiPkg += `@${ver}`;
}
let releaseParams = new URLSearchParams({
os: rel.os,
arch: rel.arch,
libc: rel.libc,
formats: formats.join(','),
pretty: true,
});
let releaseSearch = releaseParams.toString();
releaseSearch = releaseSearch.replace(/%2C/g, ',');
let releaseUrl = `/api/releases/${pkg}@${tag}.tab?${releaseSearch}`;
let releaseCsv = [
rel.version,
rel.lts,
rel.channel,
rel.date,
rel.os,
rel.arch,
rel.ext,
'-',
rel.download,
rel.name,
rel.comment || '',
]
.join(',')
.replace(/'/g, '');
let webiChecksum = await Installers.getWebiShChecksum();
let envReplacements = [
['WEBI_CHECKSUM', webiChecksum],
['WEBI_PKG', webiPkg],
['WEBI_HOST', baseurl],
['WEBI_OS', os],
['WEBI_ARCH', arch],
['WEBI_LIBC', libc],
['WEBI_TAG', tag],
['WEBI_RELEASES', `${baseurl}${releaseUrl}`],
['WEBI_CSV', releaseCsv],
['WEBI_VERSION', rel.version],
['WEBI_MAJOR', v.major],
['WEBI_MINOR', v.minor],
['WEBI_PATCH', v.patch],
['WEBI_BUILD', v.build],
['WEBI_GIT_BRANCH', rel.git_branch || rel.git_tag],
['WEBI_GIT_TAG', rel.git_tag], // TODO replace with branch
['WEBI_LTS', rel.lts],
['WEBI_CHANNEL', rel.channel],
['WEBI_EXT', rel.ext],
['WEBI_FORMATS', formats.join(',')],
['WEBI_PKG_URL', rel.download],
['WEBI_PKG_PATHNAME', pkgFile],
['WEBI_PKG_FILE', pkgFile], // TODO replace with pathname
['PKG_NAME', pkg],
['PKG_STABLE', rel.stable],
['PKG_LATEST', rel.latest],
['PKG_OSES', (rel.oses || []).join(' ')],
['PKG_ARCHES', (rel.arches || []).join(' ')],
['PKG_LIBCS', (rel.libcs || []).join(' ')],
['PKG_FORMATS', (rel.formats || []).join(' ')],
];
for (let env of envReplacements) {
let name = env[0];
let value = env[1];
// Ex:
// #export WEBI_FOO=xyz => export WEBI_FOO='123'
// export WEBI_FOO= => export WEBI_FOO='123'
let envRe = new RegExp(
`^[ \\t]*#?[ \\t]*(export[ \\t])?[ \\t]*(${name})=.*`,
'm',
);
if (BAD_SH_RE.test(value)) {
throw new Error(`key '${name}' has invalid value '${value}'`);
}
tplTxt = tplTxt.replace(envRe, `$1$2='${value}'`);
}
tplTxt = tplTxt
.replace(/CHEATSHEET_URL/g, `${baseurl}/${pkg}`)
// $', $0, ... $9, $`, $&, and $_ all have special meaning
// (see https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp)
// However, it can be escaped with $$ (which must be escaped with $$)
.replace(reInstallTpl, '\n' + installTxt.replace(/\$/g, '$$$$'));
return tplTxt;
};
Installers.renderPowerShell = async function (
pkgdir,
rel,
{ baseurl, pkg, tag, ver, os, arch, libc = '', formats },
) {
if (!Array.isArray(formats)) {
formats = [];
}
if (!tag) {
tag = '';
}
let installTxt = await Fs.readFile(path.join(pkgdir, 'install.ps1'), 'utf8');
installTxt = padScript(installTxt);
/*
var vers = rel.version.split('.');
var v = {
major: vers.shift() || '',
minor: vers.shift() || '',
patch: vers.join('.').replace(/[+\-].*$/, ''),
build: vers
.join('.')
.replace(/[^+\-]*()/, '')
.replace(/^-/, '')
};
*/
let tplTxt = await Fs.readFile(
path.join(__dirname, 'package-install.tpl.ps1'),
'utf8',
);
var pkgver = pkg + '@' + ver;
return (
tplTxt
.replace(
/^(#)?\$Env:WEBI_LIBC\s*=.*/im,
"$Env:WEBI_LIBC = '" + libc + "'",
)
.replace(
/^(#)?\$Env:WEBI_HOST\s*=.*/im,
"$Env:WEBI_HOST = '" + baseurl + "'",
)
.replace(
/^(#)?\$Env:WEBI_PKG\s*=.*/im,
"$Env:WEBI_PKG = '" + pkgver + "'",
)
.replace(/^(#)?\$Env:PKG_NAME\s*=.*/im, "$Env:PKG_NAME = '" + pkg + "'")
.replace(
/^(#)?\$Env:WEBI_VERSION\s*=.*/im,
"$Env:WEBI_VERSION = '" + rel.version + "'",
)
.replace(
/^(#)?\$Env:WEBI_GIT_TAG\s*=.*/im,
"$Env:WEBI_GIT_TAG = '" + rel.git_tag + "'",
)
.replace(
/^(#)?\$Env:WEBI_PKG_URL\s*=.*/im,
"$Env:WEBI_PKG_URL = '" + rel.download + "'",
)
// TODO replace WEBI_PKG_FILE (which is sometimes a dir)
.replace(
/^(#)?\$Env:WEBI_PKG_PATHNAME\s*=.*/im,
"$Env:WEBI_PKG_PATHNAME = '" + rel.name + "'",
)
// TODO deprecate
.replace(
/^(#)?\$Env:WEBI_PKG_FILE\s*=.*/im,
"$Env:WEBI_PKG_FILE = '" + rel.name + "'",
)
.replace(reInstallTpl, '\n' + installTxt)
);
};
var _webiShMeta = {
stale: 10 * 1000,
updated_at: 0,
checksum: '',
mtime: 0,
};
Installers.getWebiShChecksum = async function () {
let now = Date.now();
let ago = now - _webiShMeta.updated_at;
if (ago <= _webiShMeta.stale) {
return _webiShMeta.checksum;
}
let webiPath = path.join(__dirname, '../webi/webi.sh');
let stat = await Fs.stat(webiPath);
if (stat.mtimeMs === _webiShMeta.mtime) {
return _webiShMeta.checksum;
}
let webiBuf = await Fs.readFile(webiPath, null);
let webiHash = Crypto.createHash('sha1').update(webiBuf).digest('hex');
let webiChecksum = webiHash.slice(0, 8);
_webiShMeta.mtime = stat.mtimeMs;
_webiShMeta.updated_at = now;
_webiShMeta.checksum = webiChecksum;
return _webiShMeta.checksum;
};

View File

@@ -1,336 +0,0 @@
#!/usr/bin/env node
'use strict';
let Fs = require('node:fs/promises');
let Path = require('node:path');
let BuildsCacher = require('./builds-cacher.js');
let HostTargets = require('./build-classifier/host-targets.js');
let Parallel = require('./parallel.js');
var INSTALLERS_DIR = Path.join(__dirname, '..');
var CACHE_DIR = Path.join(__dirname, '../_cache');
let UserAgentsMap = require('./build-classifier/uas.json');
let uas = Object.keys(UserAgentsMap);
let uaTargetsMap = {};
for (let ua of uas) {
let terms = ua.split(/[\s\/]+/g);
let target = {};
void HostTargets.termsToTarget(target, terms);
if (!target) {
continue;
}
if (target.errors.length) {
throw target.errors[0];
}
if (!target.os) {
// TODO make target null, or create error for this
console.warn(`no os for terms: ${terms}`);
//throw new Error(`terms: ${terms}`);
continue;
}
if (!target.arch) {
// TODO make target null, or create error for this
console.warn(`no arch for terms: ${terms}`);
//throw new Error(`terms: ${terms}`);
continue;
}
if (!target.libc) {
// TODO make target null, or create error for this
console.warn(`no libc for terms: ${terms}`);
//throw new Error(`terms: ${terms}`);
continue;
}
let triplet = `${target.os}-${target.arch}-${target.libc}`;
uaTargetsMap[triplet] = target;
}
let uaTargets = [];
let triplets = Object.keys(uaTargetsMap);
for (let triplet of triplets) {
let target = uaTargetsMap[triplet];
uaTargets.push(target);
}
function showDirs(dirs) {
{
let errors = Object.keys(dirs.errors);
console.error('');
console.error(`Errors: ${errors.length}`);
for (let name of errors) {
let err = dirs.errors[name];
console.error(`${name}/: ${err.message}`);
}
}
{
let hidden = Object.keys(dirs.hidden);
console.debug('');
console.debug(`Hidden: ${hidden.length}`);
for (let name of hidden) {
let kind = dirs.hidden[name];
if (kind === '!directory') {
console.debug(` ${name}`);
} else {
console.debug(` ${name}/`);
}
}
}
{
let alias = Object.keys(dirs.alias);
console.debug('');
console.debug(`Alias: ${alias.length}`);
for (let name of alias) {
let kind = dirs.alias[name];
if (kind === 'symlink') {
console.debug(` ${name} => ...`);
} else {
console.debug(` ${name}/`);
}
}
}
{
let invalids = Object.keys(dirs.invalid);
console.warn('');
console.warn(`Invalid: ${invalids.length}`);
for (let name of invalids) {
console.warn(` ${name}/`);
}
}
{
let selfhosted = Object.keys(dirs.selfhosted);
console.info('');
console.info(`Self-Hosted: ${selfhosted.length}`);
for (let name of selfhosted) {
console.info(` ${name}/`);
}
}
{
let valids = Object.keys(dirs.valid);
console.info('');
console.info(`Found: ${valids.length}`);
for (let name of valids) {
console.info(` ${name}/`);
}
}
}
let bc = BuildsCacher.create({
caches: CACHE_DIR,
installers: INSTALLERS_DIR,
});
async function main() {
/* jshint maxcomplexity: 25 */
// TODO
// node ./_webi/lint-builds.js caddy@beta 'x86_64/unknown Darwin libc'
//
//let [projName, userAgent] = process.argv.slice(2);
let projName = process.argv[2];
// create test case for zoxide, goreleaser, go, yq, caddy, rg
let dirs = await bc.getProjectsByType();
if (!projName) {
showDirs(dirs);
console.info('');
}
let rows = [];
let triples = [];
let valids = Object.keys(dirs.valid);
if (projName) {
if (!valids.includes(projName)) {
throw new Error(`'${projName}' is not a valid installable project`);
}
valids = [projName];
}
//valids = ['atomicparsley', 'caddy', 'macos'];
//valids = ['atomicparsley'];
console.info('');
console.info(`Fetching project release assets`);
let parallel = 25;
let projects = [];
await Parallel.run(parallel, valids, getAll);
async function getAll(name, i) {
console.info(` ${name}`);
let projInfo = await bc.getPackages({
//Releases: Releases,
name: name,
date: new Date(),
});
projects[i] = projInfo;
}
console.info(`Classifying build assets for...`);
for (let projInfo of projects) {
console.info(` ${projInfo.name}`);
let nStr = projInfo.releases.length.toString();
let n = nStr.padStart(5, ' ');
let row = `##### ${n}\t${projInfo.name}\tv`;
rows.push(row);
// ignore known, non-package extensions
for (let build of projInfo.releases) {
let target = bc.classify(projInfo, build);
if (!target) {
// non-build file
continue;
}
if (target.error) {
let e = target.error;
if (e.code === 'E_BUILD_NO_PATTERN') {
console.warn(`>>> ${e.message} <<<`);
console.warn(projInfo);
console.warn(build);
console.warn(`^^^ ${e.message} ^^^`);
}
throw e;
}
if (target.unknownTerms?.length) {
let msg = `${projInfo.name}: unrecognized term(s) '${target.unknownTerms}' in '${build.download}'`;
let err = new Error(msg);
throw err;
}
triples.push(target.triplet);
// if (!build.version) {
// throw new Error(`no version for ${pkg.name} ${build.name}`);
// }
// // For debug printing versions
// console.error(build.version);
rows.push(`${target.triplet}\t${projInfo.name}\t${build.version}`);
}
}
console.info(`Fetching builds for`);
for (let projInfo of projects) {
console.info('');
console.info('');
console.info(` ${projInfo.name}`);
for (let target of uaTargets) {
let libc = target.libc || 'libc';
let hostTriplet = `${target.os}-${target.arch}-${libc}`;
console.info('');
console.info(` target: ${hostTriplet}`);
let match = bc.findMatchingPackages(projInfo, target, {
ver: '',
});
if (!match) {
console.info(
` project: ${projInfo.name}: missing build for os '${target.os}'`,
);
continue;
}
if (!match.releases) {
console.info(
` project: ${projInfo.name}: missing build for os '${target.os}-${target.arch}-${libc}'`,
);
} else if (match.triplet === hostTriplet) {
let releaseNames = Object.keys(match.releases);
console.info(` selected ${releaseNames.length}`);
} else {
let releaseNames = Object.keys(match.releases);
console.info(
` selected ${releaseNames.length} (${match.triplet} fallback)`,
);
}
}
}
let tsv = rows.join('\n');
console.info('');
console.info('#rows', rows.length);
await Fs.writeFile('builds.tsv', tsv, 'utf8');
console.info('');
console.info('Triplets Detected:');
let triplets = Object.keys(bc._triplets);
if (triplets.length) {
triplets.sort();
console.info(' ', triplets.join('\n '));
} else {
console.info(' (none)');
}
console.info('');
console.info('New / Unknown Terms:');
let unknowns = Object.keys(bc.unknownTerms);
if (unknowns.length) {
unknowns.sort();
console.warn(' ', unknowns.join('\n '));
} else {
console.info(' (none)');
}
console.info('');
console.info('Unused Terms:');
let unuseds = Object.keys(bc.orphanTerms);
if (unuseds.length) {
unuseds.sort();
console.warn(' ', unuseds.join('\n '));
} else {
console.info(' (none)');
}
console.info('');
console.info('Formats:');
if (bc.formats.length) {
let formats = bc.formats.slice();
formats.sort();
if (!formats[0]) {
formats[0] = '(bin)';
}
console.warn(' ', formats.join('\n '));
} else {
console.info(' (none)');
}
// sort -u -k1 builds.tsv | rg -v '^#|^https?:' | rg -i arm
// cut -f1 builds.tsv | sort -u -k1 | rg -v '^#|^https?:' | rg -i arm
}
if (module === require.main) {
let times = [];
let now = Date.now();
main()
.then(async function () {
let then = Date.now();
let delta = then - now;
times.push(delta);
now = then;
await main();
then = Date.now();
delta = then - now;
times.push(delta);
})
.then(function () {
console.info('');
console.info('Run times');
for (let delta of times) {
let s = delta / 1000;
console.info(` ${s}`);
}
function forceExit() {
console.warn(`warn: dangling event loop reference`);
process.exit(0);
}
let exitTimeout = setTimeout(forceExit, 250);
exitTimeout.unref();
})
.catch(function (err) {
console.error(err.stack || err);
process.exit(1);
});
}

171
_webi/normalize.js Normal file
View File

@@ -0,0 +1,171 @@
'use strict';
// this may need customizations between packages
var osMap = {
macos: /(\b|_)(apple|os(\s_-)?x\b|mac|darwin|iPhone|iOS|iPad)/i,
linux: /(\b|_)(linux)/i,
freebsd: /(\b|_)(freebsd)/i,
windows: /(\b|_)(win|microsoft|msft)/i,
sunos: /(\b|_)(sun)/i,
aix: /(\b|_)(aix)/i,
};
var maps = {
oses: {},
arches: {},
formats: {},
};
Object.keys(osMap).forEach(function (name) {
maps.oses[name] = true;
});
var formats = ['zip', 'xz', 'tar', 'pkg', 'msi', 'git', 'exe', 'dmg'];
formats.forEach(function (name) {
maps.formats[name] = true;
});
// evaluation order matters
// (i.e. otherwise x86 and x64 can cross match)
var arches = [
// arm 7 cannot be confused with arm64
'armv7l',
// amd64 is more likely than arm64
'amd64',
// arm6 has the same prefix as arm64
'armv6l',
// arm64 is more likely than arm6, and should be the default
'arm64',
'x86',
'ppc64le',
'ppc64',
's390x',
];
// Used for detecting system arch from package download url, for example:
//
// https://git.com/org/foo/releases/v0.7.9/foo-aarch64-linux-musl.tar.gz
// https://git.com/org/foo/releases/v0.7.9/foo-arm-linux-musleabihf.tar.gz
// https://git.com/org/foo/releases/v0.7.9/foo-armv7-linux-musleabihf.tar.gz
// https://git.com/org/foo/releases/v0.7.9/foo-x86_64-linux-musl.tar.gz
//
var archMap = {
armv7l: /(\b|_)(armv?7l?)/i,
//amd64: /(amd.?64|x64|[_\-]64)/i,
amd64:
/(\b|_|amd|(dar)?win(dows)?|mac(os)?|linux|osx|x)64([_\-]?bit)?(\b|_)/i,
//x86: /(86)(\b|_)/i,
armv6l: /(\b|_)(aarch32|armv?6l?)(\b|_)/i,
arm64: /(\b|_)((aarch|arm)64|arm)/i,
x86: /(\b|_|amd|(dar)?win(dows)?|mac(os)?|linux|osx|x)(86|32)([_\-]?bit)(\b|_)/i,
ppc64le: /(\b|_)(ppc64le)/i,
ppc64: /(\b|_)(ppc64)(\b|_)/i,
s390x: /(\b|_)(s390x)/i,
};
arches.forEach(function (name) {
maps.arches[name] = true;
});
function normalize(all) {
var supported = {
oses: {},
arches: {},
formats: {},
};
all.releases.forEach(function (rel) {
rel.version = rel.version.replace(/^v/i, '');
if (!rel.name) {
rel.name = rel.download.replace(/.*\//, '');
}
if (!rel.os) {
rel.os =
Object.keys(osMap).find(function (regKey) {
return osMap[regKey].test(rel.name || rel.download);
}) || 'unknown';
}
// Hacky-doo for musl
// TODO some sort of glibc vs musl tag?
if (!rel._musl_native) {
if (!rel._musl) {
if (/(\b|\.|_|-)(musl)(\b|\.|_|-)/.test(rel.download)) {
rel._musl = true;
}
}
}
supported.oses[rel.os] = true;
if (!rel.arch) {
arches.some(function (regKey) {
var arch = (rel.name || rel.download).match(archMap[regKey]) && regKey;
if (arch) {
rel.arch = arch;
return true;
}
});
}
if (!rel.arch) {
if ('macos' === rel.os) {
rel.arch = 'amd64';
}
}
supported.arches[rel.arch] = true;
var tarExt;
if (!rel.ext) {
// pkg-v1.0.tar.gz => ['gz', 'tar', '0', 'pkg-v1']
// pkg-v1.0.tar => ['tar', '0' ,'pkg-v1']
// pkg-v1.0.zip => ['zip', '0', 'pkg-v1']
var exts = (rel.name || rel.download).split('.');
if (1 === exts.length) {
// for bare releases in the format of foo-linux-amd64
rel.ext = 'exe';
}
exts = exts.reverse().slice(0, 2);
if ('tar' === exts[1]) {
rel.ext = exts.reverse().join('.');
tarExt = 'tar';
} else if ('tgz' === exts[0]) {
rel.ext = 'tar.gz';
tarExt = 'tar';
} else {
rel.ext = exts[0];
}
if (/\-|linux|mac|os[_\-]?x|arm|amd|86|64|mip/i.test(rel.ext)) {
// for bare releases in the format of foo.linux-amd64
rel.ext = 'exe';
}
}
supported.formats[tarExt || rel.ext] = true;
if (all.download) {
rel.download = all.download.replace(/{{ download }}/, rel.download);
}
});
all.oses = Object.keys(supported.oses).filter(function (name) {
return maps.oses[name];
});
all.arches = Object.keys(supported.arches).filter(function (name) {
return maps.arches[name];
});
all.formats = Object.keys(supported.formats).filter(function (name) {
return maps.formats[name];
});
return all;
}
module.exports = normalize;
module.exports._debug = function (all) {
all = normalize(all);
all.releases = all.releases
.filter(function (r) {
return ['windows', 'macos', 'linux'].includes(r.os) && 'amd64' === r.arch;
})
.slice(0, 10);
return all;
};
// NOT in order of priority (which would be tar, xz, zip, ...)
module.exports.formats = formats;
module.exports.arches = arches;
module.exports.formatsMap = maps.formats;

View File

@@ -1,155 +0,0 @@
#!/usr/bin/env pwsh
#350 check if windows user run as admin
# this allows us to call ps1 files, which allows us to have spaces in filenames
# ('powershell "$Env:USERPROFILE\test.ps1" foo' will fail if it has a space in
# the path but '& "$Env:USERPROFILE\test.ps1" foo' will work even with a space)
Set-ExecutionPolicy -Scope Process Bypass
# If a command returns an error, halt the script.
$ErrorActionPreference = 'Stop'
# Ignore progress events from cmdlets so Invoke-WebRequest is not painfully slow
$ProgressPreference = 'SilentlyContinue'
$Env:WEBI_HOST = 'https://webinstall.dev'
#$Env:WEBI_PKG = 'node@lts'
#$Env:PKG_NAME = node
#$Env:WEBI_VERSION = v12.16.2
#$Env:WEBI_GIT_TAG = 12.16.2
#$Env:WEBI_PKG_URL = "https://.../node-....zip"
#$Env:WEBI_PKG_FILE = "node-v12.16.2-win-x64.zip"
#$Env:WEBI_PKG_PATHNAME = "node-v12.16.2-win-x64.zip"
# Switch to userprofile
Push-Location $Env:USERPROFILE
# Make paths
New-Item -Path "$Env:USERPROFILE\Downloads" -ItemType Directory -Force | Out-Null
New-Item -Path "$Env:USERPROFILE\.local\bin" -ItemType Directory -Force | Out-Null
New-Item -Path "$Env:USERPROFILE\.local\opt" -ItemType Directory -Force | Out-Null
# {{ baseurl }}
# {{ version }}
# See
# - <https://superuser.com/q/1264444>
# - <https://stackoverflow.com/a/60572643/151312>
$Esc = [char]27
$TTask = "${Esc}[36m"
$TName = "${Esc}[1m${Esc}[32m"
$TUrl = "${Esc}[2m"
$TPath = "${Esc}[2m${Esc}[32m"
$TCmd = "${Esc}[2m${Esc}[35m"
$TDim = "${Esc}[2m"
$TReset = "${Esc}[0m"
function Invoke-DownloadUrl {
param (
[string]$URL,
[string]$Params,
[string]$Path,
[switch]$Force
)
if (Test-Path -Path "$Path") {
if (-not $Force.IsPresent) {
Write-Host " ${TDim}Found${TReset} $Path"
return
}
Write-Host " Updating ${TDim}${Path}${TDim}"
}
$TmpPath = "${Path}.part"
Remove-Item -Path $TmpPath -Force -ErrorAction Ignore
Write-Host " Downloading ${TDim}from${TReset}"
Write-Host " ${TDim}${URL}${TReset}"
if ($Params.Length -ne 0) {
Write-Host " ?$Params"
$URL = "${URL}?${Params}"
}
curl.exe '-#' -f -sS -A $Env:WEBI_UA -L $URL | Out-File $TmpPath
Remove-Item -Path $Path -Force -ErrorAction Ignore
Move-Item $TmpPath $Path
Write-Host " Saved ${TPath}${Path}${TReset}"
}
function Get-UserAgent {
# This is the canonical CPU arch when the process is emulated
$my_arch = "$Env:PROCESSOR_ARCHITEW6432"
if ($my_arch -eq $null -or $my_arch -eq "") {
# This is the canonical CPU arch when the process is native
$my_arch = "$Env:PROCESSOR_ARCHITECTURE"
}
if ($my_arch -eq "AMD64") {
# Because PowerShell is sometimes AMD64 on Windows 10 ARM
# See https://oofhours.com/2020/02/04/powershell-on-windows-10-arm64/
$my_os_arch = (Get-CimInstance -ClassName Win32_OperatingSystem).OSArchitecture
# Using -clike because of the trailing newline
if ($my_os_arch -clike "ARM 64*") {
$my_arch = "ARM64"
}
}
"PowerShell+curl Windows/10+ $my_arch msvc"
}
function webi_path_add($pathname) {
# C:\Users\me => C:/Users/me
$my_home = $Env:UserProfile
$my_home = $my_home.replace('\\', '/')
$my_home_re = [regex]::escape($my_home)
# ~/bin => %USERPROFILE%/bin
$pathname = $pathname.replace('~/', "$Env:UserProfile/")
# C:\Users\me\bin => %USERPROFILE%/bin
$my_pathname = $pathname.replace('\\', '/')
$my_pathname = $my_pathname -ireplace $my_home_re, "%USERPROFILE%"
$all_user_paths = [Environment]::GetEnvironmentVariable("Path", "User")
$user_paths = "${all_user_paths}".Trim(';').Split(';')
$exists_in_path = $false
foreach ($user_path in $user_paths) {
# C:\Users\me\bin => %USERPROFILE%/bin
$my_user_path = $user_path.replace('\\', '/')
$my_user_path = $my_user_path -ireplace $my_home_re, "%USERPROFILE%"
if ($my_user_path -ieq $my_pathname) {
$exists_in_path = $true
}
}
if (-not $exists_in_path) {
$all_user_paths = "${pathname};${all_user_paths}".Trim(';')
[Environment]::SetEnvironmentVariable("Path", $all_user_paths, "User")
$null = Sync-EnvPath
}
}
function Sync-EnvPath {
$UserPath = [Environment]::GetEnvironmentVariable("Path", "User").Trim(';')
$MachinePath = [Environment]::GetEnvironmentVariable("Path", "Machine").Trim(';')
$Env:Path = "${UserPath};${MachinePath}"
[Environment]::SetEnvironmentVariable("Path", $Env:Path)
$Env:Path
}
$Env:WEBI_UA = Get-UserAgent
#$has_local_bin = echo "$Env:PATH" | Select-String -Pattern '\.local.bin'
#if (!$has_local_bin)
#{
webi_path_add ~/.local/bin
#}
# {{ installer }}
webi_path_add ~/.local/bin
# Done
Pop-Location

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
projects.js

96
_webi/packages.js Normal file
View File

@@ -0,0 +1,96 @@
'use strict';
var frontmarker = require('./frontmarker.js');
var fs = require('fs');
var path = require('path');
var pkgs = module.exports;
pkgs.create = function (Pkgs, basepath) {
if (!Pkgs) {
Pkgs = {};
}
if (!basepath) {
basepath = path.join(__dirname, '../');
}
Pkgs.all = function () {
return fs.promises.readdir(basepath).then(function (nodes) {
var items = [];
return nodes
.reduce(function (p, node) {
return p.then(function () {
return pkgs.get(node).then(function (meta) {
if (meta && '_' !== node[0]) {
meta.name = node;
items.push(meta);
}
});
});
}, Promise.resolve())
.then(function () {
return items;
});
});
};
Pkgs.get = function (node) {
return fs.promises.access(path.join(basepath, node)).then(function () {
return Pkgs._get(node);
});
};
Pkgs._get = function (node) {
var curlbash = path.join(basepath, node, 'install.sh');
var readme = path.join(basepath, node, 'README.md');
var winstall = path.join(basepath, node, 'install.ps1');
return Promise.all([
fs.promises
.readFile(readme, 'utf-8')
.then(function (txt) {
// TODO
return frontmarker.parse(txt);
})
.catch(function (e) {
if ('ENOENT' !== e.code && 'ENOTDIR' !== e.code) {
console.error("failed to read '" + node + "/README.md'");
console.error(e);
}
}),
fs.promises.access(curlbash).catch(function (e) {
// no *nix installer
curlbash = '';
if ('ENOENT' !== e.code && 'ENOTDIR' !== e.code) {
console.error("failed to parse '" + node + "/install.sh'");
console.error(e);
}
}),
fs.promises.access(winstall).catch(function (e) {
// no winstaller
winstall = '';
if ('ENOENT' !== e.code && 'ENOTDIR' !== e.code) {
console.error("failed to read '" + node + "/install.ps1'");
console.error(e);
}
}),
]).then(function (items) {
var meta = items[0] || items[1];
if (!meta) {
// doesn't exist
return;
}
meta.windows = !!winstall;
meta.bash = !!curlbash;
return meta;
});
};
return Pkgs;
};
pkgs.create(pkgs);
if (module === require.main) {
pkgs.all().then(function (data) {
console.info('package info:');
console.info(data);
});
}

View File

@@ -1,44 +0,0 @@
'use strict';
var Parallel = module.exports;
Parallel.run = async function (limit, arr, fn) {
let index = 0;
let actives = [];
let results = [];
limit = Math.min(limit, arr.length);
function launch() {
let _index = index;
let p = fn(arr[_index], _index, arr);
// some tasks may be synchronous
// so we must push before removing
actives.push(p);
p.then(function _resolve(result) {
let i = actives.indexOf(p);
actives.splice(i, 1);
results[_index] = result;
});
index += 1;
}
// start tasks in parallel, up to limit
for (; actives.length < limit; ) {
launch();
}
// keep the task queue full
for (; index < arr.length; ) {
// wait for one task to complete
await Promise.race(actives);
// add one task again
launch();
}
// wait for all remaining tasks
await Promise.all(actives);
return results;
};

View File

@@ -1,96 +0,0 @@
'use strict';
var frontmarker = require('./frontmarker.js');
var fs = require('fs');
var path = require('path');
var pkgs = module.exports;
pkgs.create = function (Pkgs, basepath) {
if (!Pkgs) {
Pkgs = {};
}
if (!basepath) {
basepath = path.join(__dirname, '../');
}
Pkgs.all = function () {
return fs.promises.readdir(basepath).then(function (nodes) {
var items = [];
return nodes
.reduce(function (p, node) {
return p.then(function () {
return pkgs.get(node).then(function (meta) {
if (meta && '_' !== node[0]) {
meta.name = node;
items.push(meta);
}
});
});
}, Promise.resolve())
.then(function () {
return items;
});
});
};
Pkgs.get = function (node) {
return fs.promises.access(path.join(basepath, node)).then(function () {
return Pkgs._get(node);
});
};
Pkgs._get = function (node) {
var curlbash = path.join(basepath, node, 'install.sh');
var readme = path.join(basepath, node, 'README.md');
var winstall = path.join(basepath, node, 'install.ps1');
return Promise.all([
fs.promises
.readFile(readme, 'utf-8')
.then(function (txt) {
// TODO
return frontmarker.parse(txt);
})
.catch(function (e) {
if ('ENOENT' !== e.code && 'ENOTDIR' !== e.code) {
console.error("failed to read '" + node + "/README.md'");
console.error(e);
}
}),
fs.promises.access(curlbash).catch(function (e) {
// no *nix installer
curlbash = '';
if ('ENOENT' !== e.code && 'ENOTDIR' !== e.code) {
console.error("failed to parse '" + node + "/install.sh'");
console.error(e);
}
}),
fs.promises.access(winstall).catch(function (e) {
// no winstaller
winstall = '';
if ('ENOENT' !== e.code && 'ENOTDIR' !== e.code) {
console.error("failed to read '" + node + "/install.ps1'");
console.error(e);
}
}),
]).then(function (items) {
var meta = items[0] || items[1];
if (!meta) {
// doesn't exist
return;
}
meta.windows = !!winstall;
meta.bash = !!curlbash;
return meta;
});
};
return Pkgs;
};
pkgs.create(pkgs);
if (module === require.main) {
pkgs.all().then(function (data) {
console.info('package info:');
console.info(data);
});
}

258
_webi/releases.js Normal file
View File

@@ -0,0 +1,258 @@
'use strict';
var fs = require('node:fs');
var path = require('node:path');
var request = require('@root/request');
var _normalize = require('../_webi/normalize.js');
var reInstallTpl = /\s*#?\s*{{ installer }}/;
var Releases = module.exports;
Releases.get = async function (pkgdir) {
let get;
try {
get = require(path.join(pkgdir, 'releases.js'));
} catch (e) {
let err = new Error('no releases.js for', pkgdir.split(/[\/\\]+/).pop());
err.code = 'E_NO_RELEASE';
throw err;
}
let all = await get(request);
return _normalize(all);
};
function padScript(txt) {
return txt.replace(/^/g, ' ');
}
Releases.renderBash = async function (
pkgdir,
rel,
{ baseurl, pkg, tag, ver, os = '', arch = '', formats },
) {
if (!Array.isArray(formats)) {
formats = [];
}
if (!tag) {
tag = '';
}
return fs.promises
.readFile(path.join(pkgdir, 'install.sh'), 'utf8')
.then(function (installTxt) {
installTxt = padScript(installTxt);
var vers = rel.version.split('.');
var v = {
major: vers.shift() || '',
minor: vers.shift() || '',
patch: vers.join('.').replace(/[+\-].*/, ''),
build: vers
.join('.')
.replace(/[^+\-]*/, '')
.replace(/^-/, ''),
};
var pkgFile = rel.filename || rel.name;
return fs.promises
.readFile(path.join(__dirname, 'template.sh'), 'utf8')
.then(function (tplTxt) {
// ex: 'node@lts' or 'node'
var webiPkg = pkg;
if (ver) {
webiPkg += `@${ver}`;
}
return (
tplTxt
.replace(/CHEATSHEET_URL/g, `${baseurl}/${pkg}`)
.replace(/^\s*#?WEBI_PKG=.*/m, `WEBI_PKG='${webiPkg}'`)
.replace(/^\s*#?WEBI_HOST=.*/m, `WEBI_HOST='${baseurl}'`)
.replace(/^\s*#?WEBI_OS=.*/m, `WEBI_OS='${os}'`)
.replace(/^\s*#?WEBI_ARCH=.*/m, `WEBI_ARCH='${arch}'`)
.replace(/^\s*#?WEBI_TAG=.*/m, `WEBI_TAG='${tag}'`)
.replace(
/^\s*#?WEBI_RELEASES=.*/m,
"WEBI_RELEASES='" +
baseurl +
'/api/releases/' +
pkg +
'@' +
tag +
'.tab?os=' +
rel.os +
'&arch=' +
rel.arch +
'&formats=' +
formats.join(',') +
'&pretty=true' +
"'",
)
.replace(
/^\s*#?WEBI_CSV=.*/m,
"WEBI_CSV='" +
[
rel.version,
rel.lts,
rel.channel,
rel.date,
rel.os,
rel.arch,
rel.ext,
'-',
rel.download,
rel.name,
rel.comment || '',
]
.join(',')
.replace(/'/g, '') +
"'",
)
.replace(
/^\s*#?WEBI_VERSION=.*/m,
'WEBI_VERSION=' + JSON.stringify(rel.version),
)
.replace(/^\s*#?WEBI_MAJOR=.*/m, 'WEBI_MAJOR=' + v.major)
.replace(/^\s*#?WEBI_MINOR=.*/m, 'WEBI_MINOR=' + v.minor)
.replace(/^\s*#?WEBI_PATCH=.*/m, 'WEBI_PATCH=' + v.patch)
.replace(/^\s*#?WEBI_BUILD=.*/m, 'WEBI_BUILD=' + v.build)
.replace(/^\s*#?WEBI_LTS=.*/m, 'WEBI_LTS=' + rel.lts)
.replace(/^\s*#?WEBI_CHANNEL=.*/m, 'WEBI_CHANNEL=' + rel.channel)
.replace(
/^\s*#?WEBI_EXT=.*/m,
'WEBI_EXT=' + rel.ext.replace(/tar.*/, 'tar'),
)
.replace(
/^\s*#?WEBI_FORMATS=.*/m,
"WEBI_FORMATS='" + formats.join(',') + "'",
)
.replace(
/^\s*#?WEBI_PKG_URL=.*/m,
"WEBI_PKG_URL='" + rel.download + "'",
)
.replace(
/^\s*#?WEBI_PKG_FILE=.*/m,
"WEBI_PKG_FILE='" + pkgFile + "'",
)
// PKG details
.replace(/^\s*#?PKG_NAME=.*/m, "PKG_NAME='" + pkg + "'")
.replace(
/^\s*#?PKG_OSES=.*/m,
"PKG_OSES='" + ((rel && rel.oses) || []).join(',') + "'",
)
.replace(
/^\s*#?PKG_ARCHES=.*/m,
"PKG_ARCHES='" + ((rel && rel.arches) || []).join(',') + "'",
)
.replace(
/^\s*#?PKG_FORMATS=.*/m,
"PKG_FORMATS='" + ((rel && rel.formats) || []).join(',') + "'",
)
// $', $0, ... $9, $`, $&, and $_ all have special meaning
// (see https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp)
// However, it can be escaped with $$ (which must be escaped with $$)
.replace(reInstallTpl, '\n' + installTxt.replace(/\$/g, '$$$$'))
);
});
});
};
Releases.renderBatch = async function (
pkgdir,
rel,
{ baseurl, pkg, tag, ver, os, arch, formats },
) {
if (!Array.isArray(formats)) {
formats = [];
}
if (!tag) {
tag = '';
}
return fs.promises
.readFile(path.join(pkgdir, 'install.bat'), 'utf8')
.then(function (installTxt) {
installTxt = padScript(installTxt);
/*
var vers = rel.version.split('.');
var v = {
major: vers.shift() || '',
minor: vers.shift() || '',
patch: vers.join('.').replace(/[+\-].*$/, ''),
build: vers
.join('.')
.replace(/[^+\-]*()/, '')
.replace(/^-/, '')
};
*/
return fs.promises
.readFile(path.join(__dirname, 'template.bat'), 'utf8')
.then(function (tplTxt) {
return tplTxt
.replace(
/^(REM )?WEBI_PKG=.*/im,
"WEBI_PKG='" + pkg + '@' + ver + "'",
)
.replace(reInstallTpl, '\n' + installTxt);
});
});
};
Releases.renderPowerShell = async function (
pkgdir,
rel,
{ baseurl, pkg, tag, ver, os, arch, formats },
) {
if (!Array.isArray(formats)) {
formats = [];
}
if (!tag) {
tag = '';
}
return fs.promises
.readFile(path.join(pkgdir, 'install.ps1'), 'utf8')
.then(function (installTxt) {
installTxt = padScript(installTxt);
/*
var vers = rel.version.split('.');
var v = {
major: vers.shift() || '',
minor: vers.shift() || '',
patch: vers.join('.').replace(/[+\-].*$/, ''),
build: vers
.join('.')
.replace(/[^+\-]*()/, '')
.replace(/^-/, '')
};
*/
return fs.promises
.readFile(path.join(__dirname, 'template.ps1'), 'utf8')
.then(function (tplTxt) {
var pkgver = pkg + '@' + ver;
return tplTxt
.replace(
/^(#)?\$Env:WEBI_HOST\s*=.*/im,
"$Env:WEBI_HOST = '" + baseurl + "'",
)
.replace(
/^(#)?\$Env:WEBI_PKG\s*=.*/im,
"$Env:WEBI_PKG = '" + pkgver + "'",
)
.replace(
/^(#)?\$Env:PKG_NAME\s*=.*/im,
"$Env:PKG_NAME = '" + pkg + "'",
)
.replace(
/^(#)?\$Env:WEBI_VERSION\s*=.*/im,
"$Env:WEBI_VERSION = '" + rel.version + "'",
)
.replace(
/^(#)?\$Env:WEBI_PKG_URL\s*=.*/im,
"$Env:WEBI_PKG_URL = '" + rel.download + "'",
)
.replace(
/^(#)?\$Env:WEBI_PKG_FILE\s*=.*/im,
"$Env:WEBI_PKG_FILE = '" + rel.name + "'",
)
.replace(reInstallTpl, '\n' + installTxt);
});
});
};

View File

@@ -1,304 +1,89 @@
'use strict';
var InstallerServer = module.exports;
var path = require('path');
let Fs = require('fs/promises');
let Path = require('path');
var uaDetect = require('./ua-detect.js');
var packages = require('./packages.js');
var Releases = require('./releases.js');
let HostTargets = require('./build-classifier/host-targets.js');
let Builds = require('./builds.js');
let Installers = require('./installers.js');
// handlers caching and transformation, probably should be broken down
var getReleases = require('./transform-releases.js');
InstallerServer.INSTALLERS_DIR = Path.join(__dirname, '..');
InstallerServer.serveInstaller = async function (
baseurl,
ua,
pkg,
tag,
ext,
formats,
libc,
) {
let unameAgent = ua;
let projectName = pkg;
let [rel, tmplParams] = await InstallerServer.helper({
unameAgent,
projectName,
tag,
var installersDir = path.join(__dirname, '..');
serveInstaller.serveInstaller = serveInstaller;
module.exports = serveInstaller;
async function serveInstaller(baseurl, ua, pkg, tag, ext, formats) {
// TODO put some of this in a middleware? or common function?
var ver = tag.replace(/^v/, '');
var lts;
var channel;
switch (ver) {
case 'latest':
ver = '';
channel = 'stable';
break;
case 'lts':
lts = true;
channel = 'stable';
ver = '';
break;
case 'stable':
channel = 'stable';
ver = '';
break;
case 'beta':
channel = 'beta';
ver = '';
break;
case 'dev':
channel = 'dev';
ver = '';
break;
}
// TODO maybe move package/version/lts/channel detection into getReleases
var myOs = uaDetect.os(ua);
var myArch = uaDetect.arch(ua);
var myLibc = uaDetect.libc(ua);
let cfg = await packages.get(pkg);
let rels = await getReleases({
pkg: cfg.alias || pkg,
ver,
os: myOs,
arch: myArch,
libc: myLibc,
lts,
channel,
formats,
libc,
limit: 1,
});
Object.assign(tmplParams, {
var rel = rels.releases[0];
var pkgdir = path.join(installersDir, pkg);
var opts = {
baseurl,
});
var pkgdir = Path.join(InstallerServer.INSTALLERS_DIR, projectName);
if ('ps1' === ext) {
return Installers.renderPowerShell(pkgdir, rel, tmplParams);
}
return Installers.renderBash(pkgdir, rel, tmplParams);
};
// TODO put some of this in a middleware? or common function?
// TODO maybe move package/version/lts/channel detection into getReleases
InstallerServer.helper = async function ({
unameAgent,
projectName,
tag,
formats,
libc,
}) {
console.log(`dbg: Installer User-Agent: ${unameAgent}`);
let releaseTarget = toReleaseTarget(tag);
let hostFormats = formats;
let terms = unameAgent.split(/[\s\/]+/g);
let hostTarget = {};
try {
void HostTargets.termsToTarget(hostTarget, terms);
} catch (e) {
// if we can't guarantee the results...
// "in the face of ambiguity, refuse the temptation to guess"
throw e;
}
console.log(`dbg: Installer Host Target:`);
console.log(hostTarget);
if (!hostTarget.os) {
throw new Error(`OS could not be identified by User-Agent '${unameAgent}'`);
}
console.log(`dbg: Get Project Installer Type for '${projectName}':`);
let proj = await Builds.getProjectType(projectName);
if (proj.type === 'alias') {
console.log(`dbg: alias`, proj);
projectName = proj.detail;
proj = await Builds.getProjectType(projectName); // an alias should never resolve to an alias
}
console.log(`dbg: proj`, proj);
let validTypes = ['selfhosted', 'valid'];
if (!validTypes.includes(proj.type)) {
let msg = `'${projectName}' doesn't have an installer: '${proj.type}': '${proj.detail}'`;
let err = new Error(msg);
err.code = 'ENOENT';
throw err;
}
let tmplParams = {
pkg: projectName,
tag: tag,
os: hostTarget.os,
arch: hostTarget.arch,
libc: hostTarget.libc,
formats: hostFormats,
pkg: cfg.alias || pkg,
ver,
tag,
os: myOs,
arch: myArch,
lts,
channel,
formats,
limit: 1,
};
Object.assign(tmplParams, releaseTarget);
console.log('tmplParams', tmplParams);
rel.oses = rels.oses;
rel.arches = rels.arches;
rel.formats = rels.formats;
let errPackage = {
name: 'doesntexist.ext',
version: '0.0.0',
lts: '-',
channel: 'error',
date: '1970-01-01',
os: hostTarget.os || '-',
arch: hostTarget.arch || '-',
libc: hostTarget.libc || '-',
ext: 'err',
download: 'https://example.com/doesntexist.ext',
comment:
'No matches found. Could be bad or missing version info' +
',' +
"Check query parameters. Should be something like '/api/releases/{package}@{version}.tab?os={macos|linux|windows|-}&arch={amd64|x86|aarch64|arm64|armv7l|-}&libc={musl|gnu|msvc|libc|static}&limit=10'",
};
if (proj.type === 'selfhosted') {
return [errPackage, tmplParams];
if ('bat' === ext) {
return Releases.renderBatch(pkgdir, rel, opts);
}
let projInfo = await Builds.getPackage({
name: projectName,
date: new Date(),
});
let latestVersions = Builds.enumerateLatestVersions(projInfo);
//console.log('projInfo', projInfo);
let buildTargetInfo = {
triplets: projInfo.triplets,
oses: projInfo.oses,
arches: projInfo.arches,
libcs: projInfo.libcs,
formats: projInfo.formats,
latest: latestVersions.latest,
stable: latestVersions.stable,
};
// TODO .findMatchingPackages() should probably account for this
let hasOs = projInfo.oses.includes(hostTarget.os);
let maybePosix = !hasOs && hostTarget.os !== 'windows';
if (maybePosix) {
let posixes = ['posix_2017', 'posix_2024'];
for (let posixYear of posixes) {
let hasPosix = projInfo.oses.includes(posixYear);
if (hasPosix) {
hasOs = true;
break;
}
}
if ('ps1' === ext) {
return Releases.renderPowerShell(pkgdir, rel, opts);
}
if (!hasOs) {
hasOs = projInfo.oses.includes('ANYOS');
}
if (!hasOs) {
let pkg1 = Object.assign(buildTargetInfo, errPackage);
return [pkg1, tmplParams];
}
let targetRelease = Builds.findMatchingPackages(
projInfo,
hostTarget,
releaseTarget,
);
// { triplet: `${os}-${arch}-${libc}`, packages: targetPackages
// , latest: projInfo.versions[0], versions: matchInfo
// }
if (!targetRelease?.packages) {
let pkg1 = Object.assign(buildTargetInfo, errPackage);
return [pkg1, tmplParams];
}
let buildPkg = Builds.selectPackage(targetRelease.packages, hostFormats);
let ext = buildPkg.ext || '.exe';
if (ext.startsWith('.')) {
ext = ext.slice(1);
}
let version = targetRelease.version;
if (version.startsWith('v')) {
version = version.slice(1);
}
buildPkg = Object.assign(buildTargetInfo, buildPkg, { ext, version });
console.log('dbg: buildPkg', buildPkg);
console.log('dbg: tmplParams', tmplParams);
return [buildPkg, tmplParams];
};
let channelNames = [
'stable',
// 'hotfix',
'latest',
'rc',
'preview',
'pre',
'dev',
'beta',
'alpha',
];
function toReleaseTarget(tag) {
tag = tag.replace(/^v/, '');
let releaseTarget = {
channel: '',
lts: false,
version: '',
};
if (tag === 'lts') {
releaseTarget.lts = true;
releaseTarget.channel = 'stable';
} else if (channelNames.includes(tag)) {
releaseTarget.channel = tag;
} else {
releaseTarget.version = tag;
}
return releaseTarget;
return Releases.renderBash(pkgdir, rel, opts);
}
var CURL_PIPE_PS1_BOOT = Path.join(__dirname, 'curl-pipe-bootstrap.tpl.ps1');
var CURL_PIPE_SH_BOOT = Path.join(__dirname, 'curl-pipe-bootstrap.tpl.sh');
var BAD_SH_RE = /[<>'"`$\\]/;
InstallerServer.getPosixCurlPipeBootstrap = async function ({
baseurl,
pkg,
ver,
}) {
let bootTxt = await Fs.readFile(CURL_PIPE_SH_BOOT, 'utf8');
var webiPkg = [pkg, ver].filter(Boolean).join('@');
var webiChecksum = await Installers.getWebiShChecksum();
var envReplacements = [
['WEBI_PKG', webiPkg],
['WEBI_HOST', baseurl],
['WEBI_CHECKSUM', webiChecksum],
];
for (let env of envReplacements) {
let name = env[0];
let value = env[1];
let envRe = new RegExp(
`^[ \\t]*#?[ \\t]*(export[ \\t])?[ \\t]*(${name})=.*`,
'm',
);
if (BAD_SH_RE.test(value)) {
throw new Error(`key '${name}' has invalid value '${value}'`);
}
bootTxt = bootTxt.replace(envRe, `$1$2='${value}'`);
}
// TODO init config here
//bootTxt.replace(/CHEATSHEET_URL/g, `${Config.cheatUrl}/${pkg}`);
return bootTxt;
};
InstallerServer.getPwshCurlPipeBootstrap = async function ({
baseurl,
pkg,
ver,
exename,
}) {
let bootTxt = await Fs.readFile(CURL_PIPE_PS1_BOOT, 'utf8');
var webiPkg = [pkg, ver].filter(Boolean).join('@');
//var webiChecksum = await InstallerServer.getWebiPs1Checksum();
var envReplacements = [
['Env:WEBI_PKG', webiPkg],
['Env:WEBI_HOST', baseurl],
//['Env:WEBI_CHECKSUM', webiChecksum],
['baseurl', baseurl],
['exename', exename],
['version', ver],
];
for (let env of envReplacements) {
let name = env[0];
let value = env[1];
if (BAD_SH_RE.test(value)) {
throw new Error(`key '${name}' has invalid value '${value}'`);
}
let tplRe = new RegExp(`{{ (${name}) }}`, 'g');
bootTxt = bootTxt.replace(tplRe, `${value}`);
let setRe = new RegExp(
`(#[ \\t]*)?(\\$${name})[ \\t]*=[ \\t]['"].*['"][ \\t]`,
'im',
);
bootTxt = bootTxt.replace(setRe, `$$${name} = '${value}'`);
}
// TODO init config here
//bootTxt.replace(/CHEATSHEET_URL/g, `${Config.cheatUrl}/${pkg}`);
return bootTxt;
};

5
_webi/template.bat Normal file
View File

@@ -0,0 +1,5 @@
REM REM debug
REM WEBI_PKG=
{{ installer }}

128
_webi/template.ps1 Normal file
View File

@@ -0,0 +1,128 @@
#!/usr/bin/env pwsh
#350 check if windows user run as admin
function Confirm-IsElevated {
$id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$p = New-Object System.Security.Principal.WindowsPrincipal($id)
if ($p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator))
{ Write-Output $true }
else
{ Write-Output $false }
}
if (Confirm-IsElevated)
{ throw "Webi MUST NOT be run with elevated privileges. Please run again as a normal user, NOT as administrator." }
# this allows us to call ps1 files, which allows us to have spaces in filenames
# ('powershell "$Env:USERPROFILE\test.ps1" foo' will fail if it has a space in
# the path but '& "$Env:USERPROFILE\test.ps1" foo' will work even with a space)
Set-ExecutionPolicy -Scope Process Bypass
# If a command returns an error, halt the script.
$ErrorActionPreference = 'Stop'
# Ignore progress events from cmdlets so Invoke-WebRequest is not painfully slow
$ProgressPreference = 'SilentlyContinue'
$Env:WEBI_HOST = 'https://webinstall.dev'
#$Env:WEBI_PKG = 'node@lts'
#$Env:PKG_NAME = node
#$Env:WEBI_VERSION = v12.16.2
#$Env:WEBI_PKG_URL = "https://.../node-....zip"
#$Env:WEBI_PKG_FILE = "node-v12.16.2-win-x64.zip"
# Switch to userprofile
pushd $Env:USERPROFILE
# Make paths
New-Item -Path Downloads -ItemType Directory -Force | out-null
New-Item -Path .local\bin -ItemType Directory -Force | out-null
New-Item -Path .local\opt -ItemType Directory -Force | out-null
# {{ baseurl }}
# {{ version }}
function webi_path_add($pathname)
{
# C:\Users\me => C:/Users/me
$my_home = $Env:UserProfile
$my_home = $my_home.replace('\\', '/')
$my_home_re = [regex]::escape($my_home)
# ~/bin => %USERPROFILE%/bin
$pathname = $pathname.replace('~/', "$Env:UserProfile/")
# C:\Users\me\bin => %USERPROFILE%/bin
$my_pathname = $pathname.replace('\\', '/')
$my_pathname = $my_pathname -ireplace $my_home_re, "%USERPROFILE%"
$all_user_paths = [Environment]::GetEnvironmentVariable("Path", "User")
$user_paths = $all_user_paths -Split(';')
$exists_in_path = $false
foreach ($user_path in $user_paths)
{
# C:\Users\me\bin => %USERPROFILE%/bin
$my_user_path = $user_path.replace('\\', '/')
$my_user_path = $my_user_path -ireplace $my_home_re, "%USERPROFILE%"
if ($my_user_path -ieq $my_pathname)
{
$exists_in_path = $true
}
}
if (-Not $exists_in_path)
{
$all_user_paths = $pathname + ";" + $all_user_paths
[Environment]::SetEnvironmentVariable("Path", $all_user_paths, "User")
}
$session_paths = $Env:Path -Split(';')
$in_session_path = $false
foreach ($session_path in $session_paths)
{
# C:\Users\me\bin => %USERPROFILE%/bin
$my_session_path = $session_path.replace('\\', '/')
$my_session_path = $my_session_path -ireplace $my_home_re, "%USERPROFILE%"
if ($my_session_path -ieq $my_pathname)
{
$in_session_path = $true
}
}
if (-Not $in_session_path)
{
$my_cmd = 'PATH ' + "$pathname" + ';%PATH%'
$my_pwsh = '$Env:Path = "' + "$pathname" + ';$Env:Path"'
Write-Host ''
Write-Host '**********************************' -ForegroundColor red -BackgroundColor white
Write-Host '* IMPORTANT -- READ ME *' -ForegroundColor red -BackgroundColor white
Write-Host '* (run the PATH command below) *' -ForegroundColor red -BackgroundColor white
Write-Host '**********************************' -ForegroundColor red -BackgroundColor white
Write-Host ''
echo ""
echo "Copy, paste, and run the appropriate command to update your PATH:"
echo "(or close and reopen the terminal, or reboot)"
echo ""
echo "cmd.exe:"
echo " $my_cmd"
echo ""
echo "PowerShell:"
echo " $my_pwsh"
echo ""
}
}
#$has_local_bin = echo "$Env:PATH" | Select-String -Pattern '\.local.bin'
#if (!$has_local_bin)
#{
webi_path_add ~/.local/bin
#}
{{ installer }}
webi_path_add ~/.local/bin
# Done
popd

625
_webi/template.sh Normal file
View File

@@ -0,0 +1,625 @@
#!/bin/sh
__bootstrap_webi() {
set -e
set -u
#set -x
my_libc=''
if ldd /bin/ls 2> /dev/null | grep -q 'musl' 2> /dev/null; then
my_libc=' musl-native'
fi
#WEBI_PKG=
#PKG_NAME=
# TODO should this be BASEURL instead?
#WEBI_OS=
#WEBI_ARCH=
#WEBI_HOST=
#WEBI_RELEASES=
#WEBI_CSV=
#WEBI_TAG=
#WEBI_VERSION=
#WEBI_MAJOR=
#WEBI_MINOR=
#WEBI_PATCH=
# TODO not sure if BUILD is the best name for this
#WEBI_BUILD=
#WEBI_LTS=
#WEBI_CHANNEL=
#WEBI_EXT=
#WEBI_FORMATS=
#WEBI_PKG_URL=
#WEBI_PKG_FILE=
#PKG_OSES=
#PKG_ARCHES=
#PKG_FORMATS=
WEBI_UA="$(uname -s)/$(uname -r) $(uname -m)/unknown${my_libc}"
WEBI_PKG_DOWNLOAD=""
WEBI_DOWNLOAD_DIR="${HOME}/Downloads"
if command -v xdg-user-dir > /dev/null; then
WEBI_DOWNLOAD_DIR="$(xdg-user-dir DOWNLOAD)"
if [ "${WEBI_DOWNLOAD_DIR}" = "${HOME}" ]; then
WEBI_DOWNLOAD_DIR="${HOME}/Downloads"
fi
fi
WEBI_PKG_PATH="${WEBI_DOWNLOAD_DIR}/webi/${PKG_NAME:-error}/${WEBI_VERSION:-latest}"
export WEBI_HOST
##
## Set up tmp, download, and install directories
##
WEBI_TMP=${WEBI_TMP:-"$(mktemp -d -t webinstall-"${WEBI_PKG-}".XXXXXXXX)"}
export _webi_tmp="${_webi_tmp:-"$HOME/.local/opt/webi-tmp.d"}"
mkdir -p "${WEBI_PKG_PATH}"
mkdir -p "$HOME/.local/bin"
mkdir -p "$HOME/.local/opt"
##
## Detect http client
##
set +e
WEBI_CURL="$(command -v curl)"
export WEBI_CURL
WEBI_WGET="$(command -v wget)"
export WEBI_WGET
set -e
# get the special formatted version (i.e. "go is go1.14" while node is "node v12.10.8")
my_versioned_name=""
_webi_canonical_name() {
if [ -n "$my_versioned_name" ]; then
echo "$my_versioned_name"
return 0
fi
if [ -n "$(command -v pkg_format_cmd_version)" ]; then
my_versioned_name="'$(pkg_format_cmd_version "$WEBI_VERSION")'"
else
my_versioned_name="'$pkg_cmd_name v$WEBI_VERSION'"
fi
echo "$my_versioned_name"
}
# update symlinks according to $HOME/.local/opt and $HOME/.local/bin install paths.
# shellcheck disable=2120
# webi_link may be used in the templated install script
webi_link() {
if [ -n "$(command -v pkg_link)" ]; then
pkg_link
return 0
fi
if [ -n "$WEBI_SINGLE" ] || [ "single" = "${1-}" ]; then
rm -rf "$pkg_dst_cmd"
ln -s "$pkg_src_cmd" "$pkg_dst_cmd"
else
# 'pkg_dst' will default to $HOME/.local/opt/<pkg>
# 'pkg_src' will be the installed version, such as to $HOME/.local/opt/<pkg>-<version>
rm -rf "$pkg_dst"
ln -s "$pkg_src" "$pkg_dst"
fi
}
# detect if this program is already installed or if an installed version may cause conflict
webi_check() {
# Test for existing version
set +e
my_path="$PATH"
PATH="$(dirname "$pkg_dst_cmd"):$PATH"
export PATH
my_current_cmd="$(command -v "$pkg_cmd_name")"
set -e
if [ -n "$my_current_cmd" ]; then
my_canonical_name="$(_webi_canonical_name)"
if [ "$my_current_cmd" != "$pkg_dst_cmd" ]; then
echo >&2 "WARN: possible PATH conflict between $my_canonical_name and currently installed version"
echo >&2 " ${pkg_dst_cmd} (new)"
echo >&2 " ${my_current_cmd} (existing)"
#my_current_version=false
fi
# 'readlink' can't read links in paths on macOS 🤦
# but that's okay, 'cmp -s' is good enough for us
if cmp -s "${pkg_src_cmd}" "${my_current_cmd}"; then
echo "${my_canonical_name} already installed:"
printf " %s" "${pkg_dst}"
if [ "${pkg_src_cmd}" != "${my_current_cmd}" ]; then
printf " => %s" "${pkg_src}"
fi
echo ""
exit 0
fi
if [ -x "$pkg_src_cmd" ]; then
# shellcheck disable=2119
# this function takes no args
webi_link
echo "switched to $my_canonical_name:"
echo " ${pkg_dst} => ${pkg_src}"
exit 0
fi
fi
export PATH="$my_path"
}
is_interactive_shell() {
# $- shows shell flags (error,unset,interactive,etc)
case $- in
*i*)
# true
return 0
;;
*)
# false
return 1
;;
esac
}
# detect if file is downloaded, and how to download it
webi_download() {
# determine the url to download
if [ -n "${1-}" ]; then
my_url="${1}"
else
if [ "error" = "$WEBI_CHANNEL" ]; then
# TODO pass back requested OS / Arch / Version
echo >&2 "Error: no '$PKG_NAME' release for '${WEBI_OS-}' on '$WEBI_ARCH' as one of '$WEBI_FORMATS' by the tag '${WEBI_TAG-}'"
echo >&2 " '$PKG_NAME' is available for '$PKG_OSES' on '$PKG_ARCHES' as one of '$PKG_FORMATS'"
echo >&2 " (check that the package name and version are correct)"
echo >&2 ""
my_release_url="$(
echo "$WEBI_RELEASES" |
sed 's:\?.*::'
)"
echo >&2 " Double check at ${my_release_url}"
echo >&2 ""
exit 1
fi
my_url="$WEBI_PKG_URL"
fi
# determine the location to download to
if [ -n "${2-}" ]; then
my_dl="${2}"
else
my_dl="${WEBI_PKG_PATH}/$WEBI_PKG_FILE"
fi
if [ -n "${3-}" ]; then
my_dl_name="${3}"
else
my_dl_name="${PKG_NAME}"
fi
WEBI_PKG_DOWNLOAD="${my_dl}"
export WEBI_PKG_DOWNLOAD
if [ -e "$my_dl" ]; then
echo "Found $my_dl"
return 0
fi
echo "Downloading ${my_dl_name} from"
echo "$my_url"
# It's only 2020, we can't expect to have reliable CLI tools
# to tell us the size of a file as part of a base system...
if [ -n "$WEBI_WGET" ]; then
# wget has resumable downloads
# TODO wget -c --content-disposition "$my_url"
set +e
my_show_progress=""
if is_interactive_shell; then
my_show_progress="--show-progress"
fi
if ! wget -q $my_show_progress --user-agent="wget $WEBI_UA" -c "$my_url" -O "$my_dl.part"; then
echo >&2 "failed to download from $WEBI_PKG_URL"
exit 1
fi
set -e
else
# Neither GNU nor BSD curl have sane resume download options, hence we don't bother
# TODO curl -fsSL --remote-name --remote-header-name --write-out "$my_url"
my_show_progress="-#"
if is_interactive_shell; then
my_show_progress=""
fi
# shellcheck disable=SC2086
# we want the flags to be split
curl -fSL $my_show_progress -H "User-Agent: curl $WEBI_UA" "$my_url" -o "$my_dl.part"
fi
mv "$my_dl.part" "$my_dl"
echo ""
echo "Saved as $my_dl"
}
# detect which archives can be used
webi_extract() {
(
cd "$WEBI_TMP"
if [ "tar" = "$WEBI_EXT" ]; then
echo "Extracting ${WEBI_PKG_PATH}/$WEBI_PKG_FILE"
tar xf "${WEBI_PKG_PATH}/$WEBI_PKG_FILE"
elif [ "zip" = "$WEBI_EXT" ]; then
echo "Extracting ${WEBI_PKG_PATH}/$WEBI_PKG_FILE"
unzip "${WEBI_PKG_PATH}/$WEBI_PKG_FILE" > __unzip__.log
elif [ "exe" = "$WEBI_EXT" ]; then
echo "Moving ${WEBI_PKG_PATH}/$WEBI_PKG_FILE"
mv "${WEBI_PKG_PATH}/$WEBI_PKG_FILE" .
elif [ "xz" = "$WEBI_EXT" ]; then
echo "Inflating ${WEBI_PKG_PATH}/$WEBI_PKG_FILE"
unxz -c "${WEBI_PKG_PATH}/$WEBI_PKG_FILE" > "$(basename "$WEBI_PKG_FILE")"
else
# do nothing
echo "Failed to extract ${WEBI_PKG_PATH}/$WEBI_PKG_FILE"
exit 1
fi
)
}
# use 'pathman' to update $HOME/.config/envman/PATH.env
webi_path_add() {
my_path="${1}"
fn_envman_init
# \v was chosen as it is extremely unlikely for a filename
# \1 could be an even better choice, but needs more testing.
# (currently tested working on: linux & mac)
# "\0001" should also work
my_delim="$(
printf '\v'
)"
my_path_expanded="$(
echo "${my_path}" |
sed -e "s${my_delim}\$HOME${my_delim}$HOME${my_delim}g" \
-e "s${my_delim}\${HOME}${my_delim}$HOME${my_delim}g" \
-e "s${my_delim}^~/${my_delim}$HOME/${my_delim}g"
)"
# A gift for @adamcstephens.
# See https://github.com/webinstall/webi-installers/issues/322
case "${PATH}" in
# matches whether the first, a middle, the last, or the only PATH entry
"${my_path_expanded}":* | \
*:"${my_path_expanded}":* | \
*:"${my_path_expanded}" | \
"${my_path_expanded}")
if fn_is_defined_in_all_shells "${my_path}"; then
return 0
fi
;;
*) ;;
esac
my_path_export="$(
echo "${my_path}" |
sed -e "s${my_delim}${HOME}${my_delim}\$HOME${my_delim}g" \
-e "s${my_delim}\${HOME}${my_delim}\$HOME${my_delim}g" \
-e "s${my_delim}^~/${my_delim}\$HOME/${my_delim}g"
)"
my_export="export PATH=\"$my_path_export:\$PATH\""
if grep -q -F "${my_export}" ~/.config/envman/PATH.env; then
return 0
fi
echo "${my_export}" >> ~/.config/envman/PATH.env
mkdir -p "$_webi_tmp"
my_path_tilde="$(
echo "${my_path}" |
sed -e "s${my_delim}${HOME}${my_delim}~${my_delim}g"
)"
if ! test -f "$_webi_tmp/.PATH.env" ||
! grep -q -F "${my_path_tilde}" "$_webi_tmp/.PATH.env"; then
echo "${my_path_tilde}" >> "$_webi_tmp/.PATH.env"
fi
}
fn_envman_init() {
mkdir -p ~/.config/envman/
if ! test -e ~/.config/envman/PATH.env; then
touch ~/.config/envman/PATH.env
fi
if ! test -e ~/.config/envman/load.sh; then
# shellcheck disable=SC2016
{
echo '# Generated for envman. Do not edit.'
echo 'for x in ~/.config/envman/*.env; do'
echo ' my_basename="$(basename "${x}")"'
echo ' if [ "*.env" = "${my_basename}" ]; then'
echo ' continue'
echo ' fi'
echo ''
echo ' # shellcheck source=/dev/null'
echo ' . "${x}"'
echo 'done'
} > ~/.config/envman/load.sh
fi
if command -v sh > /dev/null; then
if test -e ~/.profile; then
if ! grep -q -F '/.config/envman/load.sh' ~/.profile; then
fn_echo_load_sh >> ~/.profile
fi
fi
fi
if command -v bash > /dev/null; then
if test -e ~/.bashrc; then
if ! grep -q -F '/.config/envman/load.sh' ~/.bashrc; then
fn_echo_load_sh >> ~/.bashrc
fi
fi
fi
if command -v zsh > /dev/null; then
if test -e ~/.zshrc; then
if ! grep -q -F '/.config/envman/load.sh' ~/.zshrc; then
fn_echo_load_sh >> ~/.zshrc
fi
fi
fi
if command -v fish > /dev/null; then
if test ! -e ~/.config/envman/load.fish; then
# shellcheck disable=SC2016
{
echo '# Generated for envman. Do not edit.'
echo 'for x in ~/.config/envman/*.env'
echo ' source "$x"'
echo 'end'
} > ~/.config/envman/load.fish
fi
mkdir -p ~/.config/fish
if test -e ~/.config/fish/config.fish; then
touch ~/.config/fish/config.fish
fi
if ! grep -q -F '/.config/envman/load.fish' ~/.config/fish/config.fish; then
fn_echo_load_fish >> ~/.config/fish/config.fish
fi
fi
}
fn_echo_load_fish() {
echo ''
echo '# Generated for envman. Do not edit.'
# shellcheck disable=SC2016
echo 'test -s "$HOME/.config/envman/load.fish"; and source "$HOME/.config/envman/load.fish"'
}
fn_echo_load_sh() {
echo ''
echo '# Generated for envman. Do not edit.'
# shellcheck disable=SC2016
echo '[ -s "$HOME/.config/envman/load.sh" ] && source "$HOME/.config/envman/load.sh"'
}
fn_is_defined_in_all_shells() {
my_path="${1}"
my_path_expanded="$(
echo "${my_path}" |
sed -e "s${my_delim}\$HOME|${my_delim}$HOME${my_delim}g" \
-e "s${my_delim}\${HOME}${my_delim}$HOME${my_delim}g" \
-e "s${my_delim}^~/${my_delim}$HOME/${my_delim}g"
)"
my_paths="$(
echo "${my_path_expanded}"
# $HOME/foo
echo "${my_path_expanded}" |
sed "s${my_delim}${HOME}${my_delim}\$HOME${my_delim}g"
# ${HOME}/foo
echo "${my_path_expanded}" |
sed "s${my_delim}${HOME}${my_delim}\${HOME}${my_delim}g"
echo "${my_path}"
)"
my_confs="$(
echo "${HOME}/.profile"
echo "${HOME}/.bashrc"
echo "${HOME}/.zshrc"
echo "${HOME}/.config/fish/config.fish"
)"
for my_conf in $my_confs; do
if test -e "${my_conf}"; then
if ! grep -q -F "${my_paths}" "${my_conf}"; then
return 1
fi
fi
done
}
# group common pre-install tasks as default
webi_pre_install() {
webi_check
webi_download
webi_extract
}
# move commands from the extracted archive directory to $HOME/.local/opt or $HOME/.local/bin
# shellcheck disable=2120
# webi_install may be sourced and used elsewhere
webi_install() {
if [ -n "$WEBI_SINGLE" ] || [ "single" = "${1-}" ]; then
mkdir -p "$(dirname "$pkg_src_cmd")"
mv ./"$pkg_cmd_name"* "$pkg_src_cmd"
else
rm -rf "$pkg_src"
mv ./"$pkg_cmd_name"* "$pkg_src"
fi
}
# run post-install functions - just updating PATH by default
webi_post_install() {
webi_path_add "$(dirname "$pkg_dst_cmd")"
}
_webi_enable_exec() {
if [ -n "$(command -v spctl)" ] && [ -n "$(command -v xattr)" ]; then
# note: some packages contain files that cannot be affected by xattr
xattr -r -d com.apple.quarantine "$pkg_src" || true
return 0
fi
# TODO need to test that the above actually worked
# (and proceed to this below if it did not)
if [ -n "$(command -v spctl)" ]; then
echo "Checking permission to execute '$pkg_cmd_name' on macOS 11+"
set +e
is_allowed="$(spctl -a "$pkg_src_cmd" 2>&1 | grep valid)"
set -e
if [ -z "$is_allowed" ]; then
echo ""
echo "##########################################"
echo "# IMPORTANT: Permission Grant Required #"
echo "##########################################"
echo ""
echo "Requesting permission to execute '$pkg_cmd_name' on macOS 10.14+"
echo ""
sleep 3
spctl --add "$pkg_src_cmd"
fi
fi
}
# a friendly message when all is well, showing the final install path in $HOME/.local
_webi_done_message() {
echo "Installed $(_webi_canonical_name) as $pkg_dst_cmd"
}
##
##
## BEGIN custom override functions from <package>/install.sh
##
##
WEBI_SINGLE=
if [ -z "${WEBI_WELCOME-}" ]; then
echo ""
printf "Thanks for using webi to install '\e[32m%s\e[0m' on '\e[31m%s/%s\e[0m'.\n" "${WEBI_PKG-}" "$(uname -s)" "$(uname -m)"
echo "Have a problem? Experience a bug? Please let us know:"
echo " https://github.com/webinstall/webi-installers/issues"
echo ""
printf "\e[31mLovin'\e[0m it? Say thanks with a \e[34mStar on GitHub\e[0m:\n"
printf " \e[32mhttps://github.com/webinstall/webi-installers\e[0m\n"
echo ""
fi
__init_installer() {
# do nothing - to satisfy parser prior to templating
printf ""
# {{ installer }}
}
__init_installer
##
##
## END custom override functions
##
##
# run everything with defaults or overrides as needed
if command -v pkg_install > /dev/null ||
command -v pkg_link > /dev/null ||
command -v pkg_post_install > /dev/null ||
command -v pkg_done_message > /dev/null ||
command -v pkg_format_cmd_version > /dev/null ||
[ -n "${WEBI_SINGLE-}" ] ||
[ -n "${pkg_cmd_name-}" ] ||
[ -n "${pkg_dst_cmd-}" ] ||
[ -n "${pkg_dst_dir-}" ] ||
[ -n "${pkg_dst-}" ] ||
[ -n "${pkg_src_cmd-}" ] ||
[ -n "${pkg_src_dir-}" ] ||
[ -n "${pkg_src-}" ]; then
pkg_cmd_name="${pkg_cmd_name:-$PKG_NAME}"
if [ -n "$WEBI_SINGLE" ]; then
pkg_dst_cmd="${pkg_dst_cmd:-$HOME/.local/bin/$pkg_cmd_name}"
pkg_dst="$pkg_dst_cmd" # "$(dirname "$(dirname $pkg_dst_cmd)")"
#pkg_src_cmd="${pkg_src_cmd:-$HOME/.local/opt/$pkg_cmd_name-v$WEBI_VERSION/bin/$pkg_cmd_name-v$WEBI_VERSION}"
pkg_src_cmd="${pkg_src_cmd:-$HOME/.local/opt/$pkg_cmd_name-v$WEBI_VERSION/bin/$pkg_cmd_name}"
pkg_src="$pkg_src_cmd" # "$(dirname "$(dirname $pkg_src_cmd)")"
else
pkg_dst="${pkg_dst:-$HOME/.local/opt/$pkg_cmd_name}"
pkg_dst_cmd="${pkg_dst_cmd:-$pkg_dst/bin/$pkg_cmd_name}"
pkg_src="${pkg_src:-$HOME/.local/opt/$pkg_cmd_name-v$WEBI_VERSION}"
pkg_src_cmd="${pkg_src_cmd:-$pkg_src/bin/$pkg_cmd_name}"
fi
# this script is templated and these are used elsewhere
# shellcheck disable=SC2034
pkg_src_bin="$(dirname "$pkg_src_cmd")"
# shellcheck disable=SC2034
pkg_dst_bin="$(dirname "$pkg_dst_cmd")"
if [ -n "$(command -v pkg_pre_install)" ]; then pkg_pre_install; else webi_pre_install; fi
(
cd "$WEBI_TMP"
echo "Installing to $pkg_src_cmd"
if [ -n "$(command -v pkg_install)" ]; then pkg_install; else webi_install; fi
chmod a+x "$pkg_src"
chmod a+x "$pkg_src_cmd"
)
webi_link
_webi_enable_exec
(
cd "$WEBI_TMP"
if [ -n "$(command -v pkg_post_install)" ]; then pkg_post_install; else webi_post_install; fi
)
(
cd "$WEBI_TMP"
if [ -n "$(command -v pkg_done_message)" ]; then pkg_done_message; else _webi_done_message; fi
)
echo ""
fi
webi_path_add "$HOME/.local/bin"
if [ -z "${_WEBI_CHILD-}" ] && [ -f "$_webi_tmp/.PATH.env" ]; then
if [ -n "$(cat "$_webi_tmp/.PATH.env")" ]; then
printf 'PATH.env updated with:\n'
sort -u "$_webi_tmp/.PATH.env" | while read -r my_new_path; do
echo " ${my_new_path}"
done
printf "\n"
rm -f "$_webi_tmp/.PATH.env"
printf "\e[31mTO FINISH\e[0m: copy, paste & run the following command:\n"
printf "\n"
printf " \e[34msource ~/.config/envman/PATH.env\e[0m\n"
printf " (newly opened terminal windows will update automatically)\n"
fi
fi
# cleanup the temp directory
rm -rf "$WEBI_TMP"
# See? No magic. Just downloading and moving files.
}
__bootstrap_webi

View File

@@ -32,10 +32,8 @@ if (/\b-?-h(elp)?\b/.test(process.argv.join(' '))) {
var os = require('os');
var fs = require('fs');
var path = require('path');
var Builds = require('./builds.js');
var Installers = require('./installers.js');
var ServeInstaller = require('./serve-installer.js');
var Releases = require('./releases.js');
var uaDetect = require('./ua-detect.js');
var pkg = process.argv[2].split('@');
var pkgdir = pkg[0];
var pkgtag = pkg[1] || '';
@@ -44,12 +42,11 @@ var nodes = fs.readdirSync(pkgdir);
nodes.forEach(function (node) {
nodesMap[node] = true;
});
var baseurl = 'https://webinstall.dev';
var maxLen = 0;
console.info('');
console.info('Has the necessary files?');
['README.md', 'install.sh', 'install.ps1']
['README.md', 'releases.js', 'install.sh', 'install.ps1']
.map(function (node) {
maxLen = Math.max(maxLen, node.length);
return node;
@@ -65,49 +62,31 @@ console.info('Has the necessary files?');
});
console.info('');
let projName = pkgdir.split('/').filter(Boolean).pop();
Builds.getPackage({ name: projName }).then(async function (/*projInfo*/) {
Releases.get(path.join(process.cwd(), pkgdir)).then(function (all) {
var pkgname = path.basename(pkgdir.replace(/\/$/, ''));
var nodeOs = os.platform();
var nodeOsRelease = os.release();
var nodeArch = os.arch();
var nodeLibc = 'libc';
if (process.platform === 'linux') {
nodeLibc = 'gnu';
let isUnofficial =
process.config.variables.node_release_urlbase.includes('unofficial');
if (isUnofficial) {
nodeLibc = 'musl';
}
}
var formats = ['exe', 'xz', 'tar', 'zip', 'git'];
var osrel = os.platform() + '-' + os.release();
var arch = os.arch();
var formats = ['exe', 'xz', 'tar', 'zip'];
let [rel, opts] = await ServeInstaller.helper({
unameAgent: `${nodeOs}/${nodeOsRelease} ${nodeArch}/unknown ${nodeLibc}`,
projectName: pkgname,
tag: pkgtag || '',
formats: formats,
libc: nodeLibc,
});
Object.assign(
{
ver: '',
lts: null,
channel: '',
os: '',
arch: '',
limit: 0,
},
opts,
{
baseurl,
},
);
var rel = all.releases.filter(function (rel) {
return (
formats.filter(function (ext) {
return rel.ext.match(ext);
})[0] &&
'stable' === rel.channel &&
rel.os === uaDetect.os(osrel) &&
rel.arch === uaDetect.arch(arch) &&
(!pkgtag ||
rel.tag === pkgtag ||
new RegExp('^' + pkgtag).test(rel.version))
);
})[0];
rel.oses = all.oses;
rel.arches = all.arches;
rel.formats = all.formats;
if (!rel) {
console.error(
`Error: ❌ no release found for @${pkgtag}?os=${nodeOs}&arch=${nodeArch}&libc=${nodeLibc}&formats=${formats}`,
);
console.error('Error: ❌ no release found for current os, arch, and tag');
process.exit(1);
return;
}
@@ -118,8 +97,24 @@ Builds.getPackage({ name: projName }).then(async function (/*projInfo*/) {
console.info('');
return Promise.all([
Installers.renderBash(pkgdir, rel, opts).catch(function () {}),
Installers.renderPowerShell(pkgdir, rel, opts).catch(function () {}),
Releases.renderBash(pkgdir, rel, {
baseurl: 'https://webinstall.dev',
pkg: pkgname,
tag: pkgtag || '',
ver: '',
os: osrel,
arch,
formats: formats,
}).catch(function () {}),
Releases.renderPowerShell(pkgdir, rel, {
baseurl: 'https://webinstall.dev',
pkg: pkgname,
tag: pkgtag || '',
ver: '',
os: osrel,
arch,
formats: formats,
}).catch(function () {}),
]).then(function (scripts) {
var bashTxt = scripts[0];
var ps1Txt = scripts[1];
@@ -143,9 +138,5 @@ Builds.getPackage({ name: projName }).then(async function (/*projInfo*/) {
console.info('\tNEEDS MANUAL TEST: powershell.exe %s', ps1File);
}
console.info('');
setTimeout(function () {
console.warn(`[warn] dangling event loop handle`);
process.exit(0);
}, 300).unref();
});
});

View File

@@ -1,80 +1,172 @@
'use strict';
var Releases = module.exports;
var Fs = require('node:fs/promises');
var Os = require('node:os');
var path = require('path');
var Releases = require('./releases.js');
var uaDetect = require('./ua-detect.js');
var cache = {};
//var staleAge = 5 * 1000;
//var expiredAge = 15 * 1000;
var staleAge = 5 * 60 * 1000;
var expiredAge = 15 * 60 * 1000;
var LEGACY_CACHE_DIR = path.join(Os.homedir(), '.cache/webi/legacy');
let installerDir = path.join(__dirname, '..');
// Sort releases by ext preference and libc within the same version.
// The cache is already sorted by version (stable before beta, newest first),
// so we only re-order within the same version string.
// TODO needs a proper test, and more accurate (though perhaps far less simple) code
function createFormatsSorter(formats) {
return function sortByExtLibc(a, b) {
if (a.version !== b.version) {
// Array.sort is stable (V8, ES2019), so returning 0 across
// versions preserves the cache's pre-sorted version-desc order.
return 0;
return function sortByVerExt(a, b) {
function lexver(semver) {
// v1.20.156 => 00001.00020.00156.zzzzz
// TODO BUG: v1.20.156-rc2 => 00001.00020.00156.rc2zz
var parts = semver.split(/[+\.\-]/g);
while (parts.length < 4) {
parts.push('');
}
return parts
.map(function (num, i) {
if (3 === i) {
return num.toString().padEnd(10, 'z');
}
return num.toString().padStart(10, '0');
})
.join('.');
}
var aver = lexver(a.version);
var bver = lexver(b.version);
if (aver > bver) {
//console.log(aver, '>', bver);
return -1;
}
if (aver < bver) {
//console.log(aver, '<', bver);
return 1;
}
var aExtPri = formats.indexOf(a.ext.replace(/tar\..*/, 'tar'));
var bExtPri = formats.indexOf(b.ext.replace(/tar\..*/, 'tar'));
if (aExtPri > bExtPri) {
//console.log(a.ext, aExtPri, '>', b.ext, bExtPri);
return -1;
}
if (aExtPri < bExtPri) {
//console.log(a.ext, aExtPri, '<', b.ext, bExtPri);
return 1;
}
// rank builds that don't depend on any form of libc first
if (a.libc === 'none' && b.libc !== 'none') {
// Hacky-doo for musl-native: prefer non-musl
if (a._musl_native && !b._musl_native) {
return 1;
}
if (!a._musl_native && b._musl_native) {
return -1;
}
if (a.libc !== 'none' && b.libc === 'none') {
// Hacky-doo for linux: prefer musl
if (a._musl && !b._musl) {
return -1;
}
if (!a._musl && b._musl) {
return 1;
}
return 0;
};
}
async function getCachedReleases(pkg) {
// returns { download: '', releases: [{ version, date, os, arch, lts, channel, download}] }
// returns { download: '<template string>', releases: [{ version, date, os, arch, lts, channel, download}] }
if (cache[pkg]) {
return cache[pkg];
async function chainCachePromise(fn) {
cache[pkg].promise = cache[pkg].promise.then(fn);
return cache[pkg].promise;
}
let dataFile = `${LEGACY_CACHE_DIR}/${pkg}.json`;
async function sleep(ms) {
return await new Promise(function (resolve, reject) {
setTimeout(resolve, ms);
});
}
let json = await Fs.readFile(dataFile, 'utf8').catch(function (err) {
if (err.code === 'ENOENT') {
return null;
async function putCache() {
var age = Date.now() - cache[pkg].updatedAt;
if (age < staleAge) {
//console.debug('NOT STALE ANYMORE - updated in previous promise');
return cache[pkg].all;
}
throw err;
});
if (!json) {
let empty = { download: '', releases: [] };
cache[pkg] = empty;
return empty;
//console.debug('DOWNLOADING NEW "%s" releases', pkg);
var pkgdir = path.join(installerDir, pkg);
// workaround for request timeout seeming to not work
let complete = false;
await Promise.race([
Releases.get(pkgdir)
.catch(function (err) {
if ('E_NO_RELEASE' === err.code) {
let all = { _error: 'E_NO_RELEASE', download: '', releases: [] };
return all;
}
throw err;
})
.catch(function (err) {
let hasReleases = cache[pkg].all?.releases?.length > 1;
if (!hasReleases) {
throw err;
}
console.error(`Error: the BOOGEYMAN got us!`);
console.error(err.stack);
return cache[pkg].all;
})
.then(function (all) {
// Note: it is possible for slightly older data
// to replace slightly newer data, but this is better
// than being in a cycle where release updates _always_
// take longer than expected.
//console.debug('DOWNLOADED NEW "%s" releases', pkg);
cache[pkg].updatedAt = Date.now();
cache[pkg].all = all;
complete = true;
}),
sleep(5000).then(function () {
if (complete) {
return;
}
console.error(`request timeout waiting for '${pkg}' release info`);
}),
]);
return cache[pkg].all;
}
let all;
try {
all = JSON.parse(json);
} catch (e) {
console.error(`error: ${dataFile}:\n\t${e.message}`);
let empty = { download: '', releases: [] };
cache[pkg] = empty;
return empty;
if (!cache[pkg]) {
cache[pkg] = {
updatedAt: 0,
all: { download: '', releases: [] },
promise: Promise.resolve(),
};
}
cache[pkg] = all;
return all;
var bgRenewal;
var age = Date.now() - cache[pkg].updatedAt;
var fresh = age < staleAge;
if (!fresh) {
bgRenewal = chainCachePromise(putCache);
}
var tooStale = age > expiredAge;
if (!tooStale) {
return await cache[pkg].all;
}
return await Promise.race([
bgRenewal,
sleep(5000).then(function () {
return cache[pkg].all;
}),
]);
}
async function filterReleases(
@@ -85,50 +177,29 @@ async function filterReleases(
// sort the most compatible format first
// (i.e. so that we don't do .pkg on linux except on purpose)
var rformats = formats.slice(0).reverse();
var sortByExtLibc = createFormatsSorter(rformats);
var sortByVerExt = createFormatsSorter(rformats);
var reVer = new RegExp('^' + ver + '\\b');
function selectMatches(rel) {
/* jshint maxcomplexity: 25 */
if (os) {
// '*' = any OS (matches anything, including windows).
// 'posix' / 'posix_20xx' = any POSIX OS (matches linux, macos,
// freebsd, etc., but NOT windows).
let isPosix = rel.os === 'posix' || rel.os.startsWith('posix_20');
let osMatches =
rel.os === '*' ||
rel.os === os ||
(isPosix && os !== 'windows');
if (!osMatches) {
if (rel.os !== os) {
return false;
}
}
if (arch) {
if (rel.arch !== '*') {
if (rel.arch !== arch) {
return false;
}
if (rel.arch !== arch) {
return false;
}
}
// libc='libc' is serve-releases.js's default when the caller
// didn't pin one — treat it as 'no preference', not a filter.
let isMeaningfulLibc = libc && libc !== 'libc' && rel.libc !== 'none';
if (isMeaningfulLibc) {
let releaseRequiresMusl = rel.libc === 'musl';
// goal: handle non-glibc (Alpine / Docker / musl)
let osHasMusl = libc === 'musl';
if (osHasMusl) {
// goal: fail if dependent on libc
let releaseRequiresLibc = rel.libc === 'gnu';
if (releaseRequiresLibc) {
return false;
}
} else if (releaseRequiresMusl) {
// goal: don't use musl++ on glibc (Ubuntu, GNU, etc)
// Hacky-doo for linux musl
if (libc === uaDetect.MUSL_NATIVE) {
if (!rel._musl && !rel._musl_native) {
return false;
}
} else if (rel._musl_native) {
return false;
}
if (lts) {
@@ -162,13 +233,13 @@ async function filterReleases(
return true;
}
var sortedRels = all.releases.filter(selectMatches).sort(sortByExtLibc);
var sortedRels = all.releases.filter(selectMatches).sort(sortByVerExt);
//console.log(sortedRels.slice(0, 4));
return sortedRels.slice(0, limit || 1000);
}
Releases.getReleases = function ({
module.exports = function getReleases({
_count,
pkg,
ver,
@@ -204,19 +275,10 @@ Releases.getReleases = function ({
console.error(err);
})
.then(function (releases) {
if (releases.length) {
return {
oses: all.oses,
arches: all.arches,
libcs: all.libcs,
formats: all.formats,
releases: releases,
};
}
if (_count < 1) {
if (!releases.length) {
// Apple Silicon M1 hacky-do workaround fix
if ('macos' === os && 'arm64' === arch) {
return Releases.getReleases({
return getReleases({
pkg,
ver,
os,
@@ -230,7 +292,7 @@ Releases.getReleases = function ({
}
// Windows ARM hacky-do workaround fix
if ('windows' === os && 'arm64' === arch) {
return Releases.getReleases({
return getReleases({
pkg,
ver,
os,
@@ -242,42 +304,9 @@ Releases.getReleases = function ({
limit,
});
}
// Raspberry Pi 3+ on Ubuntu arm64 (via Bionic?)
if ('linux' === os && 'arm64' === arch) {
return Releases.getReleases({
_count: _count + 1,
pkg,
ver,
os,
arch: 'armv7l',
libc,
lts,
channel,
formats,
limit,
});
}
// armv7 can run armv6
if ('linux' === os && 'armv7l' === arch) {
return Releases.getReleases({
_count: _count + 1,
pkg,
ver,
os,
arch: 'armv6l',
libc,
lts,
channel,
formats,
limit,
});
}
}
if (_count < 2) {
// Raspberry Pi 3+ on Raspbian arm7 (not Ubuntu arm64)
// hail mary
if ('linux' === os && 'armv7l' === arch) {
return Releases.getReleases({
// Raspberry Pi 3+ on Raspbian x86 (not Ubuntu arm64)
if (!_count && 'linux' === os && 'armv7l' === arch) {
return getReleases({
_count: _count + 1,
pkg,
ver,
@@ -290,29 +319,60 @@ Releases.getReleases = function ({
limit,
});
}
// Raspberry Pi 3+ on Ubuntu arm64 (via Bionic?)
// (this may be the same as the prior search, that's okay)
if ('linux' === os && 'arm64' === arch) {
return getReleases({
_count: _count + 1,
pkg,
ver,
os,
arch: 'armv7l',
libc,
lts,
channel,
formats,
limit,
});
}
// Raspberry Pi 3+ on Ubuntu arm64 (via Bionic?)
if ('linux' === os && 'armv7l' === arch) {
return getReleases({
_count: _count + 1,
pkg,
ver,
os,
arch: 'armv6l',
libc,
lts,
channel,
formats,
limit,
});
}
releases = [
{
name: 'doesntexist.ext',
version: '0.0.0',
lts: '-',
channel: 'error',
date: '1970-01-01',
os: os || '-',
arch: arch || '-',
_musl: undefined,
_musl_native: undefined,
ext: 'err',
download: 'https://example.com/doesntexist.ext',
comment:
'No matches found. Could be bad or missing version info' +
',' +
"Check query parameters. Should be something like '/api/releases/{package}@{version}.tab?os={macos|linux|windows|-}&arch={amd64|x86|aarch64|arm64|armv7l|-}&limit=100'",
},
];
}
releases = [
{
name: 'doesntexist.ext',
version: '0.0.0',
lts: '-',
channel: 'error',
date: '1970-01-01',
os: os || '-',
arch: arch || '-',
libc: libc || '-',
ext: 'err',
download: 'https://example.com/doesntexist.ext',
comment:
'No matches found. Could be bad or missing version info' +
',' +
"Check query parameters. Should be something like '/api/releases/{package}@{version}.tab?os={macos|linux|windows|-}&arch={amd64|x86|aarch64|arm64|armv7l|-}&libc={musl|gnu|msvc|libc|static}&limit=10'",
},
];
return {
oses: all.oses,
arches: all.arches,
libcs: all.libcs,
formats: all.formats,
releases: releases,
};
@@ -321,14 +381,14 @@ Releases.getReleases = function ({
};
if (require.main === module) {
return Releases
.getReleases({
return module
.exports({
pkg: 'node',
ver: '',
os: 'macos',
arch: 'amd64',
lts: true,
libc: 'libc',
libc: '',
channel: 'stable',
formats: ['tar', 'exe', 'zip', 'xz', 'dmg', 'pkg'],
limit: 10,

View File

@@ -2,6 +2,10 @@
var uaDetect = module.exports;
const MUSL_NATIVE = 'musl-native';
uaDetect.MUSL_NATIVE = MUSL_NATIVE;
function getRequest(req) {
var ua = req.headers['user-agent'] || '';
var os = req.query.os;
@@ -38,8 +42,9 @@ function getOs(ua) {
// It's the year of the Linux Desktop!
// See also http://www.mslinux.org/
// 'linux' must be tested before 'Microsoft' because WSL
// (TODO: does this affect cygwin / msysgit?)
return 'linux';
} else if (/^ms$|Microsoft|Windows|win32|win|PowerShell|CYGWIN|MINGW/i.test(ua)) {
} else if (/^ms$|Microsoft|Windows|win32|win|PowerShell/i.test(ua)) {
// 'win' must be tested after 'darwin'
return 'windows';
} else if (/Linux|curl|wget/i.test(ua)) {
@@ -66,12 +71,10 @@ function getArch(ua) {
if (/aarch64|arm64|arm8|armv8/i.test(ua)) {
return 'arm64';
} else if (/aarch|arm7|armv7|arm32/i.test(ua)) {
} else if (/aarch|arm7|armv7/i.test(ua)) {
return 'armv7l';
} else if (/arm6|armv6|arm(\b|_)/i.test(ua)) {
} else if (/arm6|armv6/i.test(ua)) {
return 'armv6l';
} else if (/ppc64le/i.test(ua)) {
return 'ppc64le';
} else if (/ppc64/i.test(ua)) {
return 'ppc64';
} else if (/mips64/i.test(ua)) {
@@ -95,55 +98,19 @@ function getLibc(ua) {
return '-';
}
// How to see a bunch of target host quadruples:
// go tool dist list
// rustup target list
// zig targets | jq -r '.libc[]' | sort -
// Use native 'libc' information, if provided
//
// Generally, we prefer 'musl' builds because they DO work on glibc systems (Ubuntu),
// but 'glibc' builds will NOT work on musl systems (Alpine / Docker).
//
// However, there are a few instances (ex: Node.js), where the 'musl' builds
// DO NOT work on glibc systems.
{
let muslRe = /(\b|_)(musl)(\b|_)/i;
if (muslRe.test(ua)) {
return 'musl';
}
if (ua.match(MUSL_NATIVE)) {
return MUSL_NATIVE;
}
{
let msvcRe = /(\b|_)(msvc|windows|microsoft)(\b|_)/i;
if (msvcRe.test(ua)) {
return 'msvc';
}
}
{
let gnuRe = /(\b|_)(gnu|glibc|linux)(\b|_)/i;
if (gnuRe.test(ua)) {
return 'gnu';
}
}
{
let libcRe = /(\b|_)(libc)(\b|_)/i;
if (libcRe.test(ua)) {
return 'libc';
}
}
{
let darwinRe = /(\b|_)(darwin)(\b|_)/i;
if (darwinRe.test(ua)) {
// not sure whether this should be "darwin" or "libc" or "none"
// https://opensource.apple.com/source/Libc/
return 'libc';
}
}
return 'libc';
// TODO handle explicit invalid different
return '';
}
uaDetect.os = getOs;

115
_webi/webi-pwsh.ps1 Normal file
View File

@@ -0,0 +1,115 @@
#!/usr/bin/env pwsh
# this allows us to call ps1 files, which allows us to have spaces in filenames
# ('powershell "$Env:USERPROFILE\test.ps1" foo' will fail if it has a space in
# the path but '& "$Env:USERPROFILE\test.ps1" foo' will work even with a space)
Set-ExecutionPolicy -Scope Process Bypass
# If a command returns an error, halt the script.
$ErrorActionPreference = 'Stop'
# Ignore progress events from cmdlets so Invoke-WebRequest is not painfully slow
$ProgressPreference = 'SilentlyContinue'
# This is the canonical CPU arch when the process is emulated
$my_arch = "$Env:PROCESSOR_ARCHITEW6432"
IF ($my_arch -eq $null -or $my_arch -eq "") {
# This is the canonical CPU arch when the process is native
$my_arch = "$Env:PROCESSOR_ARCHITECTURE"
}
IF ($my_arch -eq "AMD64") {
# Because PowerShell isn't ARM yet.
# See https://oofhours.com/2020/02/04/powershell-on-windows-10-arm64/
$my_os_arch = wmic os get osarchitecture
# Using -clike because of the trailing newline
IF ($my_os_arch -clike "ARM 64*") {
$my_arch = "ARM64"
}
}
$Env:WEBI_UA = "Windows/10 $my_arch"
$exename = $args[0]
# Switch to userprofile
pushd $Env:USERPROFILE
# Make paths if needed
New-Item -Path .local\bin -ItemType Directory -Force | out-null
# TODO replace all xbin with opt\bin\
New-Item -Path .local\xbin -ItemType Directory -Force | out-null
# See note on Set-ExecutionPolicy above
Set-Content -Path .local\bin\webi.bat -Value "@echo off`r`npushd %USERPROFILE%`r`npowershell -ExecutionPolicy Bypass .local\bin\webi-pwsh.ps1 %1`r`npopd"
# Backwards-compat bugfix: remove old webi-pwsh.ps1 location
Remove-Item -Path .local\bin\webi.ps1 -Recurse -ErrorAction Ignore
if (!(Test-Path -Path .local\opt))
{
New-Item -Path .local\opt -ItemType Directory -Force | out-null
}
# TODO windows version of mktemp -d
if (!(Test-Path -Path .local\tmp))
{
New-Item -Path .local\tmp -ItemType Directory -Force | out-null
}
# TODO SetStrictMode
# TODO Test-Path variable:global:Env:WEBI_HOST ???
IF($Env:WEBI_HOST -eq $null -or $Env:WEBI_HOST -eq "")
{
$Env:WEBI_HOST = "https://webinstall.dev"
}
# {{ baseurl }}
# {{ version }}
$my_version = 'v1.1.15'
## show help if no params given or help flags are used
if ($exename -eq $null -or $exename -eq "-h" -or $exename -eq "--help" -or $exename -eq "help" -or $exename -eq "/?") {
Write-Host "webi " -ForegroundColor Green -NoNewline; Write-Host "$my_version " -ForegroundColor Red -NoNewline; Write-Host "Copyright 2020+ AJ ONeal"
Write-Host " https://webinstall.dev/webi" -ForegroundColor blue
echo ""
echo "SUMMARY"
echo " Webi is the best way to install the modern developer tools you love."
echo " It's fast, easy-to-remember, and conflict free."
echo ""
echo "USAGE"
echo " webi <thing1>[@version] [thing2] ..."
echo ""
echo "UNINSTALL"
echo " Almost everything that is installed with webi is scoped to"
echo " ~/.local/opt/<thing1>, so you can remove it like so:"
echo ""
echo " rmdir /s %USERPROFILE%\.local\opt\<thing1>"
echo " del %USERPROFILE%\.local\bin\<thing1>"
echo ""
echo " Some packages have special uninstall instructions, check"
echo " https://webinstall.dev/<thing1> to be sure."
echo ""
echo "FAQ"
Write-Host " See " -NoNewline; Write-Host "https://webinstall.dev/faq" -ForegroundColor blue
echo ""
echo "ALWAYS REMEMBER"
echo " Friends don't let friends use brew for simple, modern tools that don't need it."
exit 0
}
if ($exename -eq "-V" -or $exename -eq "--version" -or $exename -eq "version" -or $exename -eq "/v") {
Write-Host "webi " -ForegroundColor Green -NoNewline; Write-Host "$my_version " -ForegroundColor Red -NoNewline; Write-Host "Copyright 2020+ AJ ONeal"
Write-Host " https://webinstall.dev/webi" -ForegroundColor blue
exit 0
}
# Fetch <whatever>.ps1
# TODO detect formats
$PKG_URL = "$Env:WEBI_HOST/api/installers/$exename.ps1?formats=zip,exe,tar"
echo "Downloading $PKG_URL"
# Invoke-WebRequest -UserAgent "Windows amd64" "$PKG_URL" -OutFile ".\.local\tmp\$exename.install.ps1"
& curl.exe -fsSL -A "$Env:WEBI_UA" "$PKG_URL" -o .\.local\tmp\$exename.install.ps1
# Run <whatever>.ps1
powershell .\.local\tmp\$exename.install.ps1
# Done
popd

27
_webi/webi.bat Normal file
View File

@@ -0,0 +1,27 @@
@echo off
pushd "%userprofile%" || goto :error
IF NOT EXIST .local (
mkdir .local || goto :error
)
IF NOT EXIST .local\bin (
mkdir .local\bin || goto :error
)
IF NOT EXIST .local\opt (
mkdir .local\opt || goto :error
)
echo Downloading and installing %1
powershell $ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest https://webinstall.dev/packages/%1/install.ps1 -OutFile %1-webinstall.bat || goto :error
rem TODO only add if it's not in there already
PATH .local\bin;%PATH%
call %1-webinstall.bat || goto :error
del %1-webinstall.bat || goto :error
popd
goto :EOF
:error
echo Failed with error #%errorlevel%.
exit /b %errorlevel%

1
_webi/webi.ps1.bat Normal file
View File

@@ -0,0 +1 @@
powershell -ExecutionPolicy Bypass .\.local\bin\webi-pwsh.ps1 %1

View File

@@ -72,8 +72,8 @@ gc "feat: new feature"
### Common aliases
Use *alias*es to make other tools you find around webi even _more_ convenient ⚡️
(and powerful 💪).
Use *alias*es to make other tools you find around webi even _more_ convenient
⚡️ (and powerful 💪).
```sh
aliasman curl 'curlie'

View File

@@ -1,2 +0,0 @@
github_sources = BeyondCodeBootcamp/aliasman
git_url = https://github.com/BeyondCodeBootcamp/aliasman.git

28
aliasman/releases.js Normal file
View File

@@ -0,0 +1,28 @@
'use strict';
var githubSource = require('../_common/github-source.js');
var owner = 'BeyondCodeBootcamp';
var repo = 'aliasman';
module.exports = function (request) {
let arches = [
'amd64',
'arm64',
'armv6l',
'armv7l',
'ppc64le',
's390x',
'x86',
];
let oses = ['freebsd', 'linux', 'macos', 'posix'];
return githubSource(request, owner, repo, oses, arches).then(function (all) {
return all;
});
};
if (module === require.main) {
module.exports(require('@root/request')).then(function (all) {
all = require('../_webi/normalize.js')(all);
console.info(JSON.stringify(all, null, 2));
});
}

View File

@@ -9,12 +9,9 @@ To update or switch versions, run `webi arc@stable` (or `@v3.5`, `@beta`, etc).
### Files
These are the files / directories that are created and/or modified with this
install:
```text
~/.config/envman/PATH.env
~/.local/bin/arc
~/.local/opt/archiver/
```
## Cheat Sheet

View File

@@ -15,39 +15,41 @@ $pkg_src_bin = "$Env:USERPROFILE\.local\opt\archiver-v$Env:WEBI_VERSION\bin"
$pkg_src_dir = "$Env:USERPROFILE\.local\opt\archiver-v$Env:WEBI_VERSION"
$pkg_src = "$pkg_src_cmd"
New-Item "$Env:USERPROFILE\Downloads\webi" -ItemType Directory -Force | Out-Null
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 archiver from $Env:WEBI_PKG_URL to $pkg_download"
IF (!(Test-Path -Path "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE"))
{
echo "Downloading archiver 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"
& move "$pkg_download.part" "$pkg_download"
}
if (!(Test-Path -Path "$pkg_src_cmd")) {
Write-Output "Installing archiver"
IF (!(Test-Path -Path "$pkg_src_cmd"))
{
echo "Installing archiver"
# TODO: create package-specific temp directory
# Enter tmp
Push-Location .local\tmp
pushd .local\tmp
# Remove any leftover tmp cruft
Remove-Item -Path ".\arc-*" -Recurse -ErrorAction Ignore
Remove-Item -Path ".\arc.exe" -Recurse -ErrorAction Ignore
# Remove any leftover tmp cruft
Remove-Item -Path ".\arc-*" -Recurse -ErrorAction Ignore
Remove-Item -Path ".\arc.exe" -Recurse -ErrorAction Ignore
# Move single binary into root of temporary folder
& Move-Item "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE" "arc.exe"
# Move single binary into root of temporary folder
& move "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE" "arc.exe"
# Settle unpacked archive into place
Write-Output "Install Location: $pkg_src_cmd"
New-Item "$pkg_src_bin" -ItemType Directory -Force | Out-Null
Move-Item -Path "arc.exe" -Destination "$pkg_src_bin"
# Settle unpacked archive into place
echo "Install Location: $pkg_src_cmd"
New-Item "$pkg_src_bin" -ItemType Directory -Force | out-null
Move-Item -Path "arc.exe" -Destination "$pkg_src_bin"
# Exit tmp
Pop-Location
popd
}
Write-Output "Copying into '$pkg_dst_cmd' from '$pkg_src_cmd'"
Remove-Item -Path "$pkg_dst_cmd" -Recurse -ErrorAction Ignore | Out-Null
echo "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

View File

@@ -1 +0,0 @@
github_releases = mholt/archiver

20
arc/releases.js Normal file
View File

@@ -0,0 +1,20 @@
'use strict';
var github = require('../_common/github.js');
var owner = 'mholt';
var repo = 'archiver';
module.exports = function (request) {
return github(request, owner, repo).then(function (all) {
return all;
});
};
if (module === require.main) {
module.exports(require('@root/request')).then(function (all) {
all = require('../_webi/normalize.js')(all);
// just select the first 5 for demonstration
all.releases = all.releases.slice(0, 5);
console.info(JSON.stringify(all, null, 2));
});
}

View File

@@ -1,5 +1,5 @@
#!/bin/pwsh
Write-Output "'archiver@$Env:WEBI_TAG' is an alias for 'arc@$Env:WEBI_VERSION'"
if ($null -eq $Env:WEBI_HOST -or $Env:WEBI_HOST -eq "") { $Env:WEBI_HOST = "https://webinstall.dev" }
curl.exe -A MS -fsSL "$Env:WEBI_HOST/arc@$Env:WEBI_VERSION" | powershell
echo "'archiver@$Env:WEBI_TAG' is an alias for 'arc@$Env:WEBI_VERSION'"
IF ($Env:WEBI_HOST -eq $null -or $Env:WEBI_HOST -eq "") { $Env:WEBI_HOST = "https://webinstall.dev" }
curl.exe -fsSL "$Env:WEBI_HOST/arc@$Env:WEBI_VERSION" | powershell

View File

@@ -1,538 +0,0 @@
---
title: atomicparsley
homepage: https://github.com/wez/atomicparsley
tagline: |
AtomicParsley is a lightweight tool for reading, parsing and setting iTunes-style metadata.
---
To update or switch versions, run `webi atomicparsley@stable` (or `@v20221229`,
`@beta`, etc).
### Files
These are the files / directories that are created and/or modified with this
install:
```text
~/.config/envman/PATH.env
~/.local/bin/AtomicParsley
```
**Windows Users**
```text
\Windows\System32\vcruntime140.dll
```
This will also attempt to install the
[Microsoft Visual C++ Redistributable](/vcruntime) via `webi vcruntime`. If it
fails and you get the error _`vcruntime140.dll` was not found_, you'll need to
[install it manually](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170).
## Cheat Sheet
> `AtomicParsley` is an alternative to `ffmpeg` and `ffprobe` for viewing and
> changing metadata in MPEG-4 files with .3gp, .3g2, .mp4, .m4a, .m4b, .m4p,
> .m4r, and .m4v extensions. Because it's purpose-built for MP4 containers, it
> can do some things `ffmpeg` can't.
### How to Change Album Art
1. Save any existing cover art
```sh
AtomicParsley ./my-song.m4a --extractPixToPath ./ 'my-song'
```
2. Remove the artwork
```sh
AtomicParsley ./my-song.m4a --artwork REMOVE_ALL
```
3. **(macOS ONLY)** Add new artwork at the specified DPI (and other constraints)
```sh
export PIC_OPTIONS="DPI=72"
AtomicParsley ./my-podcast-audiobook.m4b --artwork ./my-season-1-cover.jpg
```
Only JPEG and PNG are supported. See `PIC_OPTIONS` down below for more options.
If you get an error, try exporting your file with a DPI to 72 (or up to 300 with
`PIC_OPTIONS` set) and a smaller resolution - perhaps 600x600 or 1500x1500 (what
old versions iTunes versions used).
### How to Remove Apple Account Info
```sh
AtomicParsley ./my-file.m4a \
--DeepScan \
--manualAtomRemove "moov.trak.mdia.minf.stbl.mp4a.pinf" \
--manualAtomRemove "moov.udta.meta.ilst.apID" \
--manualAtomRemove "moov.udta.meta.ilst.cnID" \
--manualAtomRemove "moov.udta.meta.ilst.purd" \
--manualAtomRemove "moov.udta.meta.ilst.sfID" \
--manualAtomRemove "moov.udta.meta.ilst.soal" \
--manualAtomRemove "moov.udta.meta.ilst.xid"
```
If you wanted to also remove information that indicates which Country or
Language the song was purchased in, or which album it was purchased from, there
are some additional IDs to consider:
| Metadata Tag | Description |
| ---------------------------------------- | -------------------------------------------- |
| moov.udta.meta.ilst.apID | Apple account email address |
| moov.udta.meta.ilst.ownr | Apple account username |
| moov.udta.meta.ilst.atID | Artist-track ID |
| moov.udta.meta.ilst.cnID | iTunes Catalog ID |
| moov.udta.meta.ilst.geID | Genre ID |
| moov.udta.meta.ilst.plID | Playlist ID (identifies album) |
| moov.udta.meta.ilst.sfID | iTunes store identifier (location/number) |
| moov.udta.meta.ilst.cprt | Copyright information |
| moov.udta.meta.ilst.flvr | Bitrate/video size related |
| moov.udta.meta.ilst.purd | Date purchased |
| moov.udta.meta.ilst.rtng | Explicit/Clean information |
| moov.udta.meta.ilst.soal | Album sort name |
| moov.udta.meta.ilst.stik | Media type information |
| moov.udta.meta.ilst.xid | Vendor xID |
| moov.udta.meta.ilst.----.name:[iTunMOVI] | Embedded plist contains filesize and flavor. |
| moov.trak.mdia.minf.stbl.stsd.mp4a.pinf | Purchase information related |
See <https://gist.github.com/riophae/f5694fd2952cb64982689b971ca6ec79>.
### Genre Lists
All values are **case sensitive**.
#### The "stik" List
These values are **case sensitive**:
0. `Home Video`
1. `Normal` (meaning music)
2. `Audiobook` (changes extension to .m4b)
3. `Whacked Bookmark`
4. `Music Video`
5. `Movie`
6. `Short Film`
7. `TV Show`
8. `Booklet`
#### Standard Music Genres
```txt
(1.) Blues
(2.) Classic Rock
(3.) Country
(4.) Dance
(5.) Disco
(6.) Funk
(7.) Grunge
(8.) Hip-Hop
(9.) Jazz
(10.) Metal
(11.) New Age
(12.) Oldies
(13.) Other
(14.) Pop
(15.) R&B
(16.) Rap
(17.) Reggae
(18.) Rock
(19.) Techno
(20.) Industrial
(21.) Alternative
(22.) Ska
(23.) Death Metal
(24.) Pranks
(25.) Soundtrack
(26.) Euro-Techno
(27.) Ambient
(28.) Trip-Hop
(29.) Vocal
(30.) Jazz+Funk
(31.) Fusion
(32.) Trance
(33.) Classical
(34.) Instrumental
(35.) Acid
(36.) House
(37.) Game
(38.) Sound Clip
(39.) Gospel
(40.) Noise
(41.) AlternRock
(42.) Bass
(43.) Soul
(44.) Punk
(45.) Space
(46.) Meditative
(47.) Instrumental Pop
(48.) Instrumental Rock
(49.) Ethnic
(50.) Gothic
(51.) Darkwave
(52.) Techno-Industrial
(53.) Electronic
(54.) Pop-Folk
(55.) Eurodance
(56.) Dream
(57.) Southern Rock
(58.) Comedy
(59.) Cult
(60.) Gangsta
(61.) Top 40
(62.) Christian Rap
(63.) Pop/Funk
(64.) Jungle
(65.) Native American
(66.) Cabaret
(67.) New Wave
(68.) Psychadelic
(69.) Rave
(70.) Showtunes
(71.) Trailer
(72.) Lo-Fi
(73.) Tribal
(74.) Acid Punk
(75.) Acid Jazz
(76.) Polka
(77.) Retro
(78.) Musical
(79.) Rock & Roll
(80.) Hard Rock
(81.) Folk
(82.) Folk/Rock
(83.) National Folk
(84.) Swing
(85.) Fast Fusion
(86.) Bebob
(87.) Latin
(88.) Revival
(89.) Celtic
(90.) Bluegrass
(91.) Avantgarde
(92.) Gothic Rock
(93.) Progressive Rock
(94.) Psychedelic Rock
(95.) Symphonic Rock
(96.) Slow Rock
(97.) Big Band
(98.) Chorus
(99.) Easy Listening
(100.) Acoustic
(101.) Humour
(102.) Speech
(103.) Chanson
(104.) Opera
(105.) Chamber Music
(106.) Sonata
(107.) Symphony
(108.) Booty Bass
(109.) Primus
(110.) Porn Groove
(111.) Satire
(112.) Slow Jam
(113.) Club
(114.) Tango
(115.) Samba
(116.) Folklore
(117.) Ballad
(118.) Power Ballad
(119.) Rhythmic Soul
(120.) Freestyle
(121.) Duet
(122.) Punk Rock
(123.) Drum Solo
(124.) A Capella
(125.) Euro-House
(126.) Dance Hall
```
#### iTunes Movie Genre IDs
```sh
AtomicParsley --genre-movie-id-list
```
```text
(4401) Action & Adventure
(4402) Anime
(4403) Classics
(4404) Comedy
(4405) Documentary
(4406) Drama
(4407) Foreign
(4408) Horror
(4409) Independent
(4410) Kids & Family
(4411) Musicals
(4412) Romance
(4413) Sci-Fi & Fantasy
(4414) Short Films
(4415) Special Interest
(4416) Thriller
(4417) Sports
(4418) Western
(4419) Urban
(4420) Holiday
(4421) Made for TV
(4422) Concert Films
(4423) Music Documentaries
(4424) Music Feature Films
(4425) Japanese Cinema
(4426) Jidaigeki
(4427) Tokusatsu
(4428) Korean Cinema
```
#### iTunes TV Genre IDs
```sh
AtomicParsley --genre-tv-id-list
```
```text
(4000) Comedy
(4001) Drama
(4002) Animation
(4003) Action & Adventure
(4004) Classic
(4005) Kids
(4005) Nonfiction
(4007) Reality TV
(4008) Sci-Fi & Fantasy
(4009) Sports
(4010) Teens
(4011) Latino TV
```
### Help
Note: Normally we don't include full help text, but since AtomicParsley has a
long history across various maintainers and repos, we feel it's appropriate to
do so in this case.
```text
AtomicParsley sets metadata into MPEG-4 files & derivatives supporting 3 tag
schemes: iTunes-style, 3GPP assets & ISO defined copyright notifications.
AtomicParsley quick help for setting iTunes-style metadata into MPEG-4 files.
General usage examples:
AtomicParsley /path/to.mp4 -T 1
AtomicParsley /path/to.mp4 -t +
AtomicParsley /path/to.mp4 --artist "Me" --artwork /path/to/art.jpg
Atomicparsley /path/to.mp4 --albumArtist "You" --podcastFlag true
Atomicparsley /path/to.mp4 --stik "TV Show" --advisory explicit
Getting information about the file & tags:
-T --test Test file for mpeg4-ishness & print atom tree
-t --textdata Prints tags embedded within the file
-E --extractPix Extracts pix to the same folder as the mpeg-4 file
Setting iTunes-style metadata tags
--artist (string) Set the artist tag
--title (string) Set the title tag
--album (string) Set the album tag
--genre (string) Genre tag (see --longhelp for more info)
--tracknum (num)[/tot] Track number (or track number/total tracks)
--disk (num)[/tot] Disk number (or disk number/total disks)
--comment (string) Set the comment tag
--year (num|UTC) Year tag (see --longhelp for "Release Date")
--lyrics (string) Set lyrics (not subject to 256 byte limit)
--lyricsFile (/path) Set lyrics to the content of a file
--composer (string) Set the composer tag
--copyright (string) Set the copyright tag
--grouping (string) Set the grouping tag
--artwork (/path) Set a piece of artwork (jpeg or png only)
--bpm (number) Set the tempo/bpm
--albumArtist (string) Set the album artist tag
--compilation (boolean) Set the compilation flag (true or false)
--hdvideo (number) Set the hdvideo flag to one of:
false or 0 for standard definition
true or 1 for 720p
2 for 1080p
--advisory (string*) Content advisory (*values: 'clean', 'explicit')
--stik (string*) Sets the iTunes "stik" atom (see --longhelp)
--description (string) Set the description tag
--longdesc (string) Set the long description tag
--storedesc (string) Set the store description tag
--TVNetwork (string) Set the TV Network name
--TVShowName (string) Set the TV Show name
--TVEpisode (string) Set the TV episode/production code
--TVSeasonNum (number) Set the TV Season number
--TVEpisodeNum (number) Set the TV Episode number
--podcastFlag (boolean) Set the podcast flag (true or false)
--category (string) Sets the podcast category
--keyword (string) Sets the podcast keyword
--podcastURL (URL) Set the podcast feed URL
--podcastGUID (URL) Set the episode's URL tag
--purchaseDate (UTC) Set time of purchase
--encodingTool (string) Set the name of the encoder
--encodedBy (string) Set the name of the Person/company who encoded the file
--apID (string) Set the Account Name
--cnID (number) Set the iTunes Catalog ID (see --longhelp)
--geID (number) Set the iTunes Genre ID (see --longhelp)
--xID (string) Set the vendor-supplied iTunes xID (see --longhelp)
--gapless (boolean) Set the gapless playback flag
--contentRating (string*) Set tv/mpaa rating (see -rDNS-help)
Deleting tags
Set the value to "": --artist "" --stik "" --bpm ""
To delete (all) artwork: --artwork REMOVE_ALL
manually removal: --manualAtomRemove "moov.udta.meta.ilst.ATOM"
More detailed iTunes help is available with AtomicParsley --longhelp
Setting reverse DNS forms for iTunes files: see --reverseDNS-help
Setting 3gp assets into 3GPP & derivative files: see --3gp-help
Setting copyright notices for all files: see --ISO-help
For file-level options & padding info: see --file-help
Setting custom private tag extensions: see --uuid-help
Setting ID3 tags onto mpeg-4 files: see --ID3-help
----------------------------------------------------------------------
AtomicParsley version: 20221229.172126.0 d813aa6e0304ed3ab6d92f1ae96cd52b586181ec (utf8)
Submit bug fixes to https://github.com/wez/atomicparsley
```
### `--longhelp`
```text
AtomicParsley help page for setting iTunes-style metadata into MPEG-4 files.
(3gp help available with AtomicParsley --3gp-help)
(ISO copyright help available with AtomicParsley --ISO-help)
(reverse DNS form help available with AtomicParsley --reverseDNS-help)
Usage: AtomicParsley [mp4FILE]... [OPTION]... [ARGUMENT]... [ [OPTION2]...[ARGUMENT2]...]
example: AtomicParsley /path/to.mp4 -e ~/Desktop/pix
example: AtomicParsley /path/to.mp4 --podcastURL "http://www.url.net" --tracknum 45/356
example: AtomicParsley /path/to.mp4 --copyright "℗ © 2006"
example: AtomicParsley /path/to.mp4 --year "2006-07-27T14:00:43Z" --purchaseDate timestamp
example: AtomicParsley /path/to.mp4 --sortOrder artist "Mighty Dub Cats, The
------------------------------------------------------------------------------------------------
Extract any pictures in user data "covr" atoms to separate files.
--extractPix , -E Extract to same folder (basename derived from file).
--extractPixToPath , -e (/path/basename) Extract to specific path (numbers added to basename).
example: --e ~/Desktop/SomeText
gives: SomeText_artwork_1.jpg SomeText_artwork_2.png
Note: extension comes from embedded image file format
------------------------------------------------------------------------------------------------
Tag setting options:
--artist , -a (str) Set the artist tag: "moov.udta.meta.ilst.©ART.data"
--title , -s (str) Set the title tag: "moov.udta.meta.ilst.©nam.data"
--album , -b (str) Set the album tag: "moov.udta.meta.ilst.©alb.data"
--genre , -g (str) Set the genre tag: "©gen" (custom) or "gnre" (standard).
see the standard list with "AtomicParsley --genre-list"
--tracknum , -k (num)[/tot] Set the track number (or track number & total tracks).
--disk , -d (num)[/tot] Set the disk number (or disk number & total disks).
--comment , -c (str) Set the comment tag: "moov.udta.meta.ilst.©cmt.data"
--year , -y (num|UTC) Set the year tag: "moov.udta.meta.ilst.©day.data"
set with UTC "2006-09-11T09:00:00Z" for Release Date
--lyrics , -l (str) Set the lyrics tag: "moov.udta.meta.ilst.©lyr.data"
--lyricsFile , (/path) Set the lyrics tag to the content of a file
--composer , -w (str) Set the composer tag: "moov.udta.meta.ilst.©wrt.data"
--copyright , -x (str) Set the copyright tag: "moov.udta.meta.ilst.cprt.data"
--grouping , -G (str) Set the grouping tag: "moov.udta.meta.ilst.©grp.data"
--artwork , -A (/path) Set a piece of artwork (jpeg or png) on "covr.data"
Note: multiple pieces are allowed with more --artwork args
--bpm , -B (num) Set the tempo/bpm tag: "moov.udta.meta.ilst.tmpo.data"
--albumArtist , -A (str) Set the album artist tag: "moov.udta.meta.ilst.aART.data"
--compilation , -C (bool) Sets the "cpil" atom (true or false to delete the atom)
--hdvideo , -V (bool) Sets the "hdvd" atom (true or false to delete the atom)
--advisory , -y (1of3) Sets the iTunes lyrics advisory ('remove', 'clean', 'explicit')
--stik , -S (1of7) Sets the iTunes "stik" atom (--stik "remove" to delete)
"Movie", "Normal", "TV Show" .... others:
see the full list with "AtomicParsley --stik-list"
or set in an integer value with --stik value=(num)
Note: --stik Audiobook will change file extension to '.m4b'
--description , -p (str) Sets the description on the "desc" atom
--Rating , (str) Sets the Rating on the "rate" atom
--longdesc , -j (str) Sets the long description on the "ldes" atom
--storedesc , (str) Sets the iTunes store description on the "sdes" atom
--TVNetwork , -n (str) Sets the TV Network name on the "tvnn" atom
--TVShowName , -H (str) Sets the TV Show name on the "tvsh" atom
--TVEpisode , -I (str) Sets the TV Episode on "tven":"209", but it is a string: "209 Part 1"
--TVSeasonNum , -U (num) Sets the TV Season number on the "tvsn" atom
--TVEpisodeNum , -N (num) Sets the TV Episode number on the "tves" atom
--podcastFlag , -f (bool) Sets the podcast flag (values are "true" or "false")
--category , -q (str) Sets the podcast category; typically a duplicate of its genre
--keyword , -K (str) Sets the podcast keyword; invisible to MacOSX Spotlight
--podcastURL , -L (URL) Set the podcast feed URL on the "purl" atom
--podcastGUID , -J (URL) Set the episode's URL tag on the "egid" atom
--purchaseDate , -D (UTC) Set Universal Coordinated Time of purchase on a "purd" atom
(use "timestamp" to set UTC to now; can be akin to id3v2 TDTG tag)
--encodingTool , (str) Set the name of the encoder on the "©too" atom
--encodedBy , (str) Set the name of the Person/company who encoded the file on the "©enc" atom
--apID , -Y (str) Set the name of the Account Name on the "apID" atom
--cnID , (num) Set iTunes Catalog ID, used for combining SD and HD encodes in iTunes on the "cnID" atom
To combine you must set "hdvd" atom on one file and must have same "stik" on both file
Must not use "stik" of value Home Video(0), use Movie(9)
iTunes Catalog numbers can be obtained by finding the item in the iTunes Store. Once item
is found in the iTunes Store right click on picture of item and select copy link. Paste this link
into a document or web browser to display the catalog number ID.
An example link for the video Street Kings is:
http://itunes.apple.com/WebObjects/MZStore.woa/wa/viewMovie?id=278743714&s=143441
Here you can see the cnID is 278743714
Alternatively you can use iMDB numbers, however these will not match the iTunes catalog.
--geID , (num) Set iTunes Genre ID. This does not necessarily have to match genre.
See --genre-movie-id-list and --genre-tv-id-list
--xID , (str) Set iTunes vendor-supplied xID, used to allow iTunes LPs and iTunes Extras to interact
with other content in your iTunes Library
--gapless , (bool) Sets the gapless playback flag for a track in a gapless album
--sortOrder (type) (str) Sets the sort order string for that type of tag.
(available types are: "name", "artist", "albumartist",
"album", "composer", "show")
NOTE: Except for artwork, only 1 of each tag is allowed; artwork allows multiple pieces.
NOTE: Tags that carry text(str) have a limit of 255 utf8 characters;
however lyrics and long descriptions have no limit.
------------------------------------------------------------------------------------------------
To delete a single atom, set the tag to null (except artwork):
--artist "" --lyrics ""
--artwork REMOVE_ALL
--metaEnema , -P Douches away every atom under "moov.udta.meta.ilst"
--foobar2000Enema , -2 Eliminates foobar2000's non-compliant so-out-o-spec tagging scheme
--manualAtomRemove "some.atom.path" where some.atom.path can be:
keys to using manualAtomRemove:
ilst.ATOM.data or ilst.ATOM target an iTunes-style metadata tag
ATOM:lang=foo target an atom with this language setting; like 3gp assets
ATOM.----.name:[foo] target a reverseDNS metadata tag; like iTunNORM
Note: these atoms show up with 'AP -t' as: Atom "----" [foo]
'foo' is actually carried on the 'name' atom
ATOM[x] target an atom with an index other than 1; like trak[2]
ATOM.uuid=hex-hex-hex-hex targt a uuid atom with the uuid of hex string representation
examples:
moov.udta.meta.ilst.----.name:[iTunNORM] moov.trak[3].cprt:lang=urd
moov.trak[2].uuid=55534d54-21d2-4fce-bb88-695cfac9c740
------------------------------------------------------------------------------------------------
Environmental Variables (affecting picture placement) (macOS ONLY)
set PIC_OPTIONS in your shell to set these flags; preferences are separated by colons (:)
MaxDimensions=num (default: 0; unlimited); sets maximum pixel dimensions
DPI=num (default: 72); sets dpi
MaxKBytes=num (default: 0; unlimited); maximum kilobytes for file (jpeg only)
AddBothPix=bool (default: false); add original & converted pic (for archival purposes)
AllPixJPEG | AllPixPNG =bool (default: false); force conversion to a specific picture format
SquareUp (include to square images to largest dimension, allows an [ugly] 160x1200->1200x1200)
removeTempPix (include to delete temp pic files created when resizing images after tagging)
ForceHeight=num (must also specify width, below) force image pixel height
ForceWidth=num (must also specify height, above) force image pixel width
Examples: (bash-style)
export PIC_OPTIONS="MaxDimensions=400:DPI=72:MaxKBytes=100:AddBothPix=true:AllPixJPEG=true"
export PIC_OPTIONS="SquareUp:removeTempPix"
export PIC_OPTIONS="ForceHeight=999:ForceWidth=333:removeTempPix"
------------------------------------------------------------------------------------------------
```
```
```

View File

@@ -1,60 +0,0 @@
#!/usr/bin/env pwsh
#########################
# Install AtomicParsley #
#########################
# Every package should define these variables
$pkg_cmd_name = "AtomicParsley"
$pkg_dst_cmd = "$Env:USERPROFILE\.local\bin\AtomicParsley.exe"
$pkg_dst_bin = "$Env:USERPROFILE\.local\bin"
$pkg_dst = "$pkg_dst_cmd"
$pkg_src_cmd = "$Env:USERPROFILE\.local\opt\AtomicParsley-v$Env:WEBI_VERSION\bin\AtomicParsley.exe"
$pkg_src_bin = "$Env:USERPROFILE\.local\opt\AtomicParsley-v$Env:WEBI_VERSION\bin"
$pkg_src_dir = "$Env:USERPROFILE\.local\opt\AtomicParsley-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 "Checking for (or Installing) MSVC Runtime..."
& "$Env:USERPROFILE\.local\bin\webi-pwsh.ps1" vcruntime
Write-Output "Downloading AtomicParsley 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 AtomicParsley"
# TODO: create package-specific temp directory
# Enter tmp
Push-Location .local\tmp
# Remove any leftover tmp cruft
Remove-Item -Path ".\AtomicParsley-v*" -Recurse -ErrorAction Ignore
Remove-Item -Path ".\AtomicParsley.exe" -Recurse -ErrorAction Ignore
# Unpack archive file into this temporary directory
# Windows BSD-tar handles zip. Imagine that.
Write-Output "Unpacking $pkg_download"
& tar xf "$pkg_download"
# Settle unpacked archive into place
Write-Output "Install Location: $pkg_src_cmd"
New-Item "$pkg_src_bin" -ItemType Directory -Force | Out-Null
Move-Item -Path ".\AtomicParsley.exe" -Destination "$pkg_src_bin"
# Exit tmp
Pop-Location
}
Write-Output "Copying into '$pkg_dst_cmd' from '$pkg_src_cmd'"
Remove-Item -Path "$pkg_dst_cmd" -Recurse -ErrorAction Ignore | Out-Null
New-Item "$pkg_dst_bin" -ItemType Directory -Force | Out-Null
Copy-Item -Path "$pkg_src" -Destination "$pkg_dst" -Recurse

View File

@@ -1,41 +0,0 @@
#!/bin/sh
set -e
set -u
__init_atomicparsley() {
#########################
# Install AtomicParsley #
#########################
WEBI_SINGLE=true
# Every package should define these 6 variables
pkg_cmd_name="AtomicParsley"
pkg_dst_cmd="$HOME/.local/bin/AtomicParsley"
pkg_dst="$pkg_dst_cmd"
pkg_src_cmd="$HOME/.local/opt/AtomicParsley-v$WEBI_VERSION/bin/AtomicParsley"
pkg_src_dir="$HOME/.local/opt/AtomicParsley-v$WEBI_VERSION"
pkg_src="$pkg_src_cmd"
# pkg_install must be defined by every package
pkg_install() {
# ~/.local/opt/AtomicParsley-v20221229.172126.0/bin
mkdir -p "$(dirname "$pkg_src_cmd")"
# mv ./AtomicParsley ~/.local/opt/AtomicParsley-v20221229.172126.0/bin/AtomicParsley
mv ./"$pkg_cmd_name" "$pkg_src_cmd"
}
pkg_get_current_version() {
# 'AtomicParsley --version' has output in this format:
# AtomicParsley version: 20221229.172126.0 d813aa6e0304ed3ab6d92f1ae96cd52b586181ec (utf8)
# This trims it down to just the version number:
# 20221229.172126.0
AtomicParsley --version 2> /dev/null | head -n 1 | cut -d' ' -f3
}
}
__init_atomicparsley

View File

@@ -1 +0,0 @@
github_releases = wez/atomicparsley

View File

@@ -7,17 +7,6 @@ tagline: |
To update or switch versions, run `webi awless@stable` (or `@v2`, `@beta`, etc).
### Files
These are the files / directories that are created and/or modified with this
install:
```text
~/.config/envman/PATH.env
~/.local/bin/awless
~/.aws/credentials
```
## Cheat Sheet
awless is modeled after popular command-line tools such as Git. Most commands

View File

@@ -16,22 +16,22 @@ $pkg_src_bin = "$Env:USERPROFILE\.local\opt\awless-v$Env:WEBI_VERSION\bin"
$pkg_src_dir = "$Env:USERPROFILE\.local\opt\awless-v$Env:WEBI_VERSION"
$pkg_src = "$pkg_src_cmd"
New-Item "$Env:USERPROFILE\Downloads\webi" -ItemType Directory -Force | Out-Null
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 awless from $Env:WEBI_PKG_URL to $pkg_download"
IF (!(Test-Path -Path "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE")) {
echo "Downloading awless 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"
& move "$pkg_download.part" "$pkg_download"
}
if (!(Test-Path -Path "$pkg_src_cmd")) {
Write-Output "Installing awless"
IF (!(Test-Path -Path "$pkg_src_cmd")) {
echo "Installing awless"
# TODO: create package-specific temp directory
# Enter tmp
Push-Location .local\tmp
pushd .local\tmp
# Remove any leftover tmp cruft
Remove-Item -Path ".\awless-v*" -Recurse -ErrorAction Ignore
@@ -39,19 +39,19 @@ if (!(Test-Path -Path "$pkg_src_cmd")) {
# Unpack archive file into this temporary directory
# Windows BSD-tar handles zip. Imagine that.
Write-Output "Unpacking $pkg_download"
echo "Unpacking $pkg_download"
& tar xf "$pkg_download"
# Settle unpacked archive into place
Write-Output "Install Location: $pkg_src_cmd"
New-Item "$pkg_src_bin" -ItemType Directory -Force | Out-Null
echo "Install Location: $pkg_src_cmd"
New-Item "$pkg_src_bin" -ItemType Directory -Force | out-null
Move-Item -Path ".\awless.exe" -Destination "$pkg_src_bin"
# Exit tmp
Pop-Location
popd
}
Write-Output "Copying into '$pkg_dst_cmd' from '$pkg_src_cmd'"
Remove-Item -Path "$pkg_dst_cmd" -Recurse -ErrorAction Ignore | Out-Null
New-Item "$pkg_dst_bin" -ItemType Directory -Force | Out-Null
echo "Copying into '$pkg_dst_cmd' from '$pkg_src_cmd'"
Remove-Item -Path "$pkg_dst_cmd" -Recurse -ErrorAction Ignore | out-null
New-Item "$pkg_dst_bin" -ItemType Directory -Force | out-null
Copy-Item -Path "$pkg_src" -Destination "$pkg_dst" -Recurse

View File

@@ -1 +0,0 @@
github_releases = wallix/awless

22
awless/releases.js Normal file
View File

@@ -0,0 +1,22 @@
'use strict';
var github = require('../_common/github.js');
var owner = 'wallix';
var repo = 'awless';
module.exports = function (request) {
return github(request, owner, repo).then(function (all) {
// remove checksums and .deb
all.releases = all.releases.filter(function (rel) {
return !/(\.txt)|(\.deb)$/i.test(rel.name);
});
return all;
});
};
if (module === require.main) {
module.exports(require('@root/request')).then(function (all) {
all = require('../_webi/normalize.js')(all);
console.info(JSON.stringify(all));
});
}

View File

@@ -9,31 +9,26 @@ To update or switch versions, run `webi bat@stable` (or `@v0.18`, `@beta`, etc).
### Files
These are the files / directories that are created and/or modified with this
install:
```text
~/.config/envman/PATH.env
~/.config/bat/config
~/.local/opt/bat/
```
**Windows Users**
```text
\Windows\System32\vcruntime140.dll
```
This will also attempt to install the
[Microsoft Visual C++ Redistributable](/vcruntime) via `webi vcruntime`. If it
fails and you get the error _`vcruntime140.dll` was not found_, you'll need to
[install it manually](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170).
## Cheat Sheet
> `bat` is pretty much what `cat` would be if it were developed today's in the
> world of Markdown, git, etc.
### How to run on Windows
On Windows you'll get an error like this:
> execution cannot proceed run because vcruntime140.dll was not found
You need to download and install the
[Microsoft Visual C++ Redistributable](https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads)
### How to alias as `cat`
Use [aliasman](/aliasman):

View File

@@ -3,45 +3,41 @@
$VERNAME = "$Env:PKG_NAME-v$Env:WEBI_VERSION.exe"
$EXENAME = "$Env:PKG_NAME.exe"
# Fetch archive
if (!(Test-Path -Path "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE")) {
Write-Output "Downloading $Env:PKG_NAME from $Env:WEBI_PKG_URL to $Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE"
IF (!(Test-Path -Path "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE"))
{
echo "Downloading $Env:PKG_NAME from $Env:WEBI_PKG_URL to $Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE"
& curl.exe -A "$Env:WEBI_UA" -fsSL "$Env:WEBI_PKG_URL" -o "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE.part"
& Move-Item "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE.part" "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE"
& move "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE.part" "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE"
}
# Fetch MSVC Runtime
Write-Output "Checking for MSVC Runtime..."
if (-not (Test-Path "\Windows\System32\vcruntime140.dll")) {
& "$Env:USERPROFILE\.local\bin\webi-pwsh.ps1" vcruntime
}
if (!(Test-Path -Path "$Env:USERPROFILE\.local\bin\$VERNAME")) {
Write-Output "Installing $Env:PKG_NAME"
IF (!(Test-Path -Path "$Env:USERPROFILE\.local\xbin\$VERNAME"))
{
echo "Installing $Env:PKG_NAME"
# TODO: temp directory
# Enter tmp
Push-Location .local\tmp
pushd .local\tmp
# Remove any leftover tmp cruft
Remove-Item -Path "$Env:PKG_NAME-v*" -Recurse -ErrorAction Ignore
# Remove any leftover tmp cruft
Remove-Item -Path "$Env:PKG_NAME-v*" -Recurse -ErrorAction Ignore
# Unpack archive
# Windows BSD-tar handles zip. Imagine that.
Write-Output "Unpacking $Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE"
& tar xf "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE"
# Move single binary into root of temporary folder
& Move-Item "bat-*\$EXENAME" "$VERNAME"
# Unpack archive
# Windows BSD-tar handles zip. Imagine that.
echo "Unpacking $Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE"
& tar xf "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE"
# Move single binary into root of temporary folder
& move "bat-*\$EXENAME" "$VERNAME"
# Settle unpacked archive into place
Write-Output "New Name: $VERNAME"
Write-Output "New Location: $Env:USERPROFILE\.local\bin\$VERNAME"
Move-Item -Path "$VERNAME" -Destination "$Env:USERPROFILE\.local\bin"
# Settle unpacked archive into place
echo "New Name: $VERNAME"
echo "New Location: $Env:USERPROFILE\.local\xbin\$VERNAME"
Move-Item -Path "$VERNAME" -Destination "$Env:USERPROFILE\.local\xbin"
# Exit tmp
Pop-Location
popd
}
Write-Output "Copying into '$Env:USERPROFILE\.local\bin\$EXENAME' from '$Env:USERPROFILE\.local\bin\$VERNAME'"
echo "Copying into '$Env:USERPROFILE\.local\bin\$EXENAME' from '$Env:USERPROFILE\.local\xbin\$VERNAME'"
Remove-Item -Path "$Env:USERPROFILE\.local\bin\$EXENAME" -Recurse -ErrorAction Ignore
Copy-Item -Path "$Env:USERPROFILE\.local\bin\$VERNAME" -Destination "$Env:USERPROFILE\.local\bin\$EXENAME" -Recurse
Copy-Item -Path "$Env:USERPROFILE\.local\xbin\$VERNAME" -Destination "$Env:USERPROFILE\.local\bin\$EXENAME" -Recurse

View File

@@ -19,7 +19,7 @@ __init_bat() {
}
pkg_install() {
# ~/.local/bin
# ~/.local/xbin
mkdir -p "$pkg_src_bin"
# mv ./bat-*/bat ~/.local/opt/bat-v0.15.4/bin/bat

View File

@@ -1 +0,0 @@
github_releases = sharkdp/bat

20
bat/releases.js Normal file
View File

@@ -0,0 +1,20 @@
'use strict';
var github = require('../_common/github.js');
var owner = 'sharkdp';
var repo = 'bat';
module.exports = function (request) {
return github(request, owner, repo).then(function (all) {
return all;
});
};
if (module === require.main) {
module.exports(require('@root/request')).then(function (all) {
all = require('../_webi/normalize.js')(all);
all.releases = all.releases.slice(0, 10);
//console.info(JSON.stringify(all));
console.info(JSON.stringify(all, null, 2));
});
}

View File

@@ -5,24 +5,13 @@ tagline: |
Brew: The Missing Package Manager for macOS (and Linux).
---
To update or switch versions, use brew's built-in upgrade management, or our
handy update service script:
## Updating `brew`
`brew` has its own built-in upgrade management.
```sh
brew update
brew upgrade
brew-update-service-install # Webi helper script
```
### Files
These are the files / directories that are created and/or modified with this
install:
```text
~/.config/envman/PATH.env
~/.local/opt/brew/
~/.local/bin/brew-update-service-install
```
## Cheat Sheet

View File

@@ -6,23 +6,14 @@ main() { (
sed '1,/^#~\/.local\/bin\/brew-updater/d' "${0}" > ~/.local/bin/brew-update-hourly
chmod a+x ~/.local/bin/brew-update-hourly
echo "Checking for serviceman..."
~/.local/bin/webi serviceman
if ! command -v serviceman > /dev/null; then
export PATH="$HOME/.local/bin:$PATH"
fi
serviceman --version
serviceman add --agent \
--workdir ~/.local/opt/brew/ \
env PATH="$PATH" serviceman add --user \
--name sh.brew.updater -- \
~/.local/bin/brew-update-hourly
); }
if ! main; then
exit 1
if main; then
exit 0
fi
exit 0
#~/.local/bin/brew-updater
#!/bin/sh

View File

@@ -132,7 +132,9 @@ file)
```
3. Add your project to the system launcher, running as the current user
```sh
serviceman add --name 'my-project' --daemon -- \
sudo env PATH="$PATH" \
serviceman add --path="$PATH" --system \
--username "$(whoami)" --name my-project -- \
bun run ./my-project.js
```
4. Restart the logging service
@@ -153,6 +155,6 @@ For **macOS**:
```
3. Add your project to the system launcher, running as the current user
```sh
serviceman add --agent --name 'my-project' -- \
serviceman add --path="$PATH" --user --name my-project -- \
bun run ./my-project.js
```

View File

@@ -1,5 +0,0 @@
github_releases = oven-sh/bun
tag_prefix = bun-
default_x86_64 = x86_64_v3
x86_64_v2 = baseline
variants = profile

32
bun/releases.js Normal file
View File

@@ -0,0 +1,32 @@
'use strict';
var github = require('../_common/github.js');
var owner = 'oven-sh';
var repo = 'bun';
module.exports = function (request) {
return github(request, owner, repo).then(function (all) {
all.releases = all.releases
.filter(function (r) {
let isDebug = /-profile/.test(r.name);
if (!isDebug) {
return true;
}
})
.map(function (r) {
// bun-v0.5.1 => v0.5.1
r.version = r.version.replace(/bun-/g, '');
return r;
});
return all;
});
};
if (module === require.main) {
module.exports(require('@root/request')).then(function (all) {
all = require('../_webi/normalize.js')(all);
// just select the first 5 for demonstration
all.releases = all.releases.slice(0, 5);
console.info(JSON.stringify(all, null, 2));
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,22 +16,22 @@ $pkg_src_bin = "$Env:USERPROFILE\.local\opt\caddy-v$Env:WEBI_VERSION\bin"
$pkg_src_dir = "$Env:USERPROFILE\.local\opt\caddy-v$Env:WEBI_VERSION"
$pkg_src = "$pkg_src_cmd"
New-Item "$Env:USERPROFILE\Downloads\webi" -ItemType Directory -Force | Out-Null
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 caddy from $Env:WEBI_PKG_URL to $pkg_download"
IF (!(Test-Path -Path "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE")) {
echo "Downloading caddy 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"
& move "$pkg_download.part" "$pkg_download"
}
if (!(Test-Path -Path "$pkg_src_cmd")) {
Write-Output "Installing caddy"
IF (!(Test-Path -Path "$pkg_src_cmd")) {
echo "Installing caddy"
# TODO: create package-specific temp directory
# Enter tmp
Push-Location .local\tmp
pushd .local\tmp
# Remove any leftover tmp cruft
Remove-Item -Path ".\caddy-v*" -Recurse -ErrorAction Ignore
@@ -39,19 +39,19 @@ if (!(Test-Path -Path "$pkg_src_cmd")) {
# Unpack archive file into this temporary directory
# Windows BSD-tar handles zip. Imagine that.
Write-Output "Unpacking $pkg_download"
echo "Unpacking $pkg_download"
& tar xf "$pkg_download"
# Settle unpacked archive into place
Write-Output "Install Location: $pkg_src_cmd"
New-Item "$pkg_src_bin" -ItemType Directory -Force | Out-Null
echo "Install Location: $pkg_src_cmd"
New-Item "$pkg_src_bin" -ItemType Directory -Force | out-null
Move-Item -Path ".\caddy.exe" -Destination "$pkg_src_bin"
# Exit tmp
Pop-Location
popd
}
Write-Output "Copying into '$pkg_dst_cmd' from '$pkg_src_cmd'"
Remove-Item -Path "$pkg_dst_cmd" -Recurse -ErrorAction Ignore | Out-Null
New-Item "$pkg_dst_bin" -ItemType Directory -Force | Out-Null
echo "Copying into '$pkg_dst_cmd' from '$pkg_src_cmd'"
Remove-Item -Path "$pkg_dst_cmd" -Recurse -ErrorAction Ignore | out-null
New-Item "$pkg_dst_bin" -ItemType Directory -Force | out-null
Copy-Item -Path "$pkg_src" -Destination "$pkg_dst" -Recurse

View File

@@ -1 +0,0 @@
github_releases = caddyserver/caddy

22
caddy/releases.js Normal file
View File

@@ -0,0 +1,22 @@
'use strict';
var github = require('../_common/github.js');
var owner = 'caddyserver';
var repo = 'caddy';
module.exports = function (request) {
return github(request, owner, repo).then(function (all) {
// remove checksums and .deb
all.releases = all.releases.filter(function (rel) {
return !/(\.txt)|(\.deb)$/i.test(rel.name);
});
return all;
});
};
if (module === require.main) {
module.exports(require('@root/request')).then(function (all) {
all = require('../_webi/normalize.js')(all);
console.info(JSON.stringify(all));
});
}

View File

@@ -15,44 +15,46 @@ $pkg_src_bin = "$Env:USERPROFILE\.local\opt\chromedriver-v$Env:WEBI_VERSION\bin"
$pkg_src_dir = "$Env:USERPROFILE\.local\opt\chromedriver-v$Env:WEBI_VERSION"
$pkg_src = "$pkg_src_cmd"
New-Item "$Env:USERPROFILE\Downloads\webi" -ItemType Directory -Force | Out-Null
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 chromedriver from $Env:WEBI_PKG_URL to $pkg_download"
IF (!(Test-Path -Path "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE"))
{
echo "Downloading chromedriver 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"
& move "$pkg_download.part" "$pkg_download"
}
if (!(Test-Path -Path "$pkg_src_cmd")) {
Write-Output "Installing chromedriver"
IF (!(Test-Path -Path "$pkg_src_cmd"))
{
echo "Installing chromedriver"
# TODO: create package-specific temp directory
# Enter tmp
Push-Location .local\tmp
pushd .local\tmp
# Remove any leftover tmp cruft
Remove-Item -Path ".\chromedriver.exe" -Recurse -ErrorAction Ignore
# Remove any leftover tmp cruft
Remove-Item -Path ".\chromedriver.exe" -Recurse -ErrorAction Ignore
# NOTE: DELETE THIS COMMENT IF NOT USED
# Move single binary into root of temporary folder
#& move "$pkg_download" "chromedriver.exe"
# NOTE: DELETE THIS COMMENT IF NOT USED
# Move single binary into root of temporary folder
#& move "$pkg_download" "chromedriver.exe"
# Unpack archive file into this temporary directory
# Windows BSD-tar handles zip. Imagine that.
Write-Output "Unpacking $pkg_download"
& tar xf "$pkg_download"
# Unpack archive file into this temporary directory
# Windows BSD-tar handles zip. Imagine that.
echo "Unpacking $pkg_download"
& tar xf "$pkg_download"
# Settle unpacked archive into place
Write-Output "Install Location: $pkg_src_cmd"
New-Item "$pkg_src_bin" -ItemType Directory -Force | Out-Null
Move-Item -Path ".\chromedriver-*\chromedriver.exe" -Destination "$pkg_src_bin"
# Settle unpacked archive into place
echo "Install Location: $pkg_src_cmd"
New-Item "$pkg_src_bin" -ItemType Directory -Force | out-null
Move-Item -Path ".\chromedriver-*\chromedriver.exe" -Destination "$pkg_src_bin"
# Exit tmp
Pop-Location
popd
}
Write-Output "Copying into '$pkg_dst_cmd' from '$pkg_src_cmd'"
Remove-Item -Path "$pkg_dst_cmd" -Recurse -ErrorAction Ignore | Out-Null
echo "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

View File

@@ -20,28 +20,20 @@ __init_chromedriver() {
# pkg_install must be defined by every package
pkg_install() {
# ~/.local/opt/chromedriver-v121.0.6130.0/bin
# ~/.local/opt/chromedriver-v88.0.4324.96/bin
mkdir -p "$(dirname "$pkg_src_cmd")"
# mv ./chromedriver-macos-arm64/chromedriver \
# ~/.local/opt/chromedriver-v121.0.6130.0/bin/chromedriver
mv ./chromedriver-*/chromedriver "$pkg_src_cmd"
echo ""
echo " $(t_warn 'MANUAL STEPS TO FINISH:') you may need to install libnss3:"
echo " $(t_cmd 'sudo apt install -y') $(t_warn 'libnss3')"
# mv ./chromedriver-*/chromedriver ~/.local/opt/chromedriver-v88.0.4324.96/bin/chromedriver
mv ./chromedriver* "$pkg_src_cmd"
}
# pkg_get_current_version is recommended, but (soon) not required
pkg_get_current_version() {
# 'chromedriver --version' has output in this format:
# ChromeDriver 121.0.6130.0 (5fc19c7ab5e88bb674c2efc65db4b7890a52a4ec-refs/branch-heads/6130@{#1})
# ChromeDriver 88.0.4324.96 (68dba2d8a0b149a1d3afac56fa74648032bcf46b-refs/branch-heads/4324@{#1784})
# This trims it down to just the version number:
# 121.0.6130.0
chromedriver --version 2> /dev/null |
head -n 1 |
tr -s ' ' |
cut -d' ' -f2
# 88.0.4324.96
chromedriver --version 2> /dev/null | head -n 1 | cut -d ' ' -f 2
}
}

View File

@@ -1 +0,0 @@
source = chromedist

88
chromedriver/releases.js Normal file
View File

@@ -0,0 +1,88 @@
'use strict';
var matchers = {
key: /.*Key>(.*)<\/Key.*/,
generation: /.*Generation>(.*)<\/Generation.*/,
metaGeneration: /.*MetaGeneration>(.*)<\/MetaGeneration.*/,
lastModified: /.*LastModified>(.*)<\/LastModified.*/,
etag: /.*ETag>(.*)<\/ETag.*/,
size: /.*Size>(.*)<\/Size.*/,
};
var baseUrl = 'https://chromedriver.storage.googleapis.com';
module.exports = function (request) {
var all = {
download: '',
releases: [],
};
// XML
return request({
url: 'https://chromedriver.storage.googleapis.com/',
json: false,
})
.then(function (resp) {
var body = resp.body;
var groups = body.split(/<\/?Contents>/g);
// get rid of leading and trailing junk
groups.shift();
groups.pop();
var metas = groups.map(function (group) {
return {
key: group.replace(matchers.key, '$1'),
//generation: group.replace(matchers.generation, '$1'),
//metaGeneration: group.replace(matchers.metaGeneration, '$1'),
lastModified: group.replace(matchers.lastModified, '$1'),
//etag: group.replace(matchers.etag, '$1'),
//size: group.replace(matchers.size, '$1')
};
});
all.download = baseUrl + '/{{ download }}';
metas.forEach(function (asset) {
if (!asset.key.includes('chromedriver')) {
// skip the indexes, images, etc
return null;
}
var osname = asset.key.replace(/.*(win|mac|linux)/, '$1');
var arch;
if (asset.key.includes('linux')) {
osname = 'linux';
} else if (asset.key.includes('mac64')) {
osname = 'macos';
if (asset.key.includes('_m1.')) {
arch = 'arm64';
}
} else if (asset.key.includes('win')) {
osname = 'windows';
arch = 'amd64';
}
all.releases.push({
// 87.0.4280.88/chromedriver_win32.zip => 87.0.4280.88
version: asset.key.replace(/(.*)\/.*/, '$1'),
lts: false,
channel: 'stable',
date: asset.lastModified.replace(/T.*/, '$1'),
os: osname,
arch: arch,
hash: '-', // not sure about including etag as hash yet
download: asset.key,
});
});
})
.then(function () {
all.releases.sort(function (a, b) {
return new Date(b.date).valueOf() - new Date(a.date).valueOf();
});
return all;
});
};
if (module === require.main) {
module.exports(require('@root/request')).then(function (all) {
all = require('../_webi/normalize.js')(all);
// just select the latest 5 for demonstration
all.releases = all.releases.slice(-20);
console.info(JSON.stringify(all, null, 2));
});
}

View File

@@ -1,47 +0,0 @@
---
title: cilium
homepage: https://github.com/cilium/cilium-cli
tagline: |
cilium: manage & troubleshoot Kubernetes clusters running Cilium
---
To update or switch versions, run `webi cilium@stable` (or `@v2`, `@beta`,etc).
### Files
These are the files / directories that are created and/or modified with this
install:
```text
~/.config/envman/PATH.env
~/.local/bin/cilium
~/.local/opt/cilium/
```
## Cheat Sheet
> Cilium is an open source, cloud native solution for providing, securing, and
> observing network connectivity between workloads, fueled by the revolutionary
> Kernel technology eBPF.
Quick Start User Guide:
<https://docs.cilium.io/en/stable/gettingstarted/k8s-install-default/#k8s-install-quick>
To install the default version of the Cilium image:
```sh
cilium install
```
To upgrade to a specific version of the Cilium image:
```sh
cilium upgrade --version v1.15.3
```
To check the status of the current Cilium deployment:
```sh
cilium status
```

View File

@@ -1,45 +0,0 @@
#!/usr/bin/env pwsh
##################
# Install cilium #
##################
$pkg_cmd_name = "cilium"
$pkg_dst_cmd = "$Env:USERPROFILE\.local\bin\cilium.exe"
$pkg_dst = "$pkg_dst_cmd"
$pkg_src_cmd = "$Env:USERPROFILE\.local\opt\cilium-v$Env:WEBI_VERSION\bin\cilium.exe"
$pkg_src_bin = "$Env:USERPROFILE\.local\opt\cilium-v$Env:WEBI_VERSION\bin"
$pkg_src_dir = "$Env:USERPROFILE\.local\opt\cilium-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 cilium 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 cilium"
Push-Location .local\tmp
Remove-Item -Path ".\cilium-v*" -Recurse -ErrorAction Ignore
Remove-Item -Path ".\cilium.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
Move-Item -Path ".\cilium-*\cilium.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
Copy-Item -Path "$pkg_src" -Destination "$pkg_dst" -Recurse

View File

@@ -1,39 +0,0 @@
#!/bin/sh
__init_cilium() {
set -e
set -u
##################
# Install cilium #
##################
pkg_cmd_name="cilium"
pkg_dst_cmd="$HOME/.local/bin/cilium"
pkg_dst="$pkg_dst_cmd"
pkg_src_cmd="$HOME/.local/opt/cilium-v$WEBI_VERSION/bin/cilium"
pkg_src_dir="$HOME/.local/opt/cilium-v$WEBI_VERSION"
pkg_src="$pkg_src_cmd"
WEBI_SINGLE=true
# pkg_install must be defined by every package
pkg_install() {
# ~/.local/opt/cilium-v0.16.16/bin
mkdir -p "$(dirname "${pkg_src_cmd}")"
# mv ./hugo ~/.local/opt/cilium-v0.16.16/bin/
mv ./cilium "${pkg_src_cmd}"
}
pkg_get_current_version() {
cilium version 2> /dev/null |
head -n 1 |
cut -d ' ' -f 2
}
}
__init_cilium

View File

@@ -1 +0,0 @@
github_releases = cilium/cilium-cli

Some files were not shown because too many files have changed in this diff Show More