From 5034ea1d71d969d2615c40c4603f35413ba4ceef Mon Sep 17 00:00:00 2001 From: Jason Swank <632526+jswank@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:41:05 -0500 Subject: [PATCH 1/2] prep for initial release --- LICENSE | 21 ++ README.md | 88 +++++ Taskfile.yaml | 68 +++- config/aichat.binstaller.yml | 25 ++ config/binstaller.binstaller.yml | 39 ++ config/eksctl.binstaller.yml | 36 ++ config/gh-md-toc.binstaller.yml | 30 ++ config/opentofu.binstaller.yml | 39 ++ config/task.binstaller.yml | 45 +++ config/toolhive.binstaller.yml | 30 ++ config/zk.binstaller.yml | 13 + scripts/aichat-install.sh | 620 ++++++++++++++++++++++++++++++ scripts/binstaller-install.sh | 622 +++++++++++++++++++++++++++++++ scripts/claude-install.sh | 5 + scripts/eksctl-install.sh | 618 ++++++++++++++++++++++++++++++ scripts/gh-md-toc-install.sh | 604 ++++++++++++++++++++++++++++++ scripts/opentofu-install.sh | 610 ++++++++++++++++++++++++++++++ scripts/task-install.sh | 615 ++++++++++++++++++++++++++++++ scripts/toolhive-install.sh | 606 ++++++++++++++++++++++++++++++ scripts/zk-install.sh | 604 ++++++++++++++++++++++++++++++ 20 files changed, 5335 insertions(+), 3 deletions(-) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config/aichat.binstaller.yml create mode 100644 config/binstaller.binstaller.yml create mode 100644 config/eksctl.binstaller.yml create mode 100644 config/gh-md-toc.binstaller.yml create mode 100644 config/opentofu.binstaller.yml create mode 100644 config/task.binstaller.yml create mode 100644 config/toolhive.binstaller.yml create mode 100644 config/zk.binstaller.yml create mode 100755 scripts/aichat-install.sh create mode 100755 scripts/binstaller-install.sh create mode 100755 scripts/claude-install.sh create mode 100755 scripts/eksctl-install.sh create mode 100755 scripts/gh-md-toc-install.sh create mode 100755 scripts/opentofu-install.sh create mode 100755 scripts/task-install.sh create mode 100755 scripts/toolhive-install.sh create mode 100755 scripts/zk-install.sh diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..13139dd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Jason Swank + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..038425f --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# binst + +A collection of installation scripts for binary tools, generated using [binstaller](https://github.com/binary-install/binstaller). These scripts are portable POSIX shell scripts that work across Linux, macOS, and Windows (Git Bash/WSL) and are designed to be a simple, secure way to install self-contained binaries from GitHub releases. + +## Quick Start + +Install any binary using its generated script: + +```bash +# install trufflehog +$ scripts/trufflehog-install.sh + +# install task +$ scripts/task-install.sh +``` + +## Workflows + +Common workflows for the creation, maintantenance, usage of the installation scripts are encapsulated in [Task](https://taskfile.dev) tasks. + +``` +# list availale tasks +$ task --list + +task: Available tasks for this project: +* default: Create a new installation script for a binary +* embed-checksums: Embed checksums into a binst configuration file. +* gen: Generate installation script from binst configuration. +* init: Initialize binst configuration for a GitHub project. +* install-*: Install a binary using its installation script. +* latest-release: Determine the latest release available for a given repo. + +# install a binary +$ task install-trufflehog +``` + +### Creating New Installation Scripts + +The default task creates a new installation script for a binary from its GitHub +repository. This is often the only step needed to add a new installation script +to the collection. + +``` +# create a new installation script by providing the GitHub repository as an argument: +$ task REPO=owner/repo-name + +# summarize the default task +$ task --summary +task: default + +Create a new installation script for a binary by initializing a binst config, +embedding checksums, and generating the installation script. The latest release, as +determined by the latest-release task, will be used unless a specific version is +provided. + +Invoke this task like: task default REPO=trufflesecurity/trufflehog + +vars: + CONFIG_DIR: "./config" + SCRIPT_DIR: "./scripts" + BINARY: "{{.REPO | base}}" + VERSION: "latest" + +requires: + vars: + - REPO + +commands: + - Task: init + - Task: embed-checksums + - Task: gen +``` + +## Notes + +### Binary Names + +Sometimes the binary executable name differs from the GitHub repository name. When this occurs, the `asset.binaries` section in the configuration file defines the actual binary name. If this is the case, the generated installation script will need to be manually edited. + +As an example, the released artifact for [stacklok/toolhive](https://github.com/stacklok/toolhive) is named `thv` rather than `toolhive`. The [configuration file](config/toolhive.binstaller.yml) specifies the correct name for the binary in the `asset.binaries` section. + +```yaml +repo: stacklok/toolhive +asset: +binaries: + - name: thv + path: thv +``` diff --git a/Taskfile.yaml b/Taskfile.yaml index 08e8c02..c13b50f 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -6,6 +6,34 @@ vars: SCRIPT_DIR: ./scripts tasks: + + default: + desc: Create a new installation script for a binary + summary: | + Create a new installation script for a binary by initializing a binst config, + embedding checksums, and generating the installation script. The latest release, as + determined by the latest-release task, will be used unless a specific version is + provided. + + Invoke this task like: task default REPO=trufflesecurity/trufflehog + vars: + BINARY: "{{.REPO | base}}" + VERSION: '{{.VERSION | default "latest"}}' + cmds: + - task: init + vars: + REPO: "{{.REPO}}" + - task: embed-checksums + vars: + BINARY: "{{.BINARY}}" + # use 'latest' as version unless VERSION is explicitly provided + VERSION: "{{.VERSION }}" + - task: gen + vars: + BINARY: "{{.BINARY}}" + requires: + vars: ["REPO"] + init: desc: Initialize binst configuration for a GitHub project. summary: | @@ -26,17 +54,40 @@ tasks: requires: vars: ["REPO"] + latest-release: + desc: Determine the latest release available for a given repo. + summary: | + Determine the latest release available for a given GitHub repository. + + Invoke this task like: + task latest-release REPO=trufflesecurity/trufflehog + cmd: | + gh release view -R {{.REPO}} --json tagName --jq .tagName + requires: + vars: ["REPO"] + embed-checksums: desc: Embed checksums into a binst configuration file. summary: | Embed checksums into a binst configuration file for a specific version. - + + If VERSION is not specified, the latest release ("latest") will be used. + Invoke this task like: task embed-checksums BINARY=trufflehog VERSION=v3.92.4 + + If --mode download fails, fallbac to --mode calculate cmd: | - binst embed-checksums --config {{.CONFIG_DIR}}/{{.BINARY}}.binstaller.yml --version {{.VERSION}} --mode download + set +o errexit + binst embed-checksums --config {{.CONFIG_DIR}}/{{.BINARY}}.binstaller.yml --version {{.VERSION}} --mode download + if [ $? -ne 0 ]; then + binst embed-checksums --config {{.CONFIG_DIR}}/{{.BINARY}}.binstaller.yml --version {{.VERSION}} --mode calculate + fi + + vars: + VERSION: "latest" requires: - vars: ["BINARY", "VERSION"] + vars: ["BINARY"] gen: desc: Generate installation script from binst configuration. @@ -52,3 +103,14 @@ tasks: requires: vars: ["BINARY"] + install-*: + desc: Install a binary using its installation script. + summary: | + Install a binary using its installation script located in the scripts directory. + + Invoke this task like: + task install-trufflehog + cmd: | + bash {{.SCRIPT_DIR}}/{{.BINARY}}-install.sh + vars: + BINARY: "{{index .MATCH 0}}" diff --git a/config/aichat.binstaller.yml b/config/aichat.binstaller.yml new file mode 100644 index 0000000..795dda8 --- /dev/null +++ b/config/aichat.binstaller.yml @@ -0,0 +1,25 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/binary-install/binstaller/main/schema/InstallSpec.json +schema: v1 +repo: sigoden/aichat +asset: + template: aichat-${TAG}-${ARCH}-${OS}${EXT} + default_extension: .tar.gz + rules: + - when: + arch: amd64 + arch: x86_64 + - when: + arch: arm64 + arch: aarch64 + - when: + os: darwin + os: apple-darwin + - when: + os: linux + os: unknown-linux-musl + - when: + os: windows + os: pc-windows-msvc + - when: + os: windows + ext: .zip diff --git a/config/binstaller.binstaller.yml b/config/binstaller.binstaller.yml new file mode 100644 index 0000000..c3d193c --- /dev/null +++ b/config/binstaller.binstaller.yml @@ -0,0 +1,39 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/binary-install/binstaller/main/schema/InstallSpec.json +schema: v1 +repo: binary-install/binstaller +asset: + template: binst_${OS}_${ARCH}${EXT} + default_extension: .tar.gz + rules: + - when: + arch: amd64 + arch: x86_64 + - when: + os: darwin + os: Darwin + - when: + os: linux + os: Linux + - when: + os: windows + os: Windows + - when: + os: windows + ext: .zip +checksums: + algorithm: sha256 + template: checksums.txt + embedded_checksums: + v0.12.0: + - filename: binst_Darwin_arm64.tar.gz + hash: 2c3a8cf9b8f95edb5d97b905ccb0204800b25f1a9bc2b4ad704596e756e41eb9 + - filename: binst_Darwin_x86_64.tar.gz + hash: bd9eae149f5735671bf1f32204f5ba9f10de269a290755f21f93bf1bb5d09b03 + - filename: binst_Linux_arm64.tar.gz + hash: 76bca4884ee3fa229d915473ddd53ab4d04c34c9e4b5a1eda36d9751763af916 + - filename: binst_Linux_x86_64.tar.gz + hash: af36ebb92c01ad4b9867a79eb45d1a130efbce20930896ec7578b3746416ee41 + - filename: binst_Windows_arm64.zip + hash: 133784089efd57f5a0164b84ac74c4924c299342c88eb40be65155193ca37761 + - filename: binst_Windows_x86_64.zip + hash: afcc9eab78de6bfa1ce360aebbf1a48738c519cdbe6a9560e6979ec3a4fcb4ef diff --git a/config/eksctl.binstaller.yml b/config/eksctl.binstaller.yml new file mode 100644 index 0000000..5d5f2eb --- /dev/null +++ b/config/eksctl.binstaller.yml @@ -0,0 +1,36 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/binary-install/binstaller/main/schema/InstallSpec.json +schema: v1 +repo: eksctl-io/eksctl +asset: + template: eksctl_${OS}_${ARCH}${EXT} + default_extension: .tar.gz + rules: + - when: + os: darwin + os: Darwin + - when: + os: linux + os: Linux + - when: + os: windows + os: Windows + - when: + os: windows + ext: .zip +checksums: + algorithm: sha256 + template: eksctl_checksums.txt + embedded_checksums: + v0.221.0: + - filename: eksctl_Darwin_amd64.tar.gz + hash: 9a5a3370ed79ffa646e25a0115aa7ee7fa203b284dffe058135b353be1542752 + - filename: eksctl_Darwin_arm64.tar.gz + hash: 80a5f4ec7c58aad8884665318f448e88f1861a9e0eb2a467e0b879161d90bcc2 + - filename: eksctl_Linux_amd64.tar.gz + hash: 9c04d16e5a0be350f9c383088ed99eac2cf1af830d25360c34588854e9e06cda + - filename: eksctl_Linux_arm64.tar.gz + hash: 3c8cfe3954c1a88a49d7f09cc03f69dceab689b1d396b9d0bcba80228d12978e + - filename: eksctl_Windows_amd64.zip + hash: c247283dc064ac55e44222191d71800f189a16b438b42cb520fb1489d1b80513 + - filename: eksctl_Windows_arm64.zip + hash: f1dbc8383281c0c318e4ee99eb688438f1d7a19dbbe2b2a03e93863740dc0f73 diff --git a/config/gh-md-toc.binstaller.yml b/config/gh-md-toc.binstaller.yml new file mode 100644 index 0000000..124763b --- /dev/null +++ b/config/gh-md-toc.binstaller.yml @@ -0,0 +1,30 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/binary-install/binstaller/main/schema/InstallSpec.json +schema: v1 +repo: ekalinin/github-markdown-toc.go +asset: + template: gh-md-toc_${VERSION}_${OS}_${ARCH}${EXT} + default_extension: .tar.gz + binaries: + - name: gh-md-toc + path: gh-md-toc +checksums: + algorithm: sha256 + template: checksums.txt + embedded_checksums: + v2.0.0: + - filename: gh-md-toc_2.0.0_darwin_amd64.tar.gz + hash: 622626f7880f6b604e93c06638e948500f4b458703d4066e91bae5b857934666 + - filename: gh-md-toc_2.0.0_darwin_arm64.tar.gz + hash: 6a610cfe0e30e46ba49027da8eef3869fd2b98be9403b9d231df8c619bb59659 + - filename: gh-md-toc_2.0.0_linux_amd64.tar.gz + hash: 7546a6e1146ba53d23a657cd6e93f2ed6ed0cc06bcf882c8f2983116f758adf5 + - filename: gh-md-toc_2.0.0_linux_arm64.tar.gz + hash: 30487683256f1a0a923433b97730b74110552e857506e3fee5cc2d11ee7366ed + - filename: gh-md-toc_2.0.0_linux_armv6.tar.gz + hash: c715d4432e95dda4a06232c9b3c01e945e07ec00a11355b5f1d4804f36794bcb + - filename: gh-md-toc_2.0.0_windows_amd64.tar.gz + hash: d06b047b3314c875034f8e29f07a1c8498e3bfaab3b01cbfa06f7d37e013d222 + - filename: gh-md-toc_2.0.0_windows_arm64.tar.gz + hash: 22d045c736b3fedc90e42467c92dbb43bef3d8a625f05587070f6ca0b2d4f1d1 + - filename: gh-md-toc_2.0.0_windows_armv6.tar.gz + hash: 24c85ede2c0d897993174d5df9905ee6d9f78d95a0f5d36a1324ab55e0dac735 diff --git a/config/opentofu.binstaller.yml b/config/opentofu.binstaller.yml new file mode 100644 index 0000000..fea7baf --- /dev/null +++ b/config/opentofu.binstaller.yml @@ -0,0 +1,39 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/binary-install/binstaller/main/schema/InstallSpec.json +schema: v1 +repo: opentofu/opentofu +asset: + template: tofu_${VERSION}_${OS}_${ARCH}${EXT} + default_extension: .tar.gz +checksums: + algorithm: sha256 + template: tofu_${VERSION}_SHA256SUMS + embedded_checksums: + v1.11.3: + - filename: tofu_1.11.3_darwin_amd64.tar.gz + hash: c895c837af131f91ec6a493e5ab29e702bf38500ebccb5c3bf31e2ae823ab5c4 + - filename: tofu_1.11.3_darwin_arm64.tar.gz + hash: ed13cc2c919a0e3532eeb71fd5cd275d44d9552d8e57f18e5867917149146490 + - filename: tofu_1.11.3_freebsd_386.tar.gz + hash: 9e94e9d4c1c0c6371069e55e7d85b9b80e31fb7651bdf6d86d6f6fd3c33d078e + - filename: tofu_1.11.3_freebsd_amd64.tar.gz + hash: 8ce117bd91d28608369e7f057b7ea10c638bef1625d55eb8bbc6d0ab1a57382d + - filename: tofu_1.11.3_freebsd_arm.tar.gz + hash: 6d60c289445d11f51bff7a828a8eaa3eeed6db1209e88eadd436c49852932775 + - filename: tofu_1.11.3_linux_386.tar.gz + hash: 15f5721d928af1c53a11a2cbcca700a16c3d81ddd0b1b6d205d662d3d8f6b320 + - filename: tofu_1.11.3_linux_amd64.tar.gz + hash: 46b567512d4cfa631551b936f3616c02277954fece29d91a52d264537399ad03 + - filename: tofu_1.11.3_linux_arm.tar.gz + hash: f670ddb67a1c187735bfe4d9e4642a1f35903e3f3548a5604fe15c0c4f3b0fb8 + - filename: tofu_1.11.3_linux_arm64.tar.gz + hash: abb19fdbb4516a30f303f0686cd156f2ff768b34c8d1e433ce5e6f1d81399425 + - filename: tofu_1.11.3_openbsd_386.tar.gz + hash: e2b74b05a5e3faf5f957361382b47129a411840b68be07cce216b6ce723af7aa + - filename: tofu_1.11.3_openbsd_amd64.tar.gz + hash: e4e21e05db28441c41d94267495d65ae098f791df365c6fcd2924e65d0f946d2 + - filename: tofu_1.11.3_solaris_amd64.tar.gz + hash: a4a88ee5bdbd0afa47488d8dabc463733a3efd5a28be57199ead6b04fdd5bd9b + - filename: tofu_1.11.3_windows_386.tar.gz + hash: dad3090337bebc23bbb0d645c86a36316827d743ae26b5ecc925c33d2d454868 + - filename: tofu_1.11.3_windows_amd64.tar.gz + hash: 5e97b41ee5d9387f1e4532b6e2de6ae244e93f70c9d48fb46ec606c7cacb7a07 diff --git a/config/task.binstaller.yml b/config/task.binstaller.yml new file mode 100644 index 0000000..e7674a5 --- /dev/null +++ b/config/task.binstaller.yml @@ -0,0 +1,45 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/binary-install/binstaller/main/schema/InstallSpec.json +schema: v1 +repo: go-task/task +asset: + template: task_${OS}_${ARCH}${EXT} + default_extension: .tar.gz + rules: + - when: + os: windows + ext: .zip +checksums: + algorithm: sha256 + template: task_checksums.txt + embedded_checksums: + v3.46.4: + - filename: task_darwin_amd64.tar.gz + hash: 6e3d1bf5fcc03e50b2c02c92335923b715d3f61e27fef5b3048b59ed97a18721 + - filename: task_darwin_arm64.tar.gz + hash: ffe2b4a3c54467172ab15734d0b451b1fc0472d0d48bb3538870b433c700bc96 + - filename: task_freebsd_386.tar.gz + hash: 37efbab3e10723fbba85114ffb5cd39938ed489b69674e15605ec96989c047b5 + - filename: task_freebsd_amd64.tar.gz + hash: cd0be727b17943a53004c9984526649010aa7710cd4fac4a84b805ae25f1f35c + - filename: task_freebsd_arm.tar.gz + hash: f267218e5ae49f7bbbe41c94377b3c0694c79411b9180fccc21b43cc8ef45284 + - filename: task_freebsd_arm64.tar.gz + hash: e68251346bc140f367f42a34fdc327edb63612a66bd782cd713a1c812531a13c + - filename: task_linux_386.tar.gz + hash: b0e93c6290ed91af882516117114999ff0b21ac08d8c4e7e6f63ed6a12ef95b5 + - filename: task_linux_amd64.tar.gz + hash: 4b0098862292de03d568c851b207bce5fdb8a59cb533d6ed4132386151926c46 + - filename: task_linux_arm.tar.gz + hash: f08d96178a6612e6c548e103c022740c82a313d04f7f90421d7550356ac8ee02 + - filename: task_linux_arm64.tar.gz + hash: 9079bbdfd439f2168ee380d9167fd77f13f9fab7afb516205ac3a578f16bcb59 + - filename: task_linux_riscv64.tar.gz + hash: c05fb7d47c0c5d1b8222362bf2edaae6cbe6d33c3b38f0ec034306425ee51761 + - filename: task_windows_386.zip + hash: 1398d3b96f5b7dfb71b2a126dbb5849369da130099783ebd899a162ff091f6a3 + - filename: task_windows_amd64.zip + hash: 790420c11a6457a27f135c292371d6a352436dfce7dba5b5ffa8ae399ecfe04b + - filename: task_windows_arm.zip + hash: 450bf906119ddf741e99c44569329b9493c4b6f55f4c3d736297b3d57826fb32 + - filename: task_windows_arm64.zip + hash: 4ef119dfc6ed5133170f6fc20085f0602701a37c90b440ee54331b6370f2914d diff --git a/config/toolhive.binstaller.yml b/config/toolhive.binstaller.yml new file mode 100644 index 0000000..dbb09f8 --- /dev/null +++ b/config/toolhive.binstaller.yml @@ -0,0 +1,30 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/binary-install/binstaller/main/schema/InstallSpec.json +schema: v1 +repo: stacklok/toolhive +asset: + template: toolhive_${VERSION}_${OS}_${ARCH}${EXT} + default_extension: .tar.gz + binaries: + - name: thv + path: thv + rules: + - when: + os: windows + ext: .zip +checksums: + algorithm: sha256 + template: toolhive_${VERSION}_checksums.txt + embedded_checksums: + v0.7.2: + - filename: toolhive_0.7.2_darwin_amd64.tar.gz + hash: a32b18485d33acefc0d69641a8f99e43c9d91ed45c6279cf158f220763840ef5 + - filename: toolhive_0.7.2_darwin_arm64.tar.gz + hash: 20b3fd98df6abdbd4898c6aa1f2fa7d01f58901055f85c374cccf110a014be1d + - filename: toolhive_0.7.2_linux_amd64.tar.gz + hash: 84a89a735f3c5d7eaa4fe0f4cba164eb2a04b83f2525c9709dc60dc86fc9dab1 + - filename: toolhive_0.7.2_linux_arm64.tar.gz + hash: 821fb80c672701b62bf3efcb4c744c685fbbde80845b6256d2116a4fe561ac21 + - filename: toolhive_0.7.2_windows_amd64.zip + hash: d6d639453938383a3399888bbe0a60449ae1a5aa196b9cd2187a1ef089b4beb6 + - filename: toolhive_0.7.2_windows_arm64.zip + hash: d291feff936764db8a02868461431bd54a840ecac7a78b303e30956609c54631 diff --git a/config/zk.binstaller.yml b/config/zk.binstaller.yml new file mode 100644 index 0000000..97cbfa0 --- /dev/null +++ b/config/zk.binstaller.yml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/binary-install/binstaller/main/schema/InstallSpec.json +schema: v1 +repo: zk-org/zk +asset: + template: zk-${TAG}-${OS}-${ARCH}${EXT} + default_extension: .tar.gz + rules: + - when: + arch: amd64 + arch: x86_64 + - when: + os: darwin + os: macos diff --git a/scripts/aichat-install.sh b/scripts/aichat-install.sh new file mode 100755 index 0000000..60acd6d --- /dev/null +++ b/scripts/aichat-install.sh @@ -0,0 +1,620 @@ +#!/bin/sh +# Code generated by binstaller. DO NOT EDIT. +# +set -e +usage() { + this=$1 + cat </dev/null +} +echoerr() { + echo "$@" 1>&2 +} +_logp=6 +log_set_priority() { + _logp="$1" +} +log_priority() { + if test -z "$1"; then + echo "$_logp" + return + fi + [ "$1" -le "$_logp" ] +} +log_tag() { + case $1 in + 0) echo "emerg" ;; + 1) echo "alert" ;; + 2) echo "crit" ;; + 3) echo "err" ;; + 4) echo "warning" ;; + 5) echo "notice" ;; + 6) echo "info" ;; + 7) echo "debug" ;; + *) echo "$1" ;; + esac +} +log_debug() { + log_priority 7 || return 0 + echoerr "$(log_prefix)" "$(log_tag 7)" "$@" +} +log_info() { + log_priority 6 || return 0 + echoerr "$(log_prefix)" "$(log_tag 6)" "$@" +} +log_err() { + log_priority 3 || return 0 + echoerr "$(log_prefix)" "$(log_tag 3)" "$@" +} +log_crit() { + log_priority 2 || return 0 + echoerr "$(log_prefix)" "$(log_tag 2)" "$@" +} +uname_os() { + os=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$os" in + msys*) os="windows" ;; + mingw*) os="windows" ;; + cygwin*) os="windows" ;; + esac + if [ "$os" = "sunos" ]; then + if [ "$(uname -o)" = "illumos" ]; then + os="illumos" + else + os="solaris" + fi + fi + echo "$os" +} +uname_arch() { + arch=$(uname -m) + case $arch in + x86_64) arch="amd64" ;; + i86pc) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; + esac + echo "${arch}" +} +uname_os_check() { + os=$(uname_os) + case "$os" in + darwin) return 0 ;; + dragonfly) return 0 ;; + freebsd) return 0 ;; + linux) return 0 ;; + android) return 0 ;; + midnightbsd) return 0 ;; + nacl) return 0 ;; + netbsd) return 0 ;; + openbsd) return 0 ;; + plan9) return 0 ;; + solaris) return 0 ;; + illumos) return 0 ;; + windows) return 0 ;; + esac + log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" + return 1 +} +uname_arch_check() { + arch=$(uname_arch) + case "$arch" in + 386) return 0 ;; + amd64) return 0 ;; + arm64) return 0 ;; + armv5) return 0 ;; + armv6) return 0 ;; + armv7) return 0 ;; + ppc64) return 0 ;; + ppc64le) return 0 ;; + mips) return 0 ;; + mipsle) return 0 ;; + mips64) return 0 ;; + mips64le) return 0 ;; + s390x) return 0 ;; + amd64p32) return 0 ;; + esac + log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" + return 1 +} +cat /dev/null </dev/null) || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command openssl; then + hash=$(openssl dgst -sha256 "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 2 + else + log_crit "hash_sha256 unable to find command to compute sha-256 hash" + return 1 + fi +} + +hash_compute() { + hash_sha256 "$1" +} + + +# shellcheck shell=sh +# Terminal progress reporting functions +progress_init() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + # OSC 9;4 sequences are safely ignored by unsupporting terminals + # Only need special handling for tmux passthrough + if [ -n "$TMUX" ]; then + # Tmux passthrough: DCS tmux; ST + # ESC characters in the wrapped sequence must be doubled + # Format: ESC P tmux; ESC ESC ] 9;4; ... ESC ESC \ ESC \ + PROGRESS_START=$(printf '\033Ptmux;\033\033]9;4;') + # shellcheck disable=SC1003 + PROGRESS_END=$(printf '\033\033\\\033\\') + else + # Direct OSC 9;4 - terminals that don't support it will safely ignore + PROGRESS_START=$(printf '\033]9;4;') + PROGRESS_END=$(printf '\007') + fi +} + +# Start pulsing progress animation +progress_pulse_start() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + + # Send OSC 9;4 with state 3 (indeterminate/pulsing) once + # The terminal will handle the continuous animation + printf "%s3%s" "$PROGRESS_START" "$PROGRESS_END" >&2 +} + +# Clear progress indicator +progress_clear() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + printf "%s0;%s" "$PROGRESS_START" "$PROGRESS_END" >&2 +} + +untar() { + tarball=$1 + strip_components=${2:-0} # default 0 + case "${tarball}" in + *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" --strip-components "${strip_components}" ;; + *.tar.xz) tar --no-same-owner -xJf "${tarball}" --strip-components "${strip_components}" ;; + *.tar.bz2) tar --no-same-owner -xjf "${tarball}" --strip-components "${strip_components}" ;; + *.tar) tar --no-same-owner -xf "${tarball}" --strip-components "${strip_components}" ;; + *.gz) gunzip "${tarball}" ;; + *.zip) + # unzip doesn't have a standard --strip-components + # Workaround: extract to a subdir and move contents up if stripping + if [ "$strip_components" -gt 0 ]; then + extract_dir=$(basename "${tarball%.zip}")_extracted + unzip -q "${tarball}" -d "${extract_dir}" + # Move contents of the *first* directory found inside extract_dir up + # This assumes wrap_in_directory=true convention + first_subdir=$(find "${extract_dir}" -mindepth 1 -maxdepth 1 -type d -print -quit) + if [ -n "$first_subdir" ]; then + # Move all contents (* includes hidden files) + mv "${first_subdir}"/* . + # Optionally remove the now-empty subdir and the extract_dir + rmdir "${first_subdir}" + rmdir "${extract_dir}" + else + log_warn "Could not find subdirectory in zip to strip components from ${extract_dir}" + # Files are extracted in current dir anyway, proceed + fi + else + unzip -q "${tarball}" + fi + ;; + *) + log_err "untar unknown archive format for ${tarball}" + return 1 + ;; + esac +} + + + +hash_verify() { + TARGET_PATH=$1 + SUMFILE=$2 + if [ -z "${SUMFILE}" ]; then + log_err "hash_verify checksum file not specified in arg2" + return 1 + fi + got=$(hash_compute "$TARGET_PATH") + if [ -z "${got}" ]; then + log_err "failed to calculate hash: ${TARGET_PATH}" + return 1 + fi + + BASENAME=${TARGET_PATH##*/} + + # Check for line matches in checksum file + # Format: " " or " *" + # Filename may include path prefix (e.g., "deployment/m2/file.tar.gz") + while IFS= read -r line || [ -n "$line" ]; do + # Normalize tabs to spaces + line=$(echo "$line" | tr '\t' ' ') + + # Remove trailing spaces for hash-only line check + line_trimmed=$(echo "$line" | sed 's/[[:space:]]*$//') + + # Check for hash-only line (no filename) - early return + if [ "$line_trimmed" = "$got" ]; then + return 0 + fi + + # Extract hash and filename parts + # First field is the hash, rest is filename (which may contain spaces) + line_hash=$(echo "$line" | cut -d' ' -f1) + + # Skip if hash doesn't match + if [ "$line_hash" != "$got" ]; then + continue + fi + + # Hash matches, now check filename + # Remove the hash part from the beginning of the line + line_rest="${line#"$got"}" + # Remove leading spaces + while [ "${line_rest#[ ]}" != "$line_rest" ]; do + line_rest="${line_rest#[ ]}" + done + + # Remove leading asterisk if present (binary mode indicator) + if [ "${line_rest#\*}" != "$line_rest" ]; then + line_rest="${line_rest#\*}" + fi + + # Extract just the filename without any path + line_filename="${line_rest##*/}" + + # Check if the filename matches + if [ "$line_filename" = "$BASENAME" ]; then + return 0 + fi + done < "$SUMFILE" + + log_err "hash_verify checksum for '$TARGET_PATH' did not verify" + log_err " Expected hash: ${got}" + log_err " Checksum file content:" + cat "$SUMFILE" >&2 + return 1 +} + +# GitHub HTTP download functions with GITHUB_TOKEN support +github_http_download_curl() { + local_file=$1 + source_url=$2 + header=$3 + if [ -n "$GITHUB_TOKEN" ]; then + log_debug "Using GITHUB_TOKEN for authentication" + if [ -z "$header" ]; then + curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -o "$local_file" "$source_url" + else + curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -H "$header" -o "$local_file" "$source_url" + fi + else + if [ -z "$header" ]; then + curl -fsSL -o "$local_file" "$source_url" + else + curl -fsSL -H "$header" -o "$local_file" "$source_url" + fi + fi +} +github_http_download_wget() { + local_file=$1 + source_url=$2 + header=$3 + if [ -n "$GITHUB_TOKEN" ]; then + log_debug "Using GITHUB_TOKEN for authentication" + if [ -z "$header" ]; then + wget -q --header "Authorization: Bearer $GITHUB_TOKEN" -O "$local_file" "$source_url" + else + wget -q --header "Authorization: Bearer $GITHUB_TOKEN" --header "$header" -O "$local_file" "$source_url" + fi + else + if [ -z "$header" ]; then + wget -q -O "$local_file" "$source_url" + else + wget -q --header "$header" -O "$local_file" "$source_url" + fi + fi +} +github_http_download() { + log_debug "github_http_download $2" + if is_command curl; then + github_http_download_curl "$@" + return + elif is_command wget; then + github_http_download_wget "$@" + return + fi + log_crit "github_http_download unable to find wget or curl" + return 1 +} +github_http_copy() { + tmp=$(mktemp) + github_http_download "${tmp}" "$@" || return 1 + body=$(cat "$tmp") + rm -f "${tmp}" + echo "$body" +} +github_release() { + owner_repo=$1 + version=$2 + test -z "$version" && version="latest" + giturl="https://github.com/${owner_repo}/releases/${version}" + json=$(github_http_copy "$giturl" "Accept:application/json") + test -z "$json" && return 1 + version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') + test -z "$version" && return 1 + echo "$version" +} + +# --- Embedded Checksums (Format: VERSION:FILENAME:HASH) --- +EMBEDDED_CHECKSUMS="" + +# Find embedded checksum for a given version and filename +find_embedded_checksum() { + version="$1" + filename="$2" + echo "$EMBEDDED_CHECKSUMS" | grep -E "^${version}:${filename}:" | cut -d':' -f3 +} +parse_args() { + BINDIR="${BINSTALLER_BIN:-${HOME}/.local/bin}" + DRY_RUN=0 + while getopts "b:dqh?xn" arg; do + case "$arg" in + b) BINDIR="$OPTARG" ;; + d) log_set_priority 10 ;; + q) log_set_priority 3 ;; + h | \?) usage "$0" ;; + x) set -x ;; + n) DRY_RUN=1 ;; + esac + done + shift $((OPTIND - 1)) + TAG="${1:-latest}" +} +tag_to_version() { + if [ "$TAG" = "latest" ]; then + log_info "checking GitHub for latest tag" + REALTAG=$(github_release "${REPO}" "${TAG}") && true + test -n "$REALTAG" || { + log_crit "Could not determine latest tag for ${REPO}" + exit 1 + } + else + # Assume TAG is a valid tag/version string + REALTAG="$TAG" + fi + if test -z "$REALTAG"; then + log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${REPO}/releases for details" + exit 1 + fi + VERSION=${REALTAG#v} # Strip leading 'v' + TAG="$REALTAG" # Use the resolved tag + log_info "Resolved version: ${VERSION} (tag: ${TAG})" +} + + + +resolve_asset_filename() { + + # --- Apply Rules --- + ASSET_FILENAME="" + if [ "${UNAME_ARCH}" = 'amd64' ] && true + then + ARCH='x86_64' + fi + if [ "${UNAME_ARCH}" = 'arm64' ] && true + then + ARCH='aarch64' + fi + if [ "${UNAME_OS}" = 'darwin' ] && true + then + OS='apple-darwin' + fi + if [ "${UNAME_OS}" = 'linux' ] && true + then + OS='unknown-linux-musl' + fi + if [ "${UNAME_OS}" = 'windows' ] && true + then + OS='pc-windows-msvc' + fi + if [ "${UNAME_OS}" = 'windows' ] && true + then + EXT='.zip' + fi + if [ -z "${ASSET_FILENAME}" ]; then + ASSET_FILENAME="aichat-${TAG}-${ARCH}-${OS}${EXT}" + fi +} +# Cleanup function to remove temporary files and stop progress +cleanup() { + # Stop progress animation + progress_clear + + if [ -n "$TMPDIR" ] && [ -d "$TMPDIR" ]; then + log_debug "Cleaning up temporary directory: $TMPDIR" + rm -rf -- "$TMPDIR" + fi +} + +execute() { + STRIP_COMPONENTS=0 + CHECKSUM_FILENAME="" + + # --- Construct URLs --- + GITHUB_DOWNLOAD="https://github.com/${REPO}/releases/download" + ASSET_URL="${GITHUB_DOWNLOAD}/${TAG}/${ASSET_FILENAME}" + CHECKSUM_URL="" + if [ -n "$CHECKSUM_FILENAME" ]; then + CHECKSUM_URL="${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM_FILENAME}" + fi + + # --- Download and Verify --- + TMPDIR=$(mktemp -d) + # Set up cleanup trap that includes progress clearing + trap cleanup EXIT HUP INT TERM + log_debug "Downloading files into ${TMPDIR}" + log_info "Downloading ${ASSET_URL}" + github_http_download "${TMPDIR}/${ASSET_FILENAME}" "${ASSET_URL}" + + # Try to find embedded checksum first + EMBEDDED_HASH=$(find_embedded_checksum "$VERSION" "$ASSET_FILENAME") + + if [ -n "$EMBEDDED_HASH" ]; then + log_info "Using embedded checksum for verification" + + # Verify using embedded hash + got=$(hash_compute "${TMPDIR}/${ASSET_FILENAME}") + if [ "$got" != "$EMBEDDED_HASH" ]; then + log_crit "Checksum verification failed for ${ASSET_FILENAME}" + log_crit "Expected: ${EMBEDDED_HASH}" + log_crit "Got: ${got}" + return 1 + fi + log_info "Checksum verification successful" + elif [ -n "$CHECKSUM_URL" ]; then + # Fall back to downloading checksum file + log_info "Downloading checksums from ${CHECKSUM_URL}" + github_http_download "${TMPDIR}/${CHECKSUM_FILENAME}" "${CHECKSUM_URL}" + log_info "Verifying checksum ..." + hash_verify "${TMPDIR}/${ASSET_FILENAME}" "${TMPDIR}/${CHECKSUM_FILENAME}" + else + log_info "No checksum found, skipping verification." + fi + + if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then + log_debug "Target is raw binary" + else + log_info "Extracting ${ASSET_FILENAME}..." + (cd "${TMPDIR}" && untar "${ASSET_FILENAME}" "${STRIP_COMPONENTS}") + fi + BINARY_NAME='aichat' + if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then + BINARY_PATH="${TMPDIR}/${ASSET_FILENAME}" + else + BINARY_PATH="${TMPDIR}/aichat" + fi + + if [ "${UNAME_OS}" = "windows" ]; then + case "${BINARY_NAME}" in *.exe) ;; *) BINARY_NAME="${BINARY_NAME}.exe" ;; esac + case "${BINARY_PATH}" in *.exe) ;; *) BINARY_PATH="${BINARY_PATH}.exe" ;; esac + fi + + if [ ! -f "${BINARY_PATH}" ]; then + log_crit "Binary not found: ${BINARY_PATH}" + log_crit "Listing contents of ${TMPDIR} ..." + if command -v find >/dev/null 2>&1; then + cd "${TMPDIR}" && find . + else + cd "${TMPDIR}" && ls -R . + fi + return 1 + fi + + progress_clear + # Install the binary + INSTALL_PATH="${BINDIR}/${BINARY_NAME}" + + if [ "$DRY_RUN" = "1" ]; then + log_info "[DRY RUN] ${BINARY_NAME} dry-run installation succeeded! (Would install to: ${INSTALL_PATH})" + else + log_info "Installing binary to ${INSTALL_PATH}" + test ! -d "${BINDIR}" && install -d "${BINDIR}" + install "${BINARY_PATH}" "${INSTALL_PATH}" + log_info "${BINARY_NAME} installation complete!" + fi +} + +# --- Configuration --- +NAME='aichat' +REPO='sigoden/aichat' +EXT='.tar.gz' + +# use in logging routines +log_prefix() { + echo "${REPO}" +} + +parse_args "$@" + +progress_init +progress_pulse_start + +# --- Determine target platform --- +OS="${BINSTALLER_OS:-$(uname_os)}" +UNAME_OS="${OS}" + +ARCH="${BINSTALLER_ARCH:-$(uname_arch)}" +UNAME_ARCH="${ARCH}" +log_info "Detected Platform: ${OS}/${ARCH}" + +# --- Validate platform --- +uname_os_check "$OS" +uname_arch_check "$ARCH" + +tag_to_version + +resolve_asset_filename +execute diff --git a/scripts/binstaller-install.sh b/scripts/binstaller-install.sh new file mode 100755 index 0000000..5f07d5b --- /dev/null +++ b/scripts/binstaller-install.sh @@ -0,0 +1,622 @@ +#!/bin/sh +# Code generated by binstaller. DO NOT EDIT. +# +set -e +usage() { + this=$1 + cat </dev/null +} +echoerr() { + echo "$@" 1>&2 +} +_logp=6 +log_set_priority() { + _logp="$1" +} +log_priority() { + if test -z "$1"; then + echo "$_logp" + return + fi + [ "$1" -le "$_logp" ] +} +log_tag() { + case $1 in + 0) echo "emerg" ;; + 1) echo "alert" ;; + 2) echo "crit" ;; + 3) echo "err" ;; + 4) echo "warning" ;; + 5) echo "notice" ;; + 6) echo "info" ;; + 7) echo "debug" ;; + *) echo "$1" ;; + esac +} +log_debug() { + log_priority 7 || return 0 + echoerr "$(log_prefix)" "$(log_tag 7)" "$@" +} +log_info() { + log_priority 6 || return 0 + echoerr "$(log_prefix)" "$(log_tag 6)" "$@" +} +log_err() { + log_priority 3 || return 0 + echoerr "$(log_prefix)" "$(log_tag 3)" "$@" +} +log_crit() { + log_priority 2 || return 0 + echoerr "$(log_prefix)" "$(log_tag 2)" "$@" +} +uname_os() { + os=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$os" in + msys*) os="windows" ;; + mingw*) os="windows" ;; + cygwin*) os="windows" ;; + esac + if [ "$os" = "sunos" ]; then + if [ "$(uname -o)" = "illumos" ]; then + os="illumos" + else + os="solaris" + fi + fi + echo "$os" +} +uname_arch() { + arch=$(uname -m) + case $arch in + x86_64) arch="amd64" ;; + i86pc) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; + esac + echo "${arch}" +} +uname_os_check() { + os=$(uname_os) + case "$os" in + darwin) return 0 ;; + dragonfly) return 0 ;; + freebsd) return 0 ;; + linux) return 0 ;; + android) return 0 ;; + midnightbsd) return 0 ;; + nacl) return 0 ;; + netbsd) return 0 ;; + openbsd) return 0 ;; + plan9) return 0 ;; + solaris) return 0 ;; + illumos) return 0 ;; + windows) return 0 ;; + esac + log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" + return 1 +} +uname_arch_check() { + arch=$(uname_arch) + case "$arch" in + 386) return 0 ;; + amd64) return 0 ;; + arm64) return 0 ;; + armv5) return 0 ;; + armv6) return 0 ;; + armv7) return 0 ;; + ppc64) return 0 ;; + ppc64le) return 0 ;; + mips) return 0 ;; + mipsle) return 0 ;; + mips64) return 0 ;; + mips64le) return 0 ;; + s390x) return 0 ;; + amd64p32) return 0 ;; + esac + log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" + return 1 +} +cat /dev/null </dev/null) || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command openssl; then + hash=$(openssl dgst -sha256 "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 2 + else + log_crit "hash_sha256 unable to find command to compute sha-256 hash" + return 1 + fi +} + +hash_compute() { + hash_sha256 "$1" +} + + +# shellcheck shell=sh +# Terminal progress reporting functions +progress_init() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + # OSC 9;4 sequences are safely ignored by unsupporting terminals + # Only need special handling for tmux passthrough + if [ -n "$TMUX" ]; then + # Tmux passthrough: DCS tmux; ST + # ESC characters in the wrapped sequence must be doubled + # Format: ESC P tmux; ESC ESC ] 9;4; ... ESC ESC \ ESC \ + PROGRESS_START=$(printf '\033Ptmux;\033\033]9;4;') + # shellcheck disable=SC1003 + PROGRESS_END=$(printf '\033\033\\\033\\') + else + # Direct OSC 9;4 - terminals that don't support it will safely ignore + PROGRESS_START=$(printf '\033]9;4;') + PROGRESS_END=$(printf '\007') + fi +} + +# Start pulsing progress animation +progress_pulse_start() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + + # Send OSC 9;4 with state 3 (indeterminate/pulsing) once + # The terminal will handle the continuous animation + printf "%s3%s" "$PROGRESS_START" "$PROGRESS_END" >&2 +} + +# Clear progress indicator +progress_clear() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + printf "%s0;%s" "$PROGRESS_START" "$PROGRESS_END" >&2 +} + +untar() { + tarball=$1 + strip_components=${2:-0} # default 0 + case "${tarball}" in + *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" --strip-components "${strip_components}" ;; + *.tar.xz) tar --no-same-owner -xJf "${tarball}" --strip-components "${strip_components}" ;; + *.tar.bz2) tar --no-same-owner -xjf "${tarball}" --strip-components "${strip_components}" ;; + *.tar) tar --no-same-owner -xf "${tarball}" --strip-components "${strip_components}" ;; + *.gz) gunzip "${tarball}" ;; + *.zip) + # unzip doesn't have a standard --strip-components + # Workaround: extract to a subdir and move contents up if stripping + if [ "$strip_components" -gt 0 ]; then + extract_dir=$(basename "${tarball%.zip}")_extracted + unzip -q "${tarball}" -d "${extract_dir}" + # Move contents of the *first* directory found inside extract_dir up + # This assumes wrap_in_directory=true convention + first_subdir=$(find "${extract_dir}" -mindepth 1 -maxdepth 1 -type d -print -quit) + if [ -n "$first_subdir" ]; then + # Move all contents (* includes hidden files) + mv "${first_subdir}"/* . + # Optionally remove the now-empty subdir and the extract_dir + rmdir "${first_subdir}" + rmdir "${extract_dir}" + else + log_warn "Could not find subdirectory in zip to strip components from ${extract_dir}" + # Files are extracted in current dir anyway, proceed + fi + else + unzip -q "${tarball}" + fi + ;; + *) + log_err "untar unknown archive format for ${tarball}" + return 1 + ;; + esac +} + + + +hash_verify() { + TARGET_PATH=$1 + SUMFILE=$2 + if [ -z "${SUMFILE}" ]; then + log_err "hash_verify checksum file not specified in arg2" + return 1 + fi + got=$(hash_compute "$TARGET_PATH") + if [ -z "${got}" ]; then + log_err "failed to calculate hash: ${TARGET_PATH}" + return 1 + fi + + BASENAME=${TARGET_PATH##*/} + + # Check for line matches in checksum file + # Format: " " or " *" + # Filename may include path prefix (e.g., "deployment/m2/file.tar.gz") + while IFS= read -r line || [ -n "$line" ]; do + # Normalize tabs to spaces + line=$(echo "$line" | tr '\t' ' ') + + # Remove trailing spaces for hash-only line check + line_trimmed=$(echo "$line" | sed 's/[[:space:]]*$//') + + # Check for hash-only line (no filename) - early return + if [ "$line_trimmed" = "$got" ]; then + return 0 + fi + + # Extract hash and filename parts + # First field is the hash, rest is filename (which may contain spaces) + line_hash=$(echo "$line" | cut -d' ' -f1) + + # Skip if hash doesn't match + if [ "$line_hash" != "$got" ]; then + continue + fi + + # Hash matches, now check filename + # Remove the hash part from the beginning of the line + line_rest="${line#"$got"}" + # Remove leading spaces + while [ "${line_rest#[ ]}" != "$line_rest" ]; do + line_rest="${line_rest#[ ]}" + done + + # Remove leading asterisk if present (binary mode indicator) + if [ "${line_rest#\*}" != "$line_rest" ]; then + line_rest="${line_rest#\*}" + fi + + # Extract just the filename without any path + line_filename="${line_rest##*/}" + + # Check if the filename matches + if [ "$line_filename" = "$BASENAME" ]; then + return 0 + fi + done < "$SUMFILE" + + log_err "hash_verify checksum for '$TARGET_PATH' did not verify" + log_err " Expected hash: ${got}" + log_err " Checksum file content:" + cat "$SUMFILE" >&2 + return 1 +} + +# GitHub HTTP download functions with GITHUB_TOKEN support +github_http_download_curl() { + local_file=$1 + source_url=$2 + header=$3 + if [ -n "$GITHUB_TOKEN" ]; then + log_debug "Using GITHUB_TOKEN for authentication" + if [ -z "$header" ]; then + curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -o "$local_file" "$source_url" + else + curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -H "$header" -o "$local_file" "$source_url" + fi + else + if [ -z "$header" ]; then + curl -fsSL -o "$local_file" "$source_url" + else + curl -fsSL -H "$header" -o "$local_file" "$source_url" + fi + fi +} +github_http_download_wget() { + local_file=$1 + source_url=$2 + header=$3 + if [ -n "$GITHUB_TOKEN" ]; then + log_debug "Using GITHUB_TOKEN for authentication" + if [ -z "$header" ]; then + wget -q --header "Authorization: Bearer $GITHUB_TOKEN" -O "$local_file" "$source_url" + else + wget -q --header "Authorization: Bearer $GITHUB_TOKEN" --header "$header" -O "$local_file" "$source_url" + fi + else + if [ -z "$header" ]; then + wget -q -O "$local_file" "$source_url" + else + wget -q --header "$header" -O "$local_file" "$source_url" + fi + fi +} +github_http_download() { + log_debug "github_http_download $2" + if is_command curl; then + github_http_download_curl "$@" + return + elif is_command wget; then + github_http_download_wget "$@" + return + fi + log_crit "github_http_download unable to find wget or curl" + return 1 +} +github_http_copy() { + tmp=$(mktemp) + github_http_download "${tmp}" "$@" || return 1 + body=$(cat "$tmp") + rm -f "${tmp}" + echo "$body" +} +github_release() { + owner_repo=$1 + version=$2 + test -z "$version" && version="latest" + giturl="https://github.com/${owner_repo}/releases/${version}" + json=$(github_http_copy "$giturl" "Accept:application/json") + test -z "$json" && return 1 + version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') + test -z "$version" && return 1 + echo "$version" +} + +# --- Embedded Checksums (Format: VERSION:FILENAME:HASH) --- +EMBEDDED_CHECKSUMS=" +0.12.0:binst_Darwin_arm64.tar.gz:2c3a8cf9b8f95edb5d97b905ccb0204800b25f1a9bc2b4ad704596e756e41eb9 +0.12.0:binst_Darwin_x86_64.tar.gz:bd9eae149f5735671bf1f32204f5ba9f10de269a290755f21f93bf1bb5d09b03 +0.12.0:binst_Linux_arm64.tar.gz:76bca4884ee3fa229d915473ddd53ab4d04c34c9e4b5a1eda36d9751763af916 +0.12.0:binst_Linux_x86_64.tar.gz:af36ebb92c01ad4b9867a79eb45d1a130efbce20930896ec7578b3746416ee41 +0.12.0:binst_Windows_arm64.zip:133784089efd57f5a0164b84ac74c4924c299342c88eb40be65155193ca37761 +0.12.0:binst_Windows_x86_64.zip:afcc9eab78de6bfa1ce360aebbf1a48738c519cdbe6a9560e6979ec3a4fcb4ef" + +# Find embedded checksum for a given version and filename +find_embedded_checksum() { + version="$1" + filename="$2" + echo "$EMBEDDED_CHECKSUMS" | grep -E "^${version}:${filename}:" | cut -d':' -f3 +} +parse_args() { + BINDIR="${BINSTALLER_BIN:-${HOME}/.local/bin}" + DRY_RUN=0 + while getopts "b:dqh?xn" arg; do + case "$arg" in + b) BINDIR="$OPTARG" ;; + d) log_set_priority 10 ;; + q) log_set_priority 3 ;; + h | \?) usage "$0" ;; + x) set -x ;; + n) DRY_RUN=1 ;; + esac + done + shift $((OPTIND - 1)) + TAG="${1:-latest}" +} +tag_to_version() { + if [ "$TAG" = "latest" ]; then + log_info "checking GitHub for latest tag" + REALTAG=$(github_release "${REPO}" "${TAG}") && true + test -n "$REALTAG" || { + log_crit "Could not determine latest tag for ${REPO}" + exit 1 + } + else + # Assume TAG is a valid tag/version string + REALTAG="$TAG" + fi + if test -z "$REALTAG"; then + log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${REPO}/releases for details" + exit 1 + fi + VERSION=${REALTAG#v} # Strip leading 'v' + TAG="$REALTAG" # Use the resolved tag + log_info "Resolved version: ${VERSION} (tag: ${TAG})" +} + + + +resolve_asset_filename() { + + # --- Apply Rules --- + ASSET_FILENAME="" + if [ "${UNAME_ARCH}" = 'amd64' ] && true + then + ARCH='x86_64' + fi + if [ "${UNAME_OS}" = 'darwin' ] && true + then + OS='Darwin' + fi + if [ "${UNAME_OS}" = 'linux' ] && true + then + OS='Linux' + fi + if [ "${UNAME_OS}" = 'windows' ] && true + then + OS='Windows' + fi + if [ "${UNAME_OS}" = 'windows' ] && true + then + EXT='.zip' + fi + if [ -z "${ASSET_FILENAME}" ]; then + ASSET_FILENAME="binst_${OS}_${ARCH}${EXT}" + fi +} +# Cleanup function to remove temporary files and stop progress +cleanup() { + # Stop progress animation + progress_clear + + if [ -n "$TMPDIR" ] && [ -d "$TMPDIR" ]; then + log_debug "Cleaning up temporary directory: $TMPDIR" + rm -rf -- "$TMPDIR" + fi +} + +execute() { + STRIP_COMPONENTS=0 + CHECKSUM_FILENAME="checksums.txt" + + # --- Construct URLs --- + GITHUB_DOWNLOAD="https://github.com/${REPO}/releases/download" + ASSET_URL="${GITHUB_DOWNLOAD}/${TAG}/${ASSET_FILENAME}" + CHECKSUM_URL="" + if [ -n "$CHECKSUM_FILENAME" ]; then + CHECKSUM_URL="${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM_FILENAME}" + fi + + # --- Download and Verify --- + TMPDIR=$(mktemp -d) + # Set up cleanup trap that includes progress clearing + trap cleanup EXIT HUP INT TERM + log_debug "Downloading files into ${TMPDIR}" + log_info "Downloading ${ASSET_URL}" + github_http_download "${TMPDIR}/${ASSET_FILENAME}" "${ASSET_URL}" + + # Try to find embedded checksum first + EMBEDDED_HASH=$(find_embedded_checksum "$VERSION" "$ASSET_FILENAME") + + if [ -n "$EMBEDDED_HASH" ]; then + log_info "Using embedded checksum for verification" + + # Verify using embedded hash + got=$(hash_compute "${TMPDIR}/${ASSET_FILENAME}") + if [ "$got" != "$EMBEDDED_HASH" ]; then + log_crit "Checksum verification failed for ${ASSET_FILENAME}" + log_crit "Expected: ${EMBEDDED_HASH}" + log_crit "Got: ${got}" + return 1 + fi + log_info "Checksum verification successful" + elif [ -n "$CHECKSUM_URL" ]; then + # Fall back to downloading checksum file + log_info "Downloading checksums from ${CHECKSUM_URL}" + github_http_download "${TMPDIR}/${CHECKSUM_FILENAME}" "${CHECKSUM_URL}" + log_info "Verifying checksum ..." + hash_verify "${TMPDIR}/${ASSET_FILENAME}" "${TMPDIR}/${CHECKSUM_FILENAME}" + else + log_info "No checksum found, skipping verification." + fi + + if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then + log_debug "Target is raw binary" + else + log_info "Extracting ${ASSET_FILENAME}..." + (cd "${TMPDIR}" && untar "${ASSET_FILENAME}" "${STRIP_COMPONENTS}") + fi + BINARY_NAME='binstaller' + if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then + BINARY_PATH="${TMPDIR}/${ASSET_FILENAME}" + else + BINARY_PATH="${TMPDIR}/binstaller" + fi + + if [ "${UNAME_OS}" = "windows" ]; then + case "${BINARY_NAME}" in *.exe) ;; *) BINARY_NAME="${BINARY_NAME}.exe" ;; esac + case "${BINARY_PATH}" in *.exe) ;; *) BINARY_PATH="${BINARY_PATH}.exe" ;; esac + fi + + if [ ! -f "${BINARY_PATH}" ]; then + log_crit "Binary not found: ${BINARY_PATH}" + log_crit "Listing contents of ${TMPDIR} ..." + if command -v find >/dev/null 2>&1; then + cd "${TMPDIR}" && find . + else + cd "${TMPDIR}" && ls -R . + fi + return 1 + fi + + progress_clear + # Install the binary + INSTALL_PATH="${BINDIR}/${BINARY_NAME}" + + if [ "$DRY_RUN" = "1" ]; then + log_info "[DRY RUN] ${BINARY_NAME} dry-run installation succeeded! (Would install to: ${INSTALL_PATH})" + else + log_info "Installing binary to ${INSTALL_PATH}" + test ! -d "${BINDIR}" && install -d "${BINDIR}" + install "${BINARY_PATH}" "${INSTALL_PATH}" + log_info "${BINARY_NAME} installation complete!" + fi +} + +# --- Configuration --- +NAME='binstaller' +REPO='binary-install/binstaller' +EXT='.tar.gz' + +# use in logging routines +log_prefix() { + echo "${REPO}" +} + +parse_args "$@" + +progress_init +progress_pulse_start + +# --- Determine target platform --- +OS="${BINSTALLER_OS:-$(uname_os)}" +UNAME_OS="${OS}" + +ARCH="${BINSTALLER_ARCH:-$(uname_arch)}" +UNAME_ARCH="${ARCH}" +log_info "Detected Platform: ${OS}/${ARCH}" + +# --- Validate platform --- +uname_os_check "$OS" +uname_arch_check "$ARCH" + +tag_to_version + +resolve_asset_filename +execute diff --git a/scripts/claude-install.sh b/scripts/claude-install.sh new file mode 100755 index 0000000..3d0a200 --- /dev/null +++ b/scripts/claude-install.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +set -o errexit + +curl -fsSL https://claude.ai/install.sh | bash diff --git a/scripts/eksctl-install.sh b/scripts/eksctl-install.sh new file mode 100755 index 0000000..22e6be2 --- /dev/null +++ b/scripts/eksctl-install.sh @@ -0,0 +1,618 @@ +#!/bin/sh +# Code generated by binstaller. DO NOT EDIT. +# +set -e +usage() { + this=$1 + cat </dev/null +} +echoerr() { + echo "$@" 1>&2 +} +_logp=6 +log_set_priority() { + _logp="$1" +} +log_priority() { + if test -z "$1"; then + echo "$_logp" + return + fi + [ "$1" -le "$_logp" ] +} +log_tag() { + case $1 in + 0) echo "emerg" ;; + 1) echo "alert" ;; + 2) echo "crit" ;; + 3) echo "err" ;; + 4) echo "warning" ;; + 5) echo "notice" ;; + 6) echo "info" ;; + 7) echo "debug" ;; + *) echo "$1" ;; + esac +} +log_debug() { + log_priority 7 || return 0 + echoerr "$(log_prefix)" "$(log_tag 7)" "$@" +} +log_info() { + log_priority 6 || return 0 + echoerr "$(log_prefix)" "$(log_tag 6)" "$@" +} +log_err() { + log_priority 3 || return 0 + echoerr "$(log_prefix)" "$(log_tag 3)" "$@" +} +log_crit() { + log_priority 2 || return 0 + echoerr "$(log_prefix)" "$(log_tag 2)" "$@" +} +uname_os() { + os=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$os" in + msys*) os="windows" ;; + mingw*) os="windows" ;; + cygwin*) os="windows" ;; + esac + if [ "$os" = "sunos" ]; then + if [ "$(uname -o)" = "illumos" ]; then + os="illumos" + else + os="solaris" + fi + fi + echo "$os" +} +uname_arch() { + arch=$(uname -m) + case $arch in + x86_64) arch="amd64" ;; + i86pc) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; + esac + echo "${arch}" +} +uname_os_check() { + os=$(uname_os) + case "$os" in + darwin) return 0 ;; + dragonfly) return 0 ;; + freebsd) return 0 ;; + linux) return 0 ;; + android) return 0 ;; + midnightbsd) return 0 ;; + nacl) return 0 ;; + netbsd) return 0 ;; + openbsd) return 0 ;; + plan9) return 0 ;; + solaris) return 0 ;; + illumos) return 0 ;; + windows) return 0 ;; + esac + log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" + return 1 +} +uname_arch_check() { + arch=$(uname_arch) + case "$arch" in + 386) return 0 ;; + amd64) return 0 ;; + arm64) return 0 ;; + armv5) return 0 ;; + armv6) return 0 ;; + armv7) return 0 ;; + ppc64) return 0 ;; + ppc64le) return 0 ;; + mips) return 0 ;; + mipsle) return 0 ;; + mips64) return 0 ;; + mips64le) return 0 ;; + s390x) return 0 ;; + amd64p32) return 0 ;; + esac + log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" + return 1 +} +cat /dev/null </dev/null) || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command openssl; then + hash=$(openssl dgst -sha256 "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 2 + else + log_crit "hash_sha256 unable to find command to compute sha-256 hash" + return 1 + fi +} + +hash_compute() { + hash_sha256 "$1" +} + + +# shellcheck shell=sh +# Terminal progress reporting functions +progress_init() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + # OSC 9;4 sequences are safely ignored by unsupporting terminals + # Only need special handling for tmux passthrough + if [ -n "$TMUX" ]; then + # Tmux passthrough: DCS tmux; ST + # ESC characters in the wrapped sequence must be doubled + # Format: ESC P tmux; ESC ESC ] 9;4; ... ESC ESC \ ESC \ + PROGRESS_START=$(printf '\033Ptmux;\033\033]9;4;') + # shellcheck disable=SC1003 + PROGRESS_END=$(printf '\033\033\\\033\\') + else + # Direct OSC 9;4 - terminals that don't support it will safely ignore + PROGRESS_START=$(printf '\033]9;4;') + PROGRESS_END=$(printf '\007') + fi +} + +# Start pulsing progress animation +progress_pulse_start() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + + # Send OSC 9;4 with state 3 (indeterminate/pulsing) once + # The terminal will handle the continuous animation + printf "%s3%s" "$PROGRESS_START" "$PROGRESS_END" >&2 +} + +# Clear progress indicator +progress_clear() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + printf "%s0;%s" "$PROGRESS_START" "$PROGRESS_END" >&2 +} + +untar() { + tarball=$1 + strip_components=${2:-0} # default 0 + case "${tarball}" in + *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" --strip-components "${strip_components}" ;; + *.tar.xz) tar --no-same-owner -xJf "${tarball}" --strip-components "${strip_components}" ;; + *.tar.bz2) tar --no-same-owner -xjf "${tarball}" --strip-components "${strip_components}" ;; + *.tar) tar --no-same-owner -xf "${tarball}" --strip-components "${strip_components}" ;; + *.gz) gunzip "${tarball}" ;; + *.zip) + # unzip doesn't have a standard --strip-components + # Workaround: extract to a subdir and move contents up if stripping + if [ "$strip_components" -gt 0 ]; then + extract_dir=$(basename "${tarball%.zip}")_extracted + unzip -q "${tarball}" -d "${extract_dir}" + # Move contents of the *first* directory found inside extract_dir up + # This assumes wrap_in_directory=true convention + first_subdir=$(find "${extract_dir}" -mindepth 1 -maxdepth 1 -type d -print -quit) + if [ -n "$first_subdir" ]; then + # Move all contents (* includes hidden files) + mv "${first_subdir}"/* . + # Optionally remove the now-empty subdir and the extract_dir + rmdir "${first_subdir}" + rmdir "${extract_dir}" + else + log_warn "Could not find subdirectory in zip to strip components from ${extract_dir}" + # Files are extracted in current dir anyway, proceed + fi + else + unzip -q "${tarball}" + fi + ;; + *) + log_err "untar unknown archive format for ${tarball}" + return 1 + ;; + esac +} + + + +hash_verify() { + TARGET_PATH=$1 + SUMFILE=$2 + if [ -z "${SUMFILE}" ]; then + log_err "hash_verify checksum file not specified in arg2" + return 1 + fi + got=$(hash_compute "$TARGET_PATH") + if [ -z "${got}" ]; then + log_err "failed to calculate hash: ${TARGET_PATH}" + return 1 + fi + + BASENAME=${TARGET_PATH##*/} + + # Check for line matches in checksum file + # Format: " " or " *" + # Filename may include path prefix (e.g., "deployment/m2/file.tar.gz") + while IFS= read -r line || [ -n "$line" ]; do + # Normalize tabs to spaces + line=$(echo "$line" | tr '\t' ' ') + + # Remove trailing spaces for hash-only line check + line_trimmed=$(echo "$line" | sed 's/[[:space:]]*$//') + + # Check for hash-only line (no filename) - early return + if [ "$line_trimmed" = "$got" ]; then + return 0 + fi + + # Extract hash and filename parts + # First field is the hash, rest is filename (which may contain spaces) + line_hash=$(echo "$line" | cut -d' ' -f1) + + # Skip if hash doesn't match + if [ "$line_hash" != "$got" ]; then + continue + fi + + # Hash matches, now check filename + # Remove the hash part from the beginning of the line + line_rest="${line#"$got"}" + # Remove leading spaces + while [ "${line_rest#[ ]}" != "$line_rest" ]; do + line_rest="${line_rest#[ ]}" + done + + # Remove leading asterisk if present (binary mode indicator) + if [ "${line_rest#\*}" != "$line_rest" ]; then + line_rest="${line_rest#\*}" + fi + + # Extract just the filename without any path + line_filename="${line_rest##*/}" + + # Check if the filename matches + if [ "$line_filename" = "$BASENAME" ]; then + return 0 + fi + done < "$SUMFILE" + + log_err "hash_verify checksum for '$TARGET_PATH' did not verify" + log_err " Expected hash: ${got}" + log_err " Checksum file content:" + cat "$SUMFILE" >&2 + return 1 +} + +# GitHub HTTP download functions with GITHUB_TOKEN support +github_http_download_curl() { + local_file=$1 + source_url=$2 + header=$3 + if [ -n "$GITHUB_TOKEN" ]; then + log_debug "Using GITHUB_TOKEN for authentication" + if [ -z "$header" ]; then + curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -o "$local_file" "$source_url" + else + curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -H "$header" -o "$local_file" "$source_url" + fi + else + if [ -z "$header" ]; then + curl -fsSL -o "$local_file" "$source_url" + else + curl -fsSL -H "$header" -o "$local_file" "$source_url" + fi + fi +} +github_http_download_wget() { + local_file=$1 + source_url=$2 + header=$3 + if [ -n "$GITHUB_TOKEN" ]; then + log_debug "Using GITHUB_TOKEN for authentication" + if [ -z "$header" ]; then + wget -q --header "Authorization: Bearer $GITHUB_TOKEN" -O "$local_file" "$source_url" + else + wget -q --header "Authorization: Bearer $GITHUB_TOKEN" --header "$header" -O "$local_file" "$source_url" + fi + else + if [ -z "$header" ]; then + wget -q -O "$local_file" "$source_url" + else + wget -q --header "$header" -O "$local_file" "$source_url" + fi + fi +} +github_http_download() { + log_debug "github_http_download $2" + if is_command curl; then + github_http_download_curl "$@" + return + elif is_command wget; then + github_http_download_wget "$@" + return + fi + log_crit "github_http_download unable to find wget or curl" + return 1 +} +github_http_copy() { + tmp=$(mktemp) + github_http_download "${tmp}" "$@" || return 1 + body=$(cat "$tmp") + rm -f "${tmp}" + echo "$body" +} +github_release() { + owner_repo=$1 + version=$2 + test -z "$version" && version="latest" + giturl="https://github.com/${owner_repo}/releases/${version}" + json=$(github_http_copy "$giturl" "Accept:application/json") + test -z "$json" && return 1 + version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') + test -z "$version" && return 1 + echo "$version" +} + +# --- Embedded Checksums (Format: VERSION:FILENAME:HASH) --- +EMBEDDED_CHECKSUMS=" +0.221.0:eksctl_Darwin_amd64.tar.gz:9a5a3370ed79ffa646e25a0115aa7ee7fa203b284dffe058135b353be1542752 +0.221.0:eksctl_Darwin_arm64.tar.gz:80a5f4ec7c58aad8884665318f448e88f1861a9e0eb2a467e0b879161d90bcc2 +0.221.0:eksctl_Linux_amd64.tar.gz:9c04d16e5a0be350f9c383088ed99eac2cf1af830d25360c34588854e9e06cda +0.221.0:eksctl_Linux_arm64.tar.gz:3c8cfe3954c1a88a49d7f09cc03f69dceab689b1d396b9d0bcba80228d12978e +0.221.0:eksctl_Windows_amd64.zip:c247283dc064ac55e44222191d71800f189a16b438b42cb520fb1489d1b80513 +0.221.0:eksctl_Windows_arm64.zip:f1dbc8383281c0c318e4ee99eb688438f1d7a19dbbe2b2a03e93863740dc0f73" + +# Find embedded checksum for a given version and filename +find_embedded_checksum() { + version="$1" + filename="$2" + echo "$EMBEDDED_CHECKSUMS" | grep -E "^${version}:${filename}:" | cut -d':' -f3 +} +parse_args() { + BINDIR="${BINSTALLER_BIN:-${HOME}/.local/bin}" + DRY_RUN=0 + while getopts "b:dqh?xn" arg; do + case "$arg" in + b) BINDIR="$OPTARG" ;; + d) log_set_priority 10 ;; + q) log_set_priority 3 ;; + h | \?) usage "$0" ;; + x) set -x ;; + n) DRY_RUN=1 ;; + esac + done + shift $((OPTIND - 1)) + TAG="${1:-latest}" +} +tag_to_version() { + if [ "$TAG" = "latest" ]; then + log_info "checking GitHub for latest tag" + REALTAG=$(github_release "${REPO}" "${TAG}") && true + test -n "$REALTAG" || { + log_crit "Could not determine latest tag for ${REPO}" + exit 1 + } + else + # Assume TAG is a valid tag/version string + REALTAG="$TAG" + fi + if test -z "$REALTAG"; then + log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${REPO}/releases for details" + exit 1 + fi + VERSION=${REALTAG#v} # Strip leading 'v' + TAG="$REALTAG" # Use the resolved tag + log_info "Resolved version: ${VERSION} (tag: ${TAG})" +} + + + +resolve_asset_filename() { + + # --- Apply Rules --- + ASSET_FILENAME="" + if [ "${UNAME_OS}" = 'darwin' ] && true + then + OS='Darwin' + fi + if [ "${UNAME_OS}" = 'linux' ] && true + then + OS='Linux' + fi + if [ "${UNAME_OS}" = 'windows' ] && true + then + OS='Windows' + fi + if [ "${UNAME_OS}" = 'windows' ] && true + then + EXT='.zip' + fi + if [ -z "${ASSET_FILENAME}" ]; then + ASSET_FILENAME="eksctl_${OS}_${ARCH}${EXT}" + fi +} +# Cleanup function to remove temporary files and stop progress +cleanup() { + # Stop progress animation + progress_clear + + if [ -n "$TMPDIR" ] && [ -d "$TMPDIR" ]; then + log_debug "Cleaning up temporary directory: $TMPDIR" + rm -rf -- "$TMPDIR" + fi +} + +execute() { + STRIP_COMPONENTS=0 + CHECKSUM_FILENAME="eksctl_checksums.txt" + + # --- Construct URLs --- + GITHUB_DOWNLOAD="https://github.com/${REPO}/releases/download" + ASSET_URL="${GITHUB_DOWNLOAD}/${TAG}/${ASSET_FILENAME}" + CHECKSUM_URL="" + if [ -n "$CHECKSUM_FILENAME" ]; then + CHECKSUM_URL="${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM_FILENAME}" + fi + + # --- Download and Verify --- + TMPDIR=$(mktemp -d) + # Set up cleanup trap that includes progress clearing + trap cleanup EXIT HUP INT TERM + log_debug "Downloading files into ${TMPDIR}" + log_info "Downloading ${ASSET_URL}" + github_http_download "${TMPDIR}/${ASSET_FILENAME}" "${ASSET_URL}" + + # Try to find embedded checksum first + EMBEDDED_HASH=$(find_embedded_checksum "$VERSION" "$ASSET_FILENAME") + + if [ -n "$EMBEDDED_HASH" ]; then + log_info "Using embedded checksum for verification" + + # Verify using embedded hash + got=$(hash_compute "${TMPDIR}/${ASSET_FILENAME}") + if [ "$got" != "$EMBEDDED_HASH" ]; then + log_crit "Checksum verification failed for ${ASSET_FILENAME}" + log_crit "Expected: ${EMBEDDED_HASH}" + log_crit "Got: ${got}" + return 1 + fi + log_info "Checksum verification successful" + elif [ -n "$CHECKSUM_URL" ]; then + # Fall back to downloading checksum file + log_info "Downloading checksums from ${CHECKSUM_URL}" + github_http_download "${TMPDIR}/${CHECKSUM_FILENAME}" "${CHECKSUM_URL}" + log_info "Verifying checksum ..." + hash_verify "${TMPDIR}/${ASSET_FILENAME}" "${TMPDIR}/${CHECKSUM_FILENAME}" + else + log_info "No checksum found, skipping verification." + fi + + if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then + log_debug "Target is raw binary" + else + log_info "Extracting ${ASSET_FILENAME}..." + (cd "${TMPDIR}" && untar "${ASSET_FILENAME}" "${STRIP_COMPONENTS}") + fi + BINARY_NAME='eksctl' + if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then + BINARY_PATH="${TMPDIR}/${ASSET_FILENAME}" + else + BINARY_PATH="${TMPDIR}/eksctl" + fi + + if [ "${UNAME_OS}" = "windows" ]; then + case "${BINARY_NAME}" in *.exe) ;; *) BINARY_NAME="${BINARY_NAME}.exe" ;; esac + case "${BINARY_PATH}" in *.exe) ;; *) BINARY_PATH="${BINARY_PATH}.exe" ;; esac + fi + + if [ ! -f "${BINARY_PATH}" ]; then + log_crit "Binary not found: ${BINARY_PATH}" + log_crit "Listing contents of ${TMPDIR} ..." + if command -v find >/dev/null 2>&1; then + cd "${TMPDIR}" && find . + else + cd "${TMPDIR}" && ls -R . + fi + return 1 + fi + + progress_clear + # Install the binary + INSTALL_PATH="${BINDIR}/${BINARY_NAME}" + + if [ "$DRY_RUN" = "1" ]; then + log_info "[DRY RUN] ${BINARY_NAME} dry-run installation succeeded! (Would install to: ${INSTALL_PATH})" + else + log_info "Installing binary to ${INSTALL_PATH}" + test ! -d "${BINDIR}" && install -d "${BINDIR}" + install "${BINARY_PATH}" "${INSTALL_PATH}" + log_info "${BINARY_NAME} installation complete!" + fi +} + +# --- Configuration --- +NAME='eksctl' +REPO='eksctl-io/eksctl' +EXT='.tar.gz' + +# use in logging routines +log_prefix() { + echo "${REPO}" +} + +parse_args "$@" + +progress_init +progress_pulse_start + +# --- Determine target platform --- +OS="${BINSTALLER_OS:-$(uname_os)}" +UNAME_OS="${OS}" + +ARCH="${BINSTALLER_ARCH:-$(uname_arch)}" + +log_info "Detected Platform: ${OS}/${ARCH}" + +# --- Validate platform --- +uname_os_check "$OS" +uname_arch_check "$ARCH" + +tag_to_version + +resolve_asset_filename +execute diff --git a/scripts/gh-md-toc-install.sh b/scripts/gh-md-toc-install.sh new file mode 100755 index 0000000..21e728d --- /dev/null +++ b/scripts/gh-md-toc-install.sh @@ -0,0 +1,604 @@ +#!/bin/sh +# Code generated by binstaller. DO NOT EDIT. +# +set -e +usage() { + this=$1 + cat </dev/null +} +echoerr() { + echo "$@" 1>&2 +} +_logp=6 +log_set_priority() { + _logp="$1" +} +log_priority() { + if test -z "$1"; then + echo "$_logp" + return + fi + [ "$1" -le "$_logp" ] +} +log_tag() { + case $1 in + 0) echo "emerg" ;; + 1) echo "alert" ;; + 2) echo "crit" ;; + 3) echo "err" ;; + 4) echo "warning" ;; + 5) echo "notice" ;; + 6) echo "info" ;; + 7) echo "debug" ;; + *) echo "$1" ;; + esac +} +log_debug() { + log_priority 7 || return 0 + echoerr "$(log_prefix)" "$(log_tag 7)" "$@" +} +log_info() { + log_priority 6 || return 0 + echoerr "$(log_prefix)" "$(log_tag 6)" "$@" +} +log_err() { + log_priority 3 || return 0 + echoerr "$(log_prefix)" "$(log_tag 3)" "$@" +} +log_crit() { + log_priority 2 || return 0 + echoerr "$(log_prefix)" "$(log_tag 2)" "$@" +} +uname_os() { + os=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$os" in + msys*) os="windows" ;; + mingw*) os="windows" ;; + cygwin*) os="windows" ;; + esac + if [ "$os" = "sunos" ]; then + if [ "$(uname -o)" = "illumos" ]; then + os="illumos" + else + os="solaris" + fi + fi + echo "$os" +} +uname_arch() { + arch=$(uname -m) + case $arch in + x86_64) arch="amd64" ;; + i86pc) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; + esac + echo "${arch}" +} +uname_os_check() { + os=$(uname_os) + case "$os" in + darwin) return 0 ;; + dragonfly) return 0 ;; + freebsd) return 0 ;; + linux) return 0 ;; + android) return 0 ;; + midnightbsd) return 0 ;; + nacl) return 0 ;; + netbsd) return 0 ;; + openbsd) return 0 ;; + plan9) return 0 ;; + solaris) return 0 ;; + illumos) return 0 ;; + windows) return 0 ;; + esac + log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" + return 1 +} +uname_arch_check() { + arch=$(uname_arch) + case "$arch" in + 386) return 0 ;; + amd64) return 0 ;; + arm64) return 0 ;; + armv5) return 0 ;; + armv6) return 0 ;; + armv7) return 0 ;; + ppc64) return 0 ;; + ppc64le) return 0 ;; + mips) return 0 ;; + mipsle) return 0 ;; + mips64) return 0 ;; + mips64le) return 0 ;; + s390x) return 0 ;; + amd64p32) return 0 ;; + esac + log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" + return 1 +} +cat /dev/null </dev/null) || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command openssl; then + hash=$(openssl dgst -sha256 "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 2 + else + log_crit "hash_sha256 unable to find command to compute sha-256 hash" + return 1 + fi +} + +hash_compute() { + hash_sha256 "$1" +} + + +# shellcheck shell=sh +# Terminal progress reporting functions +progress_init() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + # OSC 9;4 sequences are safely ignored by unsupporting terminals + # Only need special handling for tmux passthrough + if [ -n "$TMUX" ]; then + # Tmux passthrough: DCS tmux; ST + # ESC characters in the wrapped sequence must be doubled + # Format: ESC P tmux; ESC ESC ] 9;4; ... ESC ESC \ ESC \ + PROGRESS_START=$(printf '\033Ptmux;\033\033]9;4;') + # shellcheck disable=SC1003 + PROGRESS_END=$(printf '\033\033\\\033\\') + else + # Direct OSC 9;4 - terminals that don't support it will safely ignore + PROGRESS_START=$(printf '\033]9;4;') + PROGRESS_END=$(printf '\007') + fi +} + +# Start pulsing progress animation +progress_pulse_start() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + + # Send OSC 9;4 with state 3 (indeterminate/pulsing) once + # The terminal will handle the continuous animation + printf "%s3%s" "$PROGRESS_START" "$PROGRESS_END" >&2 +} + +# Clear progress indicator +progress_clear() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + printf "%s0;%s" "$PROGRESS_START" "$PROGRESS_END" >&2 +} + +untar() { + tarball=$1 + strip_components=${2:-0} # default 0 + case "${tarball}" in + *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" --strip-components "${strip_components}" ;; + *.tar.xz) tar --no-same-owner -xJf "${tarball}" --strip-components "${strip_components}" ;; + *.tar.bz2) tar --no-same-owner -xjf "${tarball}" --strip-components "${strip_components}" ;; + *.tar) tar --no-same-owner -xf "${tarball}" --strip-components "${strip_components}" ;; + *.gz) gunzip "${tarball}" ;; + *.zip) + # unzip doesn't have a standard --strip-components + # Workaround: extract to a subdir and move contents up if stripping + if [ "$strip_components" -gt 0 ]; then + extract_dir=$(basename "${tarball%.zip}")_extracted + unzip -q "${tarball}" -d "${extract_dir}" + # Move contents of the *first* directory found inside extract_dir up + # This assumes wrap_in_directory=true convention + first_subdir=$(find "${extract_dir}" -mindepth 1 -maxdepth 1 -type d -print -quit) + if [ -n "$first_subdir" ]; then + # Move all contents (* includes hidden files) + mv "${first_subdir}"/* . + # Optionally remove the now-empty subdir and the extract_dir + rmdir "${first_subdir}" + rmdir "${extract_dir}" + else + log_warn "Could not find subdirectory in zip to strip components from ${extract_dir}" + # Files are extracted in current dir anyway, proceed + fi + else + unzip -q "${tarball}" + fi + ;; + *) + log_err "untar unknown archive format for ${tarball}" + return 1 + ;; + esac +} + + + +hash_verify() { + TARGET_PATH=$1 + SUMFILE=$2 + if [ -z "${SUMFILE}" ]; then + log_err "hash_verify checksum file not specified in arg2" + return 1 + fi + got=$(hash_compute "$TARGET_PATH") + if [ -z "${got}" ]; then + log_err "failed to calculate hash: ${TARGET_PATH}" + return 1 + fi + + BASENAME=${TARGET_PATH##*/} + + # Check for line matches in checksum file + # Format: " " or " *" + # Filename may include path prefix (e.g., "deployment/m2/file.tar.gz") + while IFS= read -r line || [ -n "$line" ]; do + # Normalize tabs to spaces + line=$(echo "$line" | tr '\t' ' ') + + # Remove trailing spaces for hash-only line check + line_trimmed=$(echo "$line" | sed 's/[[:space:]]*$//') + + # Check for hash-only line (no filename) - early return + if [ "$line_trimmed" = "$got" ]; then + return 0 + fi + + # Extract hash and filename parts + # First field is the hash, rest is filename (which may contain spaces) + line_hash=$(echo "$line" | cut -d' ' -f1) + + # Skip if hash doesn't match + if [ "$line_hash" != "$got" ]; then + continue + fi + + # Hash matches, now check filename + # Remove the hash part from the beginning of the line + line_rest="${line#"$got"}" + # Remove leading spaces + while [ "${line_rest#[ ]}" != "$line_rest" ]; do + line_rest="${line_rest#[ ]}" + done + + # Remove leading asterisk if present (binary mode indicator) + if [ "${line_rest#\*}" != "$line_rest" ]; then + line_rest="${line_rest#\*}" + fi + + # Extract just the filename without any path + line_filename="${line_rest##*/}" + + # Check if the filename matches + if [ "$line_filename" = "$BASENAME" ]; then + return 0 + fi + done < "$SUMFILE" + + log_err "hash_verify checksum for '$TARGET_PATH' did not verify" + log_err " Expected hash: ${got}" + log_err " Checksum file content:" + cat "$SUMFILE" >&2 + return 1 +} + +# GitHub HTTP download functions with GITHUB_TOKEN support +github_http_download_curl() { + local_file=$1 + source_url=$2 + header=$3 + if [ -n "$GITHUB_TOKEN" ]; then + log_debug "Using GITHUB_TOKEN for authentication" + if [ -z "$header" ]; then + curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -o "$local_file" "$source_url" + else + curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -H "$header" -o "$local_file" "$source_url" + fi + else + if [ -z "$header" ]; then + curl -fsSL -o "$local_file" "$source_url" + else + curl -fsSL -H "$header" -o "$local_file" "$source_url" + fi + fi +} +github_http_download_wget() { + local_file=$1 + source_url=$2 + header=$3 + if [ -n "$GITHUB_TOKEN" ]; then + log_debug "Using GITHUB_TOKEN for authentication" + if [ -z "$header" ]; then + wget -q --header "Authorization: Bearer $GITHUB_TOKEN" -O "$local_file" "$source_url" + else + wget -q --header "Authorization: Bearer $GITHUB_TOKEN" --header "$header" -O "$local_file" "$source_url" + fi + else + if [ -z "$header" ]; then + wget -q -O "$local_file" "$source_url" + else + wget -q --header "$header" -O "$local_file" "$source_url" + fi + fi +} +github_http_download() { + log_debug "github_http_download $2" + if is_command curl; then + github_http_download_curl "$@" + return + elif is_command wget; then + github_http_download_wget "$@" + return + fi + log_crit "github_http_download unable to find wget or curl" + return 1 +} +github_http_copy() { + tmp=$(mktemp) + github_http_download "${tmp}" "$@" || return 1 + body=$(cat "$tmp") + rm -f "${tmp}" + echo "$body" +} +github_release() { + owner_repo=$1 + version=$2 + test -z "$version" && version="latest" + giturl="https://github.com/${owner_repo}/releases/${version}" + json=$(github_http_copy "$giturl" "Accept:application/json") + test -z "$json" && return 1 + version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') + test -z "$version" && return 1 + echo "$version" +} + +# --- Embedded Checksums (Format: VERSION:FILENAME:HASH) --- +EMBEDDED_CHECKSUMS=" +2.0.0:gh-md-toc_2.0.0_darwin_amd64.tar.gz:622626f7880f6b604e93c06638e948500f4b458703d4066e91bae5b857934666 +2.0.0:gh-md-toc_2.0.0_darwin_arm64.tar.gz:6a610cfe0e30e46ba49027da8eef3869fd2b98be9403b9d231df8c619bb59659 +2.0.0:gh-md-toc_2.0.0_linux_amd64.tar.gz:7546a6e1146ba53d23a657cd6e93f2ed6ed0cc06bcf882c8f2983116f758adf5 +2.0.0:gh-md-toc_2.0.0_linux_arm64.tar.gz:30487683256f1a0a923433b97730b74110552e857506e3fee5cc2d11ee7366ed +2.0.0:gh-md-toc_2.0.0_linux_armv6.tar.gz:c715d4432e95dda4a06232c9b3c01e945e07ec00a11355b5f1d4804f36794bcb +2.0.0:gh-md-toc_2.0.0_windows_amd64.tar.gz:d06b047b3314c875034f8e29f07a1c8498e3bfaab3b01cbfa06f7d37e013d222 +2.0.0:gh-md-toc_2.0.0_windows_arm64.tar.gz:22d045c736b3fedc90e42467c92dbb43bef3d8a625f05587070f6ca0b2d4f1d1 +2.0.0:gh-md-toc_2.0.0_windows_armv6.tar.gz:24c85ede2c0d897993174d5df9905ee6d9f78d95a0f5d36a1324ab55e0dac735" + +# Find embedded checksum for a given version and filename +find_embedded_checksum() { + version="$1" + filename="$2" + echo "$EMBEDDED_CHECKSUMS" | grep -E "^${version}:${filename}:" | cut -d':' -f3 +} +parse_args() { + BINDIR="${BINSTALLER_BIN:-${HOME}/.local/bin}" + DRY_RUN=0 + while getopts "b:dqh?xn" arg; do + case "$arg" in + b) BINDIR="$OPTARG" ;; + d) log_set_priority 10 ;; + q) log_set_priority 3 ;; + h | \?) usage "$0" ;; + x) set -x ;; + n) DRY_RUN=1 ;; + esac + done + shift $((OPTIND - 1)) + TAG="${1:-latest}" +} +tag_to_version() { + if [ "$TAG" = "latest" ]; then + log_info "checking GitHub for latest tag" + REALTAG=$(github_release "${REPO}" "${TAG}") && true + test -n "$REALTAG" || { + log_crit "Could not determine latest tag for ${REPO}" + exit 1 + } + else + # Assume TAG is a valid tag/version string + REALTAG="$TAG" + fi + if test -z "$REALTAG"; then + log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${REPO}/releases for details" + exit 1 + fi + VERSION=${REALTAG#v} # Strip leading 'v' + TAG="$REALTAG" # Use the resolved tag + log_info "Resolved version: ${VERSION} (tag: ${TAG})" +} + + + +resolve_asset_filename() { + + # --- Apply Rules --- + ASSET_FILENAME="" + if [ -z "${ASSET_FILENAME}" ]; then + ASSET_FILENAME="gh-md-toc_${VERSION}_${OS}_${ARCH}${EXT}" + fi +} +# Cleanup function to remove temporary files and stop progress +cleanup() { + # Stop progress animation + progress_clear + + if [ -n "$TMPDIR" ] && [ -d "$TMPDIR" ]; then + log_debug "Cleaning up temporary directory: $TMPDIR" + rm -rf -- "$TMPDIR" + fi +} + +execute() { + STRIP_COMPONENTS=0 + CHECKSUM_FILENAME="checksums.txt" + + # --- Construct URLs --- + GITHUB_DOWNLOAD="https://github.com/${REPO}/releases/download" + ASSET_URL="${GITHUB_DOWNLOAD}/${TAG}/${ASSET_FILENAME}" + CHECKSUM_URL="" + if [ -n "$CHECKSUM_FILENAME" ]; then + CHECKSUM_URL="${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM_FILENAME}" + fi + + # --- Download and Verify --- + TMPDIR=$(mktemp -d) + # Set up cleanup trap that includes progress clearing + trap cleanup EXIT HUP INT TERM + log_debug "Downloading files into ${TMPDIR}" + log_info "Downloading ${ASSET_URL}" + github_http_download "${TMPDIR}/${ASSET_FILENAME}" "${ASSET_URL}" + + # Try to find embedded checksum first + EMBEDDED_HASH=$(find_embedded_checksum "$VERSION" "$ASSET_FILENAME") + + if [ -n "$EMBEDDED_HASH" ]; then + log_info "Using embedded checksum for verification" + + # Verify using embedded hash + got=$(hash_compute "${TMPDIR}/${ASSET_FILENAME}") + if [ "$got" != "$EMBEDDED_HASH" ]; then + log_crit "Checksum verification failed for ${ASSET_FILENAME}" + log_crit "Expected: ${EMBEDDED_HASH}" + log_crit "Got: ${got}" + return 1 + fi + log_info "Checksum verification successful" + elif [ -n "$CHECKSUM_URL" ]; then + # Fall back to downloading checksum file + log_info "Downloading checksums from ${CHECKSUM_URL}" + github_http_download "${TMPDIR}/${CHECKSUM_FILENAME}" "${CHECKSUM_URL}" + log_info "Verifying checksum ..." + hash_verify "${TMPDIR}/${ASSET_FILENAME}" "${TMPDIR}/${CHECKSUM_FILENAME}" + else + log_info "No checksum found, skipping verification." + fi + + if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then + log_debug "Target is raw binary" + else + log_info "Extracting ${ASSET_FILENAME}..." + (cd "${TMPDIR}" && untar "${ASSET_FILENAME}" "${STRIP_COMPONENTS}") + fi + BINARY_NAME='gh-md-toc' + if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then + BINARY_PATH="${TMPDIR}/${ASSET_FILENAME}" + else + BINARY_PATH="${TMPDIR}/gh-md-toc" + fi + + if [ "${UNAME_OS}" = "windows" ]; then + case "${BINARY_NAME}" in *.exe) ;; *) BINARY_NAME="${BINARY_NAME}.exe" ;; esac + case "${BINARY_PATH}" in *.exe) ;; *) BINARY_PATH="${BINARY_PATH}.exe" ;; esac + fi + + if [ ! -f "${BINARY_PATH}" ]; then + log_crit "Binary not found: ${BINARY_PATH}" + log_crit "Listing contents of ${TMPDIR} ..." + if command -v find >/dev/null 2>&1; then + cd "${TMPDIR}" && find . + else + cd "${TMPDIR}" && ls -R . + fi + return 1 + fi + + progress_clear + # Install the binary + INSTALL_PATH="${BINDIR}/${BINARY_NAME}" + + if [ "$DRY_RUN" = "1" ]; then + log_info "[DRY RUN] ${BINARY_NAME} dry-run installation succeeded! (Would install to: ${INSTALL_PATH})" + else + log_info "Installing binary to ${INSTALL_PATH}" + test ! -d "${BINDIR}" && install -d "${BINDIR}" + install "${BINARY_PATH}" "${INSTALL_PATH}" + log_info "${BINARY_NAME} installation complete!" + fi +} + +# --- Configuration --- +NAME='github-markdown-toc.go' +REPO='ekalinin/github-markdown-toc.go' +EXT='.tar.gz' + +# use in logging routines +log_prefix() { + echo "${REPO}" +} + +parse_args "$@" + +progress_init +progress_pulse_start + +# --- Determine target platform --- +OS="${BINSTALLER_OS:-$(uname_os)}" +UNAME_OS="${OS}" + +ARCH="${BINSTALLER_ARCH:-$(uname_arch)}" + +log_info "Detected Platform: ${OS}/${ARCH}" + +# --- Validate platform --- +uname_os_check "$OS" +uname_arch_check "$ARCH" + +tag_to_version + +resolve_asset_filename +execute diff --git a/scripts/opentofu-install.sh b/scripts/opentofu-install.sh new file mode 100755 index 0000000..dc87a6a --- /dev/null +++ b/scripts/opentofu-install.sh @@ -0,0 +1,610 @@ +#!/bin/sh +# Code generated by binstaller. DO NOT EDIT. +# +set -e +usage() { + this=$1 + cat </dev/null +} +echoerr() { + echo "$@" 1>&2 +} +_logp=6 +log_set_priority() { + _logp="$1" +} +log_priority() { + if test -z "$1"; then + echo "$_logp" + return + fi + [ "$1" -le "$_logp" ] +} +log_tag() { + case $1 in + 0) echo "emerg" ;; + 1) echo "alert" ;; + 2) echo "crit" ;; + 3) echo "err" ;; + 4) echo "warning" ;; + 5) echo "notice" ;; + 6) echo "info" ;; + 7) echo "debug" ;; + *) echo "$1" ;; + esac +} +log_debug() { + log_priority 7 || return 0 + echoerr "$(log_prefix)" "$(log_tag 7)" "$@" +} +log_info() { + log_priority 6 || return 0 + echoerr "$(log_prefix)" "$(log_tag 6)" "$@" +} +log_err() { + log_priority 3 || return 0 + echoerr "$(log_prefix)" "$(log_tag 3)" "$@" +} +log_crit() { + log_priority 2 || return 0 + echoerr "$(log_prefix)" "$(log_tag 2)" "$@" +} +uname_os() { + os=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$os" in + msys*) os="windows" ;; + mingw*) os="windows" ;; + cygwin*) os="windows" ;; + esac + if [ "$os" = "sunos" ]; then + if [ "$(uname -o)" = "illumos" ]; then + os="illumos" + else + os="solaris" + fi + fi + echo "$os" +} +uname_arch() { + arch=$(uname -m) + case $arch in + x86_64) arch="amd64" ;; + i86pc) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; + esac + echo "${arch}" +} +uname_os_check() { + os=$(uname_os) + case "$os" in + darwin) return 0 ;; + dragonfly) return 0 ;; + freebsd) return 0 ;; + linux) return 0 ;; + android) return 0 ;; + midnightbsd) return 0 ;; + nacl) return 0 ;; + netbsd) return 0 ;; + openbsd) return 0 ;; + plan9) return 0 ;; + solaris) return 0 ;; + illumos) return 0 ;; + windows) return 0 ;; + esac + log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" + return 1 +} +uname_arch_check() { + arch=$(uname_arch) + case "$arch" in + 386) return 0 ;; + amd64) return 0 ;; + arm64) return 0 ;; + armv5) return 0 ;; + armv6) return 0 ;; + armv7) return 0 ;; + ppc64) return 0 ;; + ppc64le) return 0 ;; + mips) return 0 ;; + mipsle) return 0 ;; + mips64) return 0 ;; + mips64le) return 0 ;; + s390x) return 0 ;; + amd64p32) return 0 ;; + esac + log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" + return 1 +} +cat /dev/null </dev/null) || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command openssl; then + hash=$(openssl dgst -sha256 "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 2 + else + log_crit "hash_sha256 unable to find command to compute sha-256 hash" + return 1 + fi +} + +hash_compute() { + hash_sha256 "$1" +} + + +# shellcheck shell=sh +# Terminal progress reporting functions +progress_init() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + # OSC 9;4 sequences are safely ignored by unsupporting terminals + # Only need special handling for tmux passthrough + if [ -n "$TMUX" ]; then + # Tmux passthrough: DCS tmux; ST + # ESC characters in the wrapped sequence must be doubled + # Format: ESC P tmux; ESC ESC ] 9;4; ... ESC ESC \ ESC \ + PROGRESS_START=$(printf '\033Ptmux;\033\033]9;4;') + # shellcheck disable=SC1003 + PROGRESS_END=$(printf '\033\033\\\033\\') + else + # Direct OSC 9;4 - terminals that don't support it will safely ignore + PROGRESS_START=$(printf '\033]9;4;') + PROGRESS_END=$(printf '\007') + fi +} + +# Start pulsing progress animation +progress_pulse_start() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + + # Send OSC 9;4 with state 3 (indeterminate/pulsing) once + # The terminal will handle the continuous animation + printf "%s3%s" "$PROGRESS_START" "$PROGRESS_END" >&2 +} + +# Clear progress indicator +progress_clear() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + printf "%s0;%s" "$PROGRESS_START" "$PROGRESS_END" >&2 +} + +untar() { + tarball=$1 + strip_components=${2:-0} # default 0 + case "${tarball}" in + *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" --strip-components "${strip_components}" ;; + *.tar.xz) tar --no-same-owner -xJf "${tarball}" --strip-components "${strip_components}" ;; + *.tar.bz2) tar --no-same-owner -xjf "${tarball}" --strip-components "${strip_components}" ;; + *.tar) tar --no-same-owner -xf "${tarball}" --strip-components "${strip_components}" ;; + *.gz) gunzip "${tarball}" ;; + *.zip) + # unzip doesn't have a standard --strip-components + # Workaround: extract to a subdir and move contents up if stripping + if [ "$strip_components" -gt 0 ]; then + extract_dir=$(basename "${tarball%.zip}")_extracted + unzip -q "${tarball}" -d "${extract_dir}" + # Move contents of the *first* directory found inside extract_dir up + # This assumes wrap_in_directory=true convention + first_subdir=$(find "${extract_dir}" -mindepth 1 -maxdepth 1 -type d -print -quit) + if [ -n "$first_subdir" ]; then + # Move all contents (* includes hidden files) + mv "${first_subdir}"/* . + # Optionally remove the now-empty subdir and the extract_dir + rmdir "${first_subdir}" + rmdir "${extract_dir}" + else + log_warn "Could not find subdirectory in zip to strip components from ${extract_dir}" + # Files are extracted in current dir anyway, proceed + fi + else + unzip -q "${tarball}" + fi + ;; + *) + log_err "untar unknown archive format for ${tarball}" + return 1 + ;; + esac +} + + + +hash_verify() { + TARGET_PATH=$1 + SUMFILE=$2 + if [ -z "${SUMFILE}" ]; then + log_err "hash_verify checksum file not specified in arg2" + return 1 + fi + got=$(hash_compute "$TARGET_PATH") + if [ -z "${got}" ]; then + log_err "failed to calculate hash: ${TARGET_PATH}" + return 1 + fi + + BASENAME=${TARGET_PATH##*/} + + # Check for line matches in checksum file + # Format: " " or " *" + # Filename may include path prefix (e.g., "deployment/m2/file.tar.gz") + while IFS= read -r line || [ -n "$line" ]; do + # Normalize tabs to spaces + line=$(echo "$line" | tr '\t' ' ') + + # Remove trailing spaces for hash-only line check + line_trimmed=$(echo "$line" | sed 's/[[:space:]]*$//') + + # Check for hash-only line (no filename) - early return + if [ "$line_trimmed" = "$got" ]; then + return 0 + fi + + # Extract hash and filename parts + # First field is the hash, rest is filename (which may contain spaces) + line_hash=$(echo "$line" | cut -d' ' -f1) + + # Skip if hash doesn't match + if [ "$line_hash" != "$got" ]; then + continue + fi + + # Hash matches, now check filename + # Remove the hash part from the beginning of the line + line_rest="${line#"$got"}" + # Remove leading spaces + while [ "${line_rest#[ ]}" != "$line_rest" ]; do + line_rest="${line_rest#[ ]}" + done + + # Remove leading asterisk if present (binary mode indicator) + if [ "${line_rest#\*}" != "$line_rest" ]; then + line_rest="${line_rest#\*}" + fi + + # Extract just the filename without any path + line_filename="${line_rest##*/}" + + # Check if the filename matches + if [ "$line_filename" = "$BASENAME" ]; then + return 0 + fi + done < "$SUMFILE" + + log_err "hash_verify checksum for '$TARGET_PATH' did not verify" + log_err " Expected hash: ${got}" + log_err " Checksum file content:" + cat "$SUMFILE" >&2 + return 1 +} + +# GitHub HTTP download functions with GITHUB_TOKEN support +github_http_download_curl() { + local_file=$1 + source_url=$2 + header=$3 + if [ -n "$GITHUB_TOKEN" ]; then + log_debug "Using GITHUB_TOKEN for authentication" + if [ -z "$header" ]; then + curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -o "$local_file" "$source_url" + else + curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -H "$header" -o "$local_file" "$source_url" + fi + else + if [ -z "$header" ]; then + curl -fsSL -o "$local_file" "$source_url" + else + curl -fsSL -H "$header" -o "$local_file" "$source_url" + fi + fi +} +github_http_download_wget() { + local_file=$1 + source_url=$2 + header=$3 + if [ -n "$GITHUB_TOKEN" ]; then + log_debug "Using GITHUB_TOKEN for authentication" + if [ -z "$header" ]; then + wget -q --header "Authorization: Bearer $GITHUB_TOKEN" -O "$local_file" "$source_url" + else + wget -q --header "Authorization: Bearer $GITHUB_TOKEN" --header "$header" -O "$local_file" "$source_url" + fi + else + if [ -z "$header" ]; then + wget -q -O "$local_file" "$source_url" + else + wget -q --header "$header" -O "$local_file" "$source_url" + fi + fi +} +github_http_download() { + log_debug "github_http_download $2" + if is_command curl; then + github_http_download_curl "$@" + return + elif is_command wget; then + github_http_download_wget "$@" + return + fi + log_crit "github_http_download unable to find wget or curl" + return 1 +} +github_http_copy() { + tmp=$(mktemp) + github_http_download "${tmp}" "$@" || return 1 + body=$(cat "$tmp") + rm -f "${tmp}" + echo "$body" +} +github_release() { + owner_repo=$1 + version=$2 + test -z "$version" && version="latest" + giturl="https://github.com/${owner_repo}/releases/${version}" + json=$(github_http_copy "$giturl" "Accept:application/json") + test -z "$json" && return 1 + version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') + test -z "$version" && return 1 + echo "$version" +} + +# --- Embedded Checksums (Format: VERSION:FILENAME:HASH) --- +EMBEDDED_CHECKSUMS=" +1.11.3:tofu_1.11.3_darwin_amd64.tar.gz:c895c837af131f91ec6a493e5ab29e702bf38500ebccb5c3bf31e2ae823ab5c4 +1.11.3:tofu_1.11.3_darwin_arm64.tar.gz:ed13cc2c919a0e3532eeb71fd5cd275d44d9552d8e57f18e5867917149146490 +1.11.3:tofu_1.11.3_freebsd_386.tar.gz:9e94e9d4c1c0c6371069e55e7d85b9b80e31fb7651bdf6d86d6f6fd3c33d078e +1.11.3:tofu_1.11.3_freebsd_amd64.tar.gz:8ce117bd91d28608369e7f057b7ea10c638bef1625d55eb8bbc6d0ab1a57382d +1.11.3:tofu_1.11.3_freebsd_arm.tar.gz:6d60c289445d11f51bff7a828a8eaa3eeed6db1209e88eadd436c49852932775 +1.11.3:tofu_1.11.3_linux_386.tar.gz:15f5721d928af1c53a11a2cbcca700a16c3d81ddd0b1b6d205d662d3d8f6b320 +1.11.3:tofu_1.11.3_linux_amd64.tar.gz:46b567512d4cfa631551b936f3616c02277954fece29d91a52d264537399ad03 +1.11.3:tofu_1.11.3_linux_arm.tar.gz:f670ddb67a1c187735bfe4d9e4642a1f35903e3f3548a5604fe15c0c4f3b0fb8 +1.11.3:tofu_1.11.3_linux_arm64.tar.gz:abb19fdbb4516a30f303f0686cd156f2ff768b34c8d1e433ce5e6f1d81399425 +1.11.3:tofu_1.11.3_openbsd_386.tar.gz:e2b74b05a5e3faf5f957361382b47129a411840b68be07cce216b6ce723af7aa +1.11.3:tofu_1.11.3_openbsd_amd64.tar.gz:e4e21e05db28441c41d94267495d65ae098f791df365c6fcd2924e65d0f946d2 +1.11.3:tofu_1.11.3_solaris_amd64.tar.gz:a4a88ee5bdbd0afa47488d8dabc463733a3efd5a28be57199ead6b04fdd5bd9b +1.11.3:tofu_1.11.3_windows_386.tar.gz:dad3090337bebc23bbb0d645c86a36316827d743ae26b5ecc925c33d2d454868 +1.11.3:tofu_1.11.3_windows_amd64.tar.gz:5e97b41ee5d9387f1e4532b6e2de6ae244e93f70c9d48fb46ec606c7cacb7a07" + +# Find embedded checksum for a given version and filename +find_embedded_checksum() { + version="$1" + filename="$2" + echo "$EMBEDDED_CHECKSUMS" | grep -E "^${version}:${filename}:" | cut -d':' -f3 +} +parse_args() { + BINDIR="${BINSTALLER_BIN:-${HOME}/.local/bin}" + DRY_RUN=0 + while getopts "b:dqh?xn" arg; do + case "$arg" in + b) BINDIR="$OPTARG" ;; + d) log_set_priority 10 ;; + q) log_set_priority 3 ;; + h | \?) usage "$0" ;; + x) set -x ;; + n) DRY_RUN=1 ;; + esac + done + shift $((OPTIND - 1)) + TAG="${1:-latest}" +} +tag_to_version() { + if [ "$TAG" = "latest" ]; then + log_info "checking GitHub for latest tag" + REALTAG=$(github_release "${REPO}" "${TAG}") && true + test -n "$REALTAG" || { + log_crit "Could not determine latest tag for ${REPO}" + exit 1 + } + else + # Assume TAG is a valid tag/version string + REALTAG="$TAG" + fi + if test -z "$REALTAG"; then + log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${REPO}/releases for details" + exit 1 + fi + VERSION=${REALTAG#v} # Strip leading 'v' + TAG="$REALTAG" # Use the resolved tag + log_info "Resolved version: ${VERSION} (tag: ${TAG})" +} + + + +resolve_asset_filename() { + + # --- Apply Rules --- + ASSET_FILENAME="" + if [ -z "${ASSET_FILENAME}" ]; then + ASSET_FILENAME="tofu_${VERSION}_${OS}_${ARCH}${EXT}" + fi +} +# Cleanup function to remove temporary files and stop progress +cleanup() { + # Stop progress animation + progress_clear + + if [ -n "$TMPDIR" ] && [ -d "$TMPDIR" ]; then + log_debug "Cleaning up temporary directory: $TMPDIR" + rm -rf -- "$TMPDIR" + fi +} + +execute() { + STRIP_COMPONENTS=0 + CHECKSUM_FILENAME="tofu_${VERSION}_SHA256SUMS" + + # --- Construct URLs --- + GITHUB_DOWNLOAD="https://github.com/${REPO}/releases/download" + ASSET_URL="${GITHUB_DOWNLOAD}/${TAG}/${ASSET_FILENAME}" + CHECKSUM_URL="" + if [ -n "$CHECKSUM_FILENAME" ]; then + CHECKSUM_URL="${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM_FILENAME}" + fi + + # --- Download and Verify --- + TMPDIR=$(mktemp -d) + # Set up cleanup trap that includes progress clearing + trap cleanup EXIT HUP INT TERM + log_debug "Downloading files into ${TMPDIR}" + log_info "Downloading ${ASSET_URL}" + github_http_download "${TMPDIR}/${ASSET_FILENAME}" "${ASSET_URL}" + + # Try to find embedded checksum first + EMBEDDED_HASH=$(find_embedded_checksum "$VERSION" "$ASSET_FILENAME") + + if [ -n "$EMBEDDED_HASH" ]; then + log_info "Using embedded checksum for verification" + + # Verify using embedded hash + got=$(hash_compute "${TMPDIR}/${ASSET_FILENAME}") + if [ "$got" != "$EMBEDDED_HASH" ]; then + log_crit "Checksum verification failed for ${ASSET_FILENAME}" + log_crit "Expected: ${EMBEDDED_HASH}" + log_crit "Got: ${got}" + return 1 + fi + log_info "Checksum verification successful" + elif [ -n "$CHECKSUM_URL" ]; then + # Fall back to downloading checksum file + log_info "Downloading checksums from ${CHECKSUM_URL}" + github_http_download "${TMPDIR}/${CHECKSUM_FILENAME}" "${CHECKSUM_URL}" + log_info "Verifying checksum ..." + hash_verify "${TMPDIR}/${ASSET_FILENAME}" "${TMPDIR}/${CHECKSUM_FILENAME}" + else + log_info "No checksum found, skipping verification." + fi + + if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then + log_debug "Target is raw binary" + else + log_info "Extracting ${ASSET_FILENAME}..." + (cd "${TMPDIR}" && untar "${ASSET_FILENAME}" "${STRIP_COMPONENTS}") + fi + BINARY_NAME='opentofu' + if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then + BINARY_PATH="${TMPDIR}/${ASSET_FILENAME}" + else + BINARY_PATH="${TMPDIR}/opentofu" + fi + + if [ "${UNAME_OS}" = "windows" ]; then + case "${BINARY_NAME}" in *.exe) ;; *) BINARY_NAME="${BINARY_NAME}.exe" ;; esac + case "${BINARY_PATH}" in *.exe) ;; *) BINARY_PATH="${BINARY_PATH}.exe" ;; esac + fi + + if [ ! -f "${BINARY_PATH}" ]; then + log_crit "Binary not found: ${BINARY_PATH}" + log_crit "Listing contents of ${TMPDIR} ..." + if command -v find >/dev/null 2>&1; then + cd "${TMPDIR}" && find . + else + cd "${TMPDIR}" && ls -R . + fi + return 1 + fi + + progress_clear + # Install the binary + INSTALL_PATH="${BINDIR}/${BINARY_NAME}" + + if [ "$DRY_RUN" = "1" ]; then + log_info "[DRY RUN] ${BINARY_NAME} dry-run installation succeeded! (Would install to: ${INSTALL_PATH})" + else + log_info "Installing binary to ${INSTALL_PATH}" + test ! -d "${BINDIR}" && install -d "${BINDIR}" + install "${BINARY_PATH}" "${INSTALL_PATH}" + log_info "${BINARY_NAME} installation complete!" + fi +} + +# --- Configuration --- +NAME='opentofu' +REPO='opentofu/opentofu' +EXT='.tar.gz' + +# use in logging routines +log_prefix() { + echo "${REPO}" +} + +parse_args "$@" + +progress_init +progress_pulse_start + +# --- Determine target platform --- +OS="${BINSTALLER_OS:-$(uname_os)}" +UNAME_OS="${OS}" + +ARCH="${BINSTALLER_ARCH:-$(uname_arch)}" + +log_info "Detected Platform: ${OS}/${ARCH}" + +# --- Validate platform --- +uname_os_check "$OS" +uname_arch_check "$ARCH" + +tag_to_version + +resolve_asset_filename +execute diff --git a/scripts/task-install.sh b/scripts/task-install.sh new file mode 100755 index 0000000..6e9dd93 --- /dev/null +++ b/scripts/task-install.sh @@ -0,0 +1,615 @@ +#!/bin/sh +# Code generated by binstaller. DO NOT EDIT. +# +set -e +usage() { + this=$1 + cat </dev/null +} +echoerr() { + echo "$@" 1>&2 +} +_logp=6 +log_set_priority() { + _logp="$1" +} +log_priority() { + if test -z "$1"; then + echo "$_logp" + return + fi + [ "$1" -le "$_logp" ] +} +log_tag() { + case $1 in + 0) echo "emerg" ;; + 1) echo "alert" ;; + 2) echo "crit" ;; + 3) echo "err" ;; + 4) echo "warning" ;; + 5) echo "notice" ;; + 6) echo "info" ;; + 7) echo "debug" ;; + *) echo "$1" ;; + esac +} +log_debug() { + log_priority 7 || return 0 + echoerr "$(log_prefix)" "$(log_tag 7)" "$@" +} +log_info() { + log_priority 6 || return 0 + echoerr "$(log_prefix)" "$(log_tag 6)" "$@" +} +log_err() { + log_priority 3 || return 0 + echoerr "$(log_prefix)" "$(log_tag 3)" "$@" +} +log_crit() { + log_priority 2 || return 0 + echoerr "$(log_prefix)" "$(log_tag 2)" "$@" +} +uname_os() { + os=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$os" in + msys*) os="windows" ;; + mingw*) os="windows" ;; + cygwin*) os="windows" ;; + esac + if [ "$os" = "sunos" ]; then + if [ "$(uname -o)" = "illumos" ]; then + os="illumos" + else + os="solaris" + fi + fi + echo "$os" +} +uname_arch() { + arch=$(uname -m) + case $arch in + x86_64) arch="amd64" ;; + i86pc) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; + esac + echo "${arch}" +} +uname_os_check() { + os=$(uname_os) + case "$os" in + darwin) return 0 ;; + dragonfly) return 0 ;; + freebsd) return 0 ;; + linux) return 0 ;; + android) return 0 ;; + midnightbsd) return 0 ;; + nacl) return 0 ;; + netbsd) return 0 ;; + openbsd) return 0 ;; + plan9) return 0 ;; + solaris) return 0 ;; + illumos) return 0 ;; + windows) return 0 ;; + esac + log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" + return 1 +} +uname_arch_check() { + arch=$(uname_arch) + case "$arch" in + 386) return 0 ;; + amd64) return 0 ;; + arm64) return 0 ;; + armv5) return 0 ;; + armv6) return 0 ;; + armv7) return 0 ;; + ppc64) return 0 ;; + ppc64le) return 0 ;; + mips) return 0 ;; + mipsle) return 0 ;; + mips64) return 0 ;; + mips64le) return 0 ;; + s390x) return 0 ;; + amd64p32) return 0 ;; + esac + log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" + return 1 +} +cat /dev/null </dev/null) || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command openssl; then + hash=$(openssl dgst -sha256 "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 2 + else + log_crit "hash_sha256 unable to find command to compute sha-256 hash" + return 1 + fi +} + +hash_compute() { + hash_sha256 "$1" +} + + +# shellcheck shell=sh +# Terminal progress reporting functions +progress_init() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + # OSC 9;4 sequences are safely ignored by unsupporting terminals + # Only need special handling for tmux passthrough + if [ -n "$TMUX" ]; then + # Tmux passthrough: DCS tmux; ST + # ESC characters in the wrapped sequence must be doubled + # Format: ESC P tmux; ESC ESC ] 9;4; ... ESC ESC \ ESC \ + PROGRESS_START=$(printf '\033Ptmux;\033\033]9;4;') + # shellcheck disable=SC1003 + PROGRESS_END=$(printf '\033\033\\\033\\') + else + # Direct OSC 9;4 - terminals that don't support it will safely ignore + PROGRESS_START=$(printf '\033]9;4;') + PROGRESS_END=$(printf '\007') + fi +} + +# Start pulsing progress animation +progress_pulse_start() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + + # Send OSC 9;4 with state 3 (indeterminate/pulsing) once + # The terminal will handle the continuous animation + printf "%s3%s" "$PROGRESS_START" "$PROGRESS_END" >&2 +} + +# Clear progress indicator +progress_clear() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + printf "%s0;%s" "$PROGRESS_START" "$PROGRESS_END" >&2 +} + +untar() { + tarball=$1 + strip_components=${2:-0} # default 0 + case "${tarball}" in + *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" --strip-components "${strip_components}" ;; + *.tar.xz) tar --no-same-owner -xJf "${tarball}" --strip-components "${strip_components}" ;; + *.tar.bz2) tar --no-same-owner -xjf "${tarball}" --strip-components "${strip_components}" ;; + *.tar) tar --no-same-owner -xf "${tarball}" --strip-components "${strip_components}" ;; + *.gz) gunzip "${tarball}" ;; + *.zip) + # unzip doesn't have a standard --strip-components + # Workaround: extract to a subdir and move contents up if stripping + if [ "$strip_components" -gt 0 ]; then + extract_dir=$(basename "${tarball%.zip}")_extracted + unzip -q "${tarball}" -d "${extract_dir}" + # Move contents of the *first* directory found inside extract_dir up + # This assumes wrap_in_directory=true convention + first_subdir=$(find "${extract_dir}" -mindepth 1 -maxdepth 1 -type d -print -quit) + if [ -n "$first_subdir" ]; then + # Move all contents (* includes hidden files) + mv "${first_subdir}"/* . + # Optionally remove the now-empty subdir and the extract_dir + rmdir "${first_subdir}" + rmdir "${extract_dir}" + else + log_warn "Could not find subdirectory in zip to strip components from ${extract_dir}" + # Files are extracted in current dir anyway, proceed + fi + else + unzip -q "${tarball}" + fi + ;; + *) + log_err "untar unknown archive format for ${tarball}" + return 1 + ;; + esac +} + + + +hash_verify() { + TARGET_PATH=$1 + SUMFILE=$2 + if [ -z "${SUMFILE}" ]; then + log_err "hash_verify checksum file not specified in arg2" + return 1 + fi + got=$(hash_compute "$TARGET_PATH") + if [ -z "${got}" ]; then + log_err "failed to calculate hash: ${TARGET_PATH}" + return 1 + fi + + BASENAME=${TARGET_PATH##*/} + + # Check for line matches in checksum file + # Format: " " or " *" + # Filename may include path prefix (e.g., "deployment/m2/file.tar.gz") + while IFS= read -r line || [ -n "$line" ]; do + # Normalize tabs to spaces + line=$(echo "$line" | tr '\t' ' ') + + # Remove trailing spaces for hash-only line check + line_trimmed=$(echo "$line" | sed 's/[[:space:]]*$//') + + # Check for hash-only line (no filename) - early return + if [ "$line_trimmed" = "$got" ]; then + return 0 + fi + + # Extract hash and filename parts + # First field is the hash, rest is filename (which may contain spaces) + line_hash=$(echo "$line" | cut -d' ' -f1) + + # Skip if hash doesn't match + if [ "$line_hash" != "$got" ]; then + continue + fi + + # Hash matches, now check filename + # Remove the hash part from the beginning of the line + line_rest="${line#"$got"}" + # Remove leading spaces + while [ "${line_rest#[ ]}" != "$line_rest" ]; do + line_rest="${line_rest#[ ]}" + done + + # Remove leading asterisk if present (binary mode indicator) + if [ "${line_rest#\*}" != "$line_rest" ]; then + line_rest="${line_rest#\*}" + fi + + # Extract just the filename without any path + line_filename="${line_rest##*/}" + + # Check if the filename matches + if [ "$line_filename" = "$BASENAME" ]; then + return 0 + fi + done < "$SUMFILE" + + log_err "hash_verify checksum for '$TARGET_PATH' did not verify" + log_err " Expected hash: ${got}" + log_err " Checksum file content:" + cat "$SUMFILE" >&2 + return 1 +} + +# GitHub HTTP download functions with GITHUB_TOKEN support +github_http_download_curl() { + local_file=$1 + source_url=$2 + header=$3 + if [ -n "$GITHUB_TOKEN" ]; then + log_debug "Using GITHUB_TOKEN for authentication" + if [ -z "$header" ]; then + curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -o "$local_file" "$source_url" + else + curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -H "$header" -o "$local_file" "$source_url" + fi + else + if [ -z "$header" ]; then + curl -fsSL -o "$local_file" "$source_url" + else + curl -fsSL -H "$header" -o "$local_file" "$source_url" + fi + fi +} +github_http_download_wget() { + local_file=$1 + source_url=$2 + header=$3 + if [ -n "$GITHUB_TOKEN" ]; then + log_debug "Using GITHUB_TOKEN for authentication" + if [ -z "$header" ]; then + wget -q --header "Authorization: Bearer $GITHUB_TOKEN" -O "$local_file" "$source_url" + else + wget -q --header "Authorization: Bearer $GITHUB_TOKEN" --header "$header" -O "$local_file" "$source_url" + fi + else + if [ -z "$header" ]; then + wget -q -O "$local_file" "$source_url" + else + wget -q --header "$header" -O "$local_file" "$source_url" + fi + fi +} +github_http_download() { + log_debug "github_http_download $2" + if is_command curl; then + github_http_download_curl "$@" + return + elif is_command wget; then + github_http_download_wget "$@" + return + fi + log_crit "github_http_download unable to find wget or curl" + return 1 +} +github_http_copy() { + tmp=$(mktemp) + github_http_download "${tmp}" "$@" || return 1 + body=$(cat "$tmp") + rm -f "${tmp}" + echo "$body" +} +github_release() { + owner_repo=$1 + version=$2 + test -z "$version" && version="latest" + giturl="https://github.com/${owner_repo}/releases/${version}" + json=$(github_http_copy "$giturl" "Accept:application/json") + test -z "$json" && return 1 + version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') + test -z "$version" && return 1 + echo "$version" +} + +# --- Embedded Checksums (Format: VERSION:FILENAME:HASH) --- +EMBEDDED_CHECKSUMS=" +3.46.4:task_darwin_amd64.tar.gz:6e3d1bf5fcc03e50b2c02c92335923b715d3f61e27fef5b3048b59ed97a18721 +3.46.4:task_darwin_arm64.tar.gz:ffe2b4a3c54467172ab15734d0b451b1fc0472d0d48bb3538870b433c700bc96 +3.46.4:task_freebsd_386.tar.gz:37efbab3e10723fbba85114ffb5cd39938ed489b69674e15605ec96989c047b5 +3.46.4:task_freebsd_amd64.tar.gz:cd0be727b17943a53004c9984526649010aa7710cd4fac4a84b805ae25f1f35c +3.46.4:task_freebsd_arm.tar.gz:f267218e5ae49f7bbbe41c94377b3c0694c79411b9180fccc21b43cc8ef45284 +3.46.4:task_freebsd_arm64.tar.gz:e68251346bc140f367f42a34fdc327edb63612a66bd782cd713a1c812531a13c +3.46.4:task_linux_386.tar.gz:b0e93c6290ed91af882516117114999ff0b21ac08d8c4e7e6f63ed6a12ef95b5 +3.46.4:task_linux_amd64.tar.gz:4b0098862292de03d568c851b207bce5fdb8a59cb533d6ed4132386151926c46 +3.46.4:task_linux_arm.tar.gz:f08d96178a6612e6c548e103c022740c82a313d04f7f90421d7550356ac8ee02 +3.46.4:task_linux_arm64.tar.gz:9079bbdfd439f2168ee380d9167fd77f13f9fab7afb516205ac3a578f16bcb59 +3.46.4:task_linux_riscv64.tar.gz:c05fb7d47c0c5d1b8222362bf2edaae6cbe6d33c3b38f0ec034306425ee51761 +3.46.4:task_windows_386.zip:1398d3b96f5b7dfb71b2a126dbb5849369da130099783ebd899a162ff091f6a3 +3.46.4:task_windows_amd64.zip:790420c11a6457a27f135c292371d6a352436dfce7dba5b5ffa8ae399ecfe04b +3.46.4:task_windows_arm.zip:450bf906119ddf741e99c44569329b9493c4b6f55f4c3d736297b3d57826fb32 +3.46.4:task_windows_arm64.zip:4ef119dfc6ed5133170f6fc20085f0602701a37c90b440ee54331b6370f2914d" + +# Find embedded checksum for a given version and filename +find_embedded_checksum() { + version="$1" + filename="$2" + echo "$EMBEDDED_CHECKSUMS" | grep -E "^${version}:${filename}:" | cut -d':' -f3 +} +parse_args() { + BINDIR="${BINSTALLER_BIN:-${HOME}/.local/bin}" + DRY_RUN=0 + while getopts "b:dqh?xn" arg; do + case "$arg" in + b) BINDIR="$OPTARG" ;; + d) log_set_priority 10 ;; + q) log_set_priority 3 ;; + h | \?) usage "$0" ;; + x) set -x ;; + n) DRY_RUN=1 ;; + esac + done + shift $((OPTIND - 1)) + TAG="${1:-latest}" +} +tag_to_version() { + if [ "$TAG" = "latest" ]; then + log_info "checking GitHub for latest tag" + REALTAG=$(github_release "${REPO}" "${TAG}") && true + test -n "$REALTAG" || { + log_crit "Could not determine latest tag for ${REPO}" + exit 1 + } + else + # Assume TAG is a valid tag/version string + REALTAG="$TAG" + fi + if test -z "$REALTAG"; then + log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${REPO}/releases for details" + exit 1 + fi + VERSION=${REALTAG#v} # Strip leading 'v' + TAG="$REALTAG" # Use the resolved tag + log_info "Resolved version: ${VERSION} (tag: ${TAG})" +} + + + +resolve_asset_filename() { + + # --- Apply Rules --- + ASSET_FILENAME="" + if [ "${UNAME_OS}" = 'windows' ] && true + then + EXT='.zip' + fi + if [ -z "${ASSET_FILENAME}" ]; then + ASSET_FILENAME="task_${OS}_${ARCH}${EXT}" + fi +} +# Cleanup function to remove temporary files and stop progress +cleanup() { + # Stop progress animation + progress_clear + + if [ -n "$TMPDIR" ] && [ -d "$TMPDIR" ]; then + log_debug "Cleaning up temporary directory: $TMPDIR" + rm -rf -- "$TMPDIR" + fi +} + +execute() { + STRIP_COMPONENTS=0 + CHECKSUM_FILENAME="task_checksums.txt" + + # --- Construct URLs --- + GITHUB_DOWNLOAD="https://github.com/${REPO}/releases/download" + ASSET_URL="${GITHUB_DOWNLOAD}/${TAG}/${ASSET_FILENAME}" + CHECKSUM_URL="" + if [ -n "$CHECKSUM_FILENAME" ]; then + CHECKSUM_URL="${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM_FILENAME}" + fi + + # --- Download and Verify --- + TMPDIR=$(mktemp -d) + # Set up cleanup trap that includes progress clearing + trap cleanup EXIT HUP INT TERM + log_debug "Downloading files into ${TMPDIR}" + log_info "Downloading ${ASSET_URL}" + github_http_download "${TMPDIR}/${ASSET_FILENAME}" "${ASSET_URL}" + + # Try to find embedded checksum first + EMBEDDED_HASH=$(find_embedded_checksum "$VERSION" "$ASSET_FILENAME") + + if [ -n "$EMBEDDED_HASH" ]; then + log_info "Using embedded checksum for verification" + + # Verify using embedded hash + got=$(hash_compute "${TMPDIR}/${ASSET_FILENAME}") + if [ "$got" != "$EMBEDDED_HASH" ]; then + log_crit "Checksum verification failed for ${ASSET_FILENAME}" + log_crit "Expected: ${EMBEDDED_HASH}" + log_crit "Got: ${got}" + return 1 + fi + log_info "Checksum verification successful" + elif [ -n "$CHECKSUM_URL" ]; then + # Fall back to downloading checksum file + log_info "Downloading checksums from ${CHECKSUM_URL}" + github_http_download "${TMPDIR}/${CHECKSUM_FILENAME}" "${CHECKSUM_URL}" + log_info "Verifying checksum ..." + hash_verify "${TMPDIR}/${ASSET_FILENAME}" "${TMPDIR}/${CHECKSUM_FILENAME}" + else + log_info "No checksum found, skipping verification." + fi + + if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then + log_debug "Target is raw binary" + else + log_info "Extracting ${ASSET_FILENAME}..." + (cd "${TMPDIR}" && untar "${ASSET_FILENAME}" "${STRIP_COMPONENTS}") + fi + BINARY_NAME='task' + if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then + BINARY_PATH="${TMPDIR}/${ASSET_FILENAME}" + else + BINARY_PATH="${TMPDIR}/task" + fi + + if [ "${UNAME_OS}" = "windows" ]; then + case "${BINARY_NAME}" in *.exe) ;; *) BINARY_NAME="${BINARY_NAME}.exe" ;; esac + case "${BINARY_PATH}" in *.exe) ;; *) BINARY_PATH="${BINARY_PATH}.exe" ;; esac + fi + + if [ ! -f "${BINARY_PATH}" ]; then + log_crit "Binary not found: ${BINARY_PATH}" + log_crit "Listing contents of ${TMPDIR} ..." + if command -v find >/dev/null 2>&1; then + cd "${TMPDIR}" && find . + else + cd "${TMPDIR}" && ls -R . + fi + return 1 + fi + + progress_clear + # Install the binary + INSTALL_PATH="${BINDIR}/${BINARY_NAME}" + + if [ "$DRY_RUN" = "1" ]; then + log_info "[DRY RUN] ${BINARY_NAME} dry-run installation succeeded! (Would install to: ${INSTALL_PATH})" + else + log_info "Installing binary to ${INSTALL_PATH}" + test ! -d "${BINDIR}" && install -d "${BINDIR}" + install "${BINARY_PATH}" "${INSTALL_PATH}" + log_info "${BINARY_NAME} installation complete!" + fi +} + +# --- Configuration --- +NAME='task' +REPO='go-task/task' +EXT='.tar.gz' + +# use in logging routines +log_prefix() { + echo "${REPO}" +} + +parse_args "$@" + +progress_init +progress_pulse_start + +# --- Determine target platform --- +OS="${BINSTALLER_OS:-$(uname_os)}" +UNAME_OS="${OS}" + +ARCH="${BINSTALLER_ARCH:-$(uname_arch)}" + +log_info "Detected Platform: ${OS}/${ARCH}" + +# --- Validate platform --- +uname_os_check "$OS" +uname_arch_check "$ARCH" + +tag_to_version + +resolve_asset_filename +execute diff --git a/scripts/toolhive-install.sh b/scripts/toolhive-install.sh new file mode 100755 index 0000000..6236709 --- /dev/null +++ b/scripts/toolhive-install.sh @@ -0,0 +1,606 @@ +#!/bin/sh +# Code generated by binstaller. DO NOT EDIT. +# +set -e +usage() { + this=$1 + cat </dev/null +} +echoerr() { + echo "$@" 1>&2 +} +_logp=6 +log_set_priority() { + _logp="$1" +} +log_priority() { + if test -z "$1"; then + echo "$_logp" + return + fi + [ "$1" -le "$_logp" ] +} +log_tag() { + case $1 in + 0) echo "emerg" ;; + 1) echo "alert" ;; + 2) echo "crit" ;; + 3) echo "err" ;; + 4) echo "warning" ;; + 5) echo "notice" ;; + 6) echo "info" ;; + 7) echo "debug" ;; + *) echo "$1" ;; + esac +} +log_debug() { + log_priority 7 || return 0 + echoerr "$(log_prefix)" "$(log_tag 7)" "$@" +} +log_info() { + log_priority 6 || return 0 + echoerr "$(log_prefix)" "$(log_tag 6)" "$@" +} +log_err() { + log_priority 3 || return 0 + echoerr "$(log_prefix)" "$(log_tag 3)" "$@" +} +log_crit() { + log_priority 2 || return 0 + echoerr "$(log_prefix)" "$(log_tag 2)" "$@" +} +uname_os() { + os=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$os" in + msys*) os="windows" ;; + mingw*) os="windows" ;; + cygwin*) os="windows" ;; + esac + if [ "$os" = "sunos" ]; then + if [ "$(uname -o)" = "illumos" ]; then + os="illumos" + else + os="solaris" + fi + fi + echo "$os" +} +uname_arch() { + arch=$(uname -m) + case $arch in + x86_64) arch="amd64" ;; + i86pc) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; + esac + echo "${arch}" +} +uname_os_check() { + os=$(uname_os) + case "$os" in + darwin) return 0 ;; + dragonfly) return 0 ;; + freebsd) return 0 ;; + linux) return 0 ;; + android) return 0 ;; + midnightbsd) return 0 ;; + nacl) return 0 ;; + netbsd) return 0 ;; + openbsd) return 0 ;; + plan9) return 0 ;; + solaris) return 0 ;; + illumos) return 0 ;; + windows) return 0 ;; + esac + log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" + return 1 +} +uname_arch_check() { + arch=$(uname_arch) + case "$arch" in + 386) return 0 ;; + amd64) return 0 ;; + arm64) return 0 ;; + armv5) return 0 ;; + armv6) return 0 ;; + armv7) return 0 ;; + ppc64) return 0 ;; + ppc64le) return 0 ;; + mips) return 0 ;; + mipsle) return 0 ;; + mips64) return 0 ;; + mips64le) return 0 ;; + s390x) return 0 ;; + amd64p32) return 0 ;; + esac + log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" + return 1 +} +cat /dev/null </dev/null) || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command openssl; then + hash=$(openssl dgst -sha256 "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 2 + else + log_crit "hash_sha256 unable to find command to compute sha-256 hash" + return 1 + fi +} + +hash_compute() { + hash_sha256 "$1" +} + + +# shellcheck shell=sh +# Terminal progress reporting functions +progress_init() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + # OSC 9;4 sequences are safely ignored by unsupporting terminals + # Only need special handling for tmux passthrough + if [ -n "$TMUX" ]; then + # Tmux passthrough: DCS tmux; ST + # ESC characters in the wrapped sequence must be doubled + # Format: ESC P tmux; ESC ESC ] 9;4; ... ESC ESC \ ESC \ + PROGRESS_START=$(printf '\033Ptmux;\033\033]9;4;') + # shellcheck disable=SC1003 + PROGRESS_END=$(printf '\033\033\\\033\\') + else + # Direct OSC 9;4 - terminals that don't support it will safely ignore + PROGRESS_START=$(printf '\033]9;4;') + PROGRESS_END=$(printf '\007') + fi +} + +# Start pulsing progress animation +progress_pulse_start() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + + # Send OSC 9;4 with state 3 (indeterminate/pulsing) once + # The terminal will handle the continuous animation + printf "%s3%s" "$PROGRESS_START" "$PROGRESS_END" >&2 +} + +# Clear progress indicator +progress_clear() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + printf "%s0;%s" "$PROGRESS_START" "$PROGRESS_END" >&2 +} + +untar() { + tarball=$1 + strip_components=${2:-0} # default 0 + case "${tarball}" in + *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" --strip-components "${strip_components}" ;; + *.tar.xz) tar --no-same-owner -xJf "${tarball}" --strip-components "${strip_components}" ;; + *.tar.bz2) tar --no-same-owner -xjf "${tarball}" --strip-components "${strip_components}" ;; + *.tar) tar --no-same-owner -xf "${tarball}" --strip-components "${strip_components}" ;; + *.gz) gunzip "${tarball}" ;; + *.zip) + # unzip doesn't have a standard --strip-components + # Workaround: extract to a subdir and move contents up if stripping + if [ "$strip_components" -gt 0 ]; then + extract_dir=$(basename "${tarball%.zip}")_extracted + unzip -q "${tarball}" -d "${extract_dir}" + # Move contents of the *first* directory found inside extract_dir up + # This assumes wrap_in_directory=true convention + first_subdir=$(find "${extract_dir}" -mindepth 1 -maxdepth 1 -type d -print -quit) + if [ -n "$first_subdir" ]; then + # Move all contents (* includes hidden files) + mv "${first_subdir}"/* . + # Optionally remove the now-empty subdir and the extract_dir + rmdir "${first_subdir}" + rmdir "${extract_dir}" + else + log_warn "Could not find subdirectory in zip to strip components from ${extract_dir}" + # Files are extracted in current dir anyway, proceed + fi + else + unzip -q "${tarball}" + fi + ;; + *) + log_err "untar unknown archive format for ${tarball}" + return 1 + ;; + esac +} + + + +hash_verify() { + TARGET_PATH=$1 + SUMFILE=$2 + if [ -z "${SUMFILE}" ]; then + log_err "hash_verify checksum file not specified in arg2" + return 1 + fi + got=$(hash_compute "$TARGET_PATH") + if [ -z "${got}" ]; then + log_err "failed to calculate hash: ${TARGET_PATH}" + return 1 + fi + + BASENAME=${TARGET_PATH##*/} + + # Check for line matches in checksum file + # Format: " " or " *" + # Filename may include path prefix (e.g., "deployment/m2/file.tar.gz") + while IFS= read -r line || [ -n "$line" ]; do + # Normalize tabs to spaces + line=$(echo "$line" | tr '\t' ' ') + + # Remove trailing spaces for hash-only line check + line_trimmed=$(echo "$line" | sed 's/[[:space:]]*$//') + + # Check for hash-only line (no filename) - early return + if [ "$line_trimmed" = "$got" ]; then + return 0 + fi + + # Extract hash and filename parts + # First field is the hash, rest is filename (which may contain spaces) + line_hash=$(echo "$line" | cut -d' ' -f1) + + # Skip if hash doesn't match + if [ "$line_hash" != "$got" ]; then + continue + fi + + # Hash matches, now check filename + # Remove the hash part from the beginning of the line + line_rest="${line#"$got"}" + # Remove leading spaces + while [ "${line_rest#[ ]}" != "$line_rest" ]; do + line_rest="${line_rest#[ ]}" + done + + # Remove leading asterisk if present (binary mode indicator) + if [ "${line_rest#\*}" != "$line_rest" ]; then + line_rest="${line_rest#\*}" + fi + + # Extract just the filename without any path + line_filename="${line_rest##*/}" + + # Check if the filename matches + if [ "$line_filename" = "$BASENAME" ]; then + return 0 + fi + done < "$SUMFILE" + + log_err "hash_verify checksum for '$TARGET_PATH' did not verify" + log_err " Expected hash: ${got}" + log_err " Checksum file content:" + cat "$SUMFILE" >&2 + return 1 +} + +# GitHub HTTP download functions with GITHUB_TOKEN support +github_http_download_curl() { + local_file=$1 + source_url=$2 + header=$3 + if [ -n "$GITHUB_TOKEN" ]; then + log_debug "Using GITHUB_TOKEN for authentication" + if [ -z "$header" ]; then + curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -o "$local_file" "$source_url" + else + curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -H "$header" -o "$local_file" "$source_url" + fi + else + if [ -z "$header" ]; then + curl -fsSL -o "$local_file" "$source_url" + else + curl -fsSL -H "$header" -o "$local_file" "$source_url" + fi + fi +} +github_http_download_wget() { + local_file=$1 + source_url=$2 + header=$3 + if [ -n "$GITHUB_TOKEN" ]; then + log_debug "Using GITHUB_TOKEN for authentication" + if [ -z "$header" ]; then + wget -q --header "Authorization: Bearer $GITHUB_TOKEN" -O "$local_file" "$source_url" + else + wget -q --header "Authorization: Bearer $GITHUB_TOKEN" --header "$header" -O "$local_file" "$source_url" + fi + else + if [ -z "$header" ]; then + wget -q -O "$local_file" "$source_url" + else + wget -q --header "$header" -O "$local_file" "$source_url" + fi + fi +} +github_http_download() { + log_debug "github_http_download $2" + if is_command curl; then + github_http_download_curl "$@" + return + elif is_command wget; then + github_http_download_wget "$@" + return + fi + log_crit "github_http_download unable to find wget or curl" + return 1 +} +github_http_copy() { + tmp=$(mktemp) + github_http_download "${tmp}" "$@" || return 1 + body=$(cat "$tmp") + rm -f "${tmp}" + echo "$body" +} +github_release() { + owner_repo=$1 + version=$2 + test -z "$version" && version="latest" + giturl="https://github.com/${owner_repo}/releases/${version}" + json=$(github_http_copy "$giturl" "Accept:application/json") + test -z "$json" && return 1 + version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') + test -z "$version" && return 1 + echo "$version" +} + +# --- Embedded Checksums (Format: VERSION:FILENAME:HASH) --- +EMBEDDED_CHECKSUMS=" +0.7.2:toolhive_0.7.2_darwin_amd64.tar.gz:a32b18485d33acefc0d69641a8f99e43c9d91ed45c6279cf158f220763840ef5 +0.7.2:toolhive_0.7.2_darwin_arm64.tar.gz:20b3fd98df6abdbd4898c6aa1f2fa7d01f58901055f85c374cccf110a014be1d +0.7.2:toolhive_0.7.2_linux_amd64.tar.gz:84a89a735f3c5d7eaa4fe0f4cba164eb2a04b83f2525c9709dc60dc86fc9dab1 +0.7.2:toolhive_0.7.2_linux_arm64.tar.gz:821fb80c672701b62bf3efcb4c744c685fbbde80845b6256d2116a4fe561ac21 +0.7.2:toolhive_0.7.2_windows_amd64.zip:d6d639453938383a3399888bbe0a60449ae1a5aa196b9cd2187a1ef089b4beb6 +0.7.2:toolhive_0.7.2_windows_arm64.zip:d291feff936764db8a02868461431bd54a840ecac7a78b303e30956609c54631" + +# Find embedded checksum for a given version and filename +find_embedded_checksum() { + version="$1" + filename="$2" + echo "$EMBEDDED_CHECKSUMS" | grep -E "^${version}:${filename}:" | cut -d':' -f3 +} +parse_args() { + BINDIR="${BINSTALLER_BIN:-${HOME}/.local/bin}" + DRY_RUN=0 + while getopts "b:dqh?xn" arg; do + case "$arg" in + b) BINDIR="$OPTARG" ;; + d) log_set_priority 10 ;; + q) log_set_priority 3 ;; + h | \?) usage "$0" ;; + x) set -x ;; + n) DRY_RUN=1 ;; + esac + done + shift $((OPTIND - 1)) + TAG="${1:-latest}" +} +tag_to_version() { + if [ "$TAG" = "latest" ]; then + log_info "checking GitHub for latest tag" + REALTAG=$(github_release "${REPO}" "${TAG}") && true + test -n "$REALTAG" || { + log_crit "Could not determine latest tag for ${REPO}" + exit 1 + } + else + # Assume TAG is a valid tag/version string + REALTAG="$TAG" + fi + if test -z "$REALTAG"; then + log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${REPO}/releases for details" + exit 1 + fi + VERSION=${REALTAG#v} # Strip leading 'v' + TAG="$REALTAG" # Use the resolved tag + log_info "Resolved version: ${VERSION} (tag: ${TAG})" +} + + + +resolve_asset_filename() { + + # --- Apply Rules --- + ASSET_FILENAME="" + if [ "${UNAME_OS}" = 'windows' ] && true + then + EXT='.zip' + fi + if [ -z "${ASSET_FILENAME}" ]; then + ASSET_FILENAME="toolhive_${VERSION}_${OS}_${ARCH}${EXT}" + fi +} +# Cleanup function to remove temporary files and stop progress +cleanup() { + # Stop progress animation + progress_clear + + if [ -n "$TMPDIR" ] && [ -d "$TMPDIR" ]; then + log_debug "Cleaning up temporary directory: $TMPDIR" + rm -rf -- "$TMPDIR" + fi +} + +execute() { + STRIP_COMPONENTS=0 + CHECKSUM_FILENAME="toolhive_${VERSION}_checksums.txt" + + # --- Construct URLs --- + GITHUB_DOWNLOAD="https://github.com/${REPO}/releases/download" + ASSET_URL="${GITHUB_DOWNLOAD}/${TAG}/${ASSET_FILENAME}" + CHECKSUM_URL="" + if [ -n "$CHECKSUM_FILENAME" ]; then + CHECKSUM_URL="${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM_FILENAME}" + fi + + # --- Download and Verify --- + TMPDIR=$(mktemp -d) + # Set up cleanup trap that includes progress clearing + trap cleanup EXIT HUP INT TERM + log_debug "Downloading files into ${TMPDIR}" + log_info "Downloading ${ASSET_URL}" + github_http_download "${TMPDIR}/${ASSET_FILENAME}" "${ASSET_URL}" + + # Try to find embedded checksum first + EMBEDDED_HASH=$(find_embedded_checksum "$VERSION" "$ASSET_FILENAME") + + if [ -n "$EMBEDDED_HASH" ]; then + log_info "Using embedded checksum for verification" + + # Verify using embedded hash + got=$(hash_compute "${TMPDIR}/${ASSET_FILENAME}") + if [ "$got" != "$EMBEDDED_HASH" ]; then + log_crit "Checksum verification failed for ${ASSET_FILENAME}" + log_crit "Expected: ${EMBEDDED_HASH}" + log_crit "Got: ${got}" + return 1 + fi + log_info "Checksum verification successful" + elif [ -n "$CHECKSUM_URL" ]; then + # Fall back to downloading checksum file + log_info "Downloading checksums from ${CHECKSUM_URL}" + github_http_download "${TMPDIR}/${CHECKSUM_FILENAME}" "${CHECKSUM_URL}" + log_info "Verifying checksum ..." + hash_verify "${TMPDIR}/${ASSET_FILENAME}" "${TMPDIR}/${CHECKSUM_FILENAME}" + else + log_info "No checksum found, skipping verification." + fi + + if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then + log_debug "Target is raw binary" + else + log_info "Extracting ${ASSET_FILENAME}..." + (cd "${TMPDIR}" && untar "${ASSET_FILENAME}" "${STRIP_COMPONENTS}") + fi + BINARY_NAME='thv' + if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then + BINARY_PATH="${TMPDIR}/${ASSET_FILENAME}" + else + BINARY_PATH="${TMPDIR}/thv" + fi + + if [ "${UNAME_OS}" = "windows" ]; then + case "${BINARY_NAME}" in *.exe) ;; *) BINARY_NAME="${BINARY_NAME}.exe" ;; esac + case "${BINARY_PATH}" in *.exe) ;; *) BINARY_PATH="${BINARY_PATH}.exe" ;; esac + fi + + if [ ! -f "${BINARY_PATH}" ]; then + log_crit "Binary not found: ${BINARY_PATH}" + log_crit "Listing contents of ${TMPDIR} ..." + if command -v find >/dev/null 2>&1; then + cd "${TMPDIR}" && find . + else + cd "${TMPDIR}" && ls -R . + fi + return 1 + fi + + progress_clear + # Install the binary + INSTALL_PATH="${BINDIR}/${BINARY_NAME}" + + if [ "$DRY_RUN" = "1" ]; then + log_info "[DRY RUN] ${BINARY_NAME} dry-run installation succeeded! (Would install to: ${INSTALL_PATH})" + else + log_info "Installing binary to ${INSTALL_PATH}" + test ! -d "${BINDIR}" && install -d "${BINDIR}" + install "${BINARY_PATH}" "${INSTALL_PATH}" + log_info "${BINARY_NAME} installation complete!" + fi +} + +# --- Configuration --- +NAME='toolhive' +REPO='stacklok/toolhive' +EXT='.tar.gz' + +# use in logging routines +log_prefix() { + echo "${REPO}" +} + +parse_args "$@" + +progress_init +progress_pulse_start + +# --- Determine target platform --- +OS="${BINSTALLER_OS:-$(uname_os)}" +UNAME_OS="${OS}" + +ARCH="${BINSTALLER_ARCH:-$(uname_arch)}" + +log_info "Detected Platform: ${OS}/${ARCH}" + +# --- Validate platform --- +uname_os_check "$OS" +uname_arch_check "$ARCH" + +tag_to_version + +resolve_asset_filename +execute diff --git a/scripts/zk-install.sh b/scripts/zk-install.sh new file mode 100755 index 0000000..78b5a5a --- /dev/null +++ b/scripts/zk-install.sh @@ -0,0 +1,604 @@ +#!/bin/sh +# Code generated by binstaller. DO NOT EDIT. +# +set -e +usage() { + this=$1 + cat </dev/null +} +echoerr() { + echo "$@" 1>&2 +} +_logp=6 +log_set_priority() { + _logp="$1" +} +log_priority() { + if test -z "$1"; then + echo "$_logp" + return + fi + [ "$1" -le "$_logp" ] +} +log_tag() { + case $1 in + 0) echo "emerg" ;; + 1) echo "alert" ;; + 2) echo "crit" ;; + 3) echo "err" ;; + 4) echo "warning" ;; + 5) echo "notice" ;; + 6) echo "info" ;; + 7) echo "debug" ;; + *) echo "$1" ;; + esac +} +log_debug() { + log_priority 7 || return 0 + echoerr "$(log_prefix)" "$(log_tag 7)" "$@" +} +log_info() { + log_priority 6 || return 0 + echoerr "$(log_prefix)" "$(log_tag 6)" "$@" +} +log_err() { + log_priority 3 || return 0 + echoerr "$(log_prefix)" "$(log_tag 3)" "$@" +} +log_crit() { + log_priority 2 || return 0 + echoerr "$(log_prefix)" "$(log_tag 2)" "$@" +} +uname_os() { + os=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$os" in + msys*) os="windows" ;; + mingw*) os="windows" ;; + cygwin*) os="windows" ;; + esac + if [ "$os" = "sunos" ]; then + if [ "$(uname -o)" = "illumos" ]; then + os="illumos" + else + os="solaris" + fi + fi + echo "$os" +} +uname_arch() { + arch=$(uname -m) + case $arch in + x86_64) arch="amd64" ;; + i86pc) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; + esac + echo "${arch}" +} +uname_os_check() { + os=$(uname_os) + case "$os" in + darwin) return 0 ;; + dragonfly) return 0 ;; + freebsd) return 0 ;; + linux) return 0 ;; + android) return 0 ;; + midnightbsd) return 0 ;; + nacl) return 0 ;; + netbsd) return 0 ;; + openbsd) return 0 ;; + plan9) return 0 ;; + solaris) return 0 ;; + illumos) return 0 ;; + windows) return 0 ;; + esac + log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" + return 1 +} +uname_arch_check() { + arch=$(uname_arch) + case "$arch" in + 386) return 0 ;; + amd64) return 0 ;; + arm64) return 0 ;; + armv5) return 0 ;; + armv6) return 0 ;; + armv7) return 0 ;; + ppc64) return 0 ;; + ppc64le) return 0 ;; + mips) return 0 ;; + mipsle) return 0 ;; + mips64) return 0 ;; + mips64le) return 0 ;; + s390x) return 0 ;; + amd64p32) return 0 ;; + esac + log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" + return 1 +} +cat /dev/null </dev/null) || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command openssl; then + hash=$(openssl dgst -sha256 "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 2 + else + log_crit "hash_sha256 unable to find command to compute sha-256 hash" + return 1 + fi +} + +hash_compute() { + hash_sha256 "$1" +} + + +# shellcheck shell=sh +# Terminal progress reporting functions +progress_init() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + # OSC 9;4 sequences are safely ignored by unsupporting terminals + # Only need special handling for tmux passthrough + if [ -n "$TMUX" ]; then + # Tmux passthrough: DCS tmux; ST + # ESC characters in the wrapped sequence must be doubled + # Format: ESC P tmux; ESC ESC ] 9;4; ... ESC ESC \ ESC \ + PROGRESS_START=$(printf '\033Ptmux;\033\033]9;4;') + # shellcheck disable=SC1003 + PROGRESS_END=$(printf '\033\033\\\033\\') + else + # Direct OSC 9;4 - terminals that don't support it will safely ignore + PROGRESS_START=$(printf '\033]9;4;') + PROGRESS_END=$(printf '\007') + fi +} + +# Start pulsing progress animation +progress_pulse_start() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + + # Send OSC 9;4 with state 3 (indeterminate/pulsing) once + # The terminal will handle the continuous animation + printf "%s3%s" "$PROGRESS_START" "$PROGRESS_END" >&2 +} + +# Clear progress indicator +progress_clear() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + printf "%s0;%s" "$PROGRESS_START" "$PROGRESS_END" >&2 +} + +untar() { + tarball=$1 + strip_components=${2:-0} # default 0 + case "${tarball}" in + *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" --strip-components "${strip_components}" ;; + *.tar.xz) tar --no-same-owner -xJf "${tarball}" --strip-components "${strip_components}" ;; + *.tar.bz2) tar --no-same-owner -xjf "${tarball}" --strip-components "${strip_components}" ;; + *.tar) tar --no-same-owner -xf "${tarball}" --strip-components "${strip_components}" ;; + *.gz) gunzip "${tarball}" ;; + *.zip) + # unzip doesn't have a standard --strip-components + # Workaround: extract to a subdir and move contents up if stripping + if [ "$strip_components" -gt 0 ]; then + extract_dir=$(basename "${tarball%.zip}")_extracted + unzip -q "${tarball}" -d "${extract_dir}" + # Move contents of the *first* directory found inside extract_dir up + # This assumes wrap_in_directory=true convention + first_subdir=$(find "${extract_dir}" -mindepth 1 -maxdepth 1 -type d -print -quit) + if [ -n "$first_subdir" ]; then + # Move all contents (* includes hidden files) + mv "${first_subdir}"/* . + # Optionally remove the now-empty subdir and the extract_dir + rmdir "${first_subdir}" + rmdir "${extract_dir}" + else + log_warn "Could not find subdirectory in zip to strip components from ${extract_dir}" + # Files are extracted in current dir anyway, proceed + fi + else + unzip -q "${tarball}" + fi + ;; + *) + log_err "untar unknown archive format for ${tarball}" + return 1 + ;; + esac +} + + + +hash_verify() { + TARGET_PATH=$1 + SUMFILE=$2 + if [ -z "${SUMFILE}" ]; then + log_err "hash_verify checksum file not specified in arg2" + return 1 + fi + got=$(hash_compute "$TARGET_PATH") + if [ -z "${got}" ]; then + log_err "failed to calculate hash: ${TARGET_PATH}" + return 1 + fi + + BASENAME=${TARGET_PATH##*/} + + # Check for line matches in checksum file + # Format: " " or " *" + # Filename may include path prefix (e.g., "deployment/m2/file.tar.gz") + while IFS= read -r line || [ -n "$line" ]; do + # Normalize tabs to spaces + line=$(echo "$line" | tr '\t' ' ') + + # Remove trailing spaces for hash-only line check + line_trimmed=$(echo "$line" | sed 's/[[:space:]]*$//') + + # Check for hash-only line (no filename) - early return + if [ "$line_trimmed" = "$got" ]; then + return 0 + fi + + # Extract hash and filename parts + # First field is the hash, rest is filename (which may contain spaces) + line_hash=$(echo "$line" | cut -d' ' -f1) + + # Skip if hash doesn't match + if [ "$line_hash" != "$got" ]; then + continue + fi + + # Hash matches, now check filename + # Remove the hash part from the beginning of the line + line_rest="${line#"$got"}" + # Remove leading spaces + while [ "${line_rest#[ ]}" != "$line_rest" ]; do + line_rest="${line_rest#[ ]}" + done + + # Remove leading asterisk if present (binary mode indicator) + if [ "${line_rest#\*}" != "$line_rest" ]; then + line_rest="${line_rest#\*}" + fi + + # Extract just the filename without any path + line_filename="${line_rest##*/}" + + # Check if the filename matches + if [ "$line_filename" = "$BASENAME" ]; then + return 0 + fi + done < "$SUMFILE" + + log_err "hash_verify checksum for '$TARGET_PATH' did not verify" + log_err " Expected hash: ${got}" + log_err " Checksum file content:" + cat "$SUMFILE" >&2 + return 1 +} + +# GitHub HTTP download functions with GITHUB_TOKEN support +github_http_download_curl() { + local_file=$1 + source_url=$2 + header=$3 + if [ -n "$GITHUB_TOKEN" ]; then + log_debug "Using GITHUB_TOKEN for authentication" + if [ -z "$header" ]; then + curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -o "$local_file" "$source_url" + else + curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -H "$header" -o "$local_file" "$source_url" + fi + else + if [ -z "$header" ]; then + curl -fsSL -o "$local_file" "$source_url" + else + curl -fsSL -H "$header" -o "$local_file" "$source_url" + fi + fi +} +github_http_download_wget() { + local_file=$1 + source_url=$2 + header=$3 + if [ -n "$GITHUB_TOKEN" ]; then + log_debug "Using GITHUB_TOKEN for authentication" + if [ -z "$header" ]; then + wget -q --header "Authorization: Bearer $GITHUB_TOKEN" -O "$local_file" "$source_url" + else + wget -q --header "Authorization: Bearer $GITHUB_TOKEN" --header "$header" -O "$local_file" "$source_url" + fi + else + if [ -z "$header" ]; then + wget -q -O "$local_file" "$source_url" + else + wget -q --header "$header" -O "$local_file" "$source_url" + fi + fi +} +github_http_download() { + log_debug "github_http_download $2" + if is_command curl; then + github_http_download_curl "$@" + return + elif is_command wget; then + github_http_download_wget "$@" + return + fi + log_crit "github_http_download unable to find wget or curl" + return 1 +} +github_http_copy() { + tmp=$(mktemp) + github_http_download "${tmp}" "$@" || return 1 + body=$(cat "$tmp") + rm -f "${tmp}" + echo "$body" +} +github_release() { + owner_repo=$1 + version=$2 + test -z "$version" && version="latest" + giturl="https://github.com/${owner_repo}/releases/${version}" + json=$(github_http_copy "$giturl" "Accept:application/json") + test -z "$json" && return 1 + version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') + test -z "$version" && return 1 + echo "$version" +} + +# --- Embedded Checksums (Format: VERSION:FILENAME:HASH) --- +EMBEDDED_CHECKSUMS="" + +# Find embedded checksum for a given version and filename +find_embedded_checksum() { + version="$1" + filename="$2" + echo "$EMBEDDED_CHECKSUMS" | grep -E "^${version}:${filename}:" | cut -d':' -f3 +} +parse_args() { + BINDIR="${BINSTALLER_BIN:-${HOME}/.local/bin}" + DRY_RUN=0 + while getopts "b:dqh?xn" arg; do + case "$arg" in + b) BINDIR="$OPTARG" ;; + d) log_set_priority 10 ;; + q) log_set_priority 3 ;; + h | \?) usage "$0" ;; + x) set -x ;; + n) DRY_RUN=1 ;; + esac + done + shift $((OPTIND - 1)) + TAG="${1:-latest}" +} +tag_to_version() { + if [ "$TAG" = "latest" ]; then + log_info "checking GitHub for latest tag" + REALTAG=$(github_release "${REPO}" "${TAG}") && true + test -n "$REALTAG" || { + log_crit "Could not determine latest tag for ${REPO}" + exit 1 + } + else + # Assume TAG is a valid tag/version string + REALTAG="$TAG" + fi + if test -z "$REALTAG"; then + log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${REPO}/releases for details" + exit 1 + fi + VERSION=${REALTAG#v} # Strip leading 'v' + TAG="$REALTAG" # Use the resolved tag + log_info "Resolved version: ${VERSION} (tag: ${TAG})" +} + + + +resolve_asset_filename() { + + # --- Apply Rules --- + ASSET_FILENAME="" + if [ "${UNAME_ARCH}" = 'amd64' ] && true + then + ARCH='x86_64' + fi + if [ "${UNAME_OS}" = 'darwin' ] && true + then + OS='macos' + fi + if [ -z "${ASSET_FILENAME}" ]; then + ASSET_FILENAME="zk-${TAG}-${OS}-${ARCH}${EXT}" + fi +} +# Cleanup function to remove temporary files and stop progress +cleanup() { + # Stop progress animation + progress_clear + + if [ -n "$TMPDIR" ] && [ -d "$TMPDIR" ]; then + log_debug "Cleaning up temporary directory: $TMPDIR" + rm -rf -- "$TMPDIR" + fi +} + +execute() { + STRIP_COMPONENTS=0 + CHECKSUM_FILENAME="" + + # --- Construct URLs --- + GITHUB_DOWNLOAD="https://github.com/${REPO}/releases/download" + ASSET_URL="${GITHUB_DOWNLOAD}/${TAG}/${ASSET_FILENAME}" + CHECKSUM_URL="" + if [ -n "$CHECKSUM_FILENAME" ]; then + CHECKSUM_URL="${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM_FILENAME}" + fi + + # --- Download and Verify --- + TMPDIR=$(mktemp -d) + # Set up cleanup trap that includes progress clearing + trap cleanup EXIT HUP INT TERM + log_debug "Downloading files into ${TMPDIR}" + log_info "Downloading ${ASSET_URL}" + github_http_download "${TMPDIR}/${ASSET_FILENAME}" "${ASSET_URL}" + + # Try to find embedded checksum first + EMBEDDED_HASH=$(find_embedded_checksum "$VERSION" "$ASSET_FILENAME") + + if [ -n "$EMBEDDED_HASH" ]; then + log_info "Using embedded checksum for verification" + + # Verify using embedded hash + got=$(hash_compute "${TMPDIR}/${ASSET_FILENAME}") + if [ "$got" != "$EMBEDDED_HASH" ]; then + log_crit "Checksum verification failed for ${ASSET_FILENAME}" + log_crit "Expected: ${EMBEDDED_HASH}" + log_crit "Got: ${got}" + return 1 + fi + log_info "Checksum verification successful" + elif [ -n "$CHECKSUM_URL" ]; then + # Fall back to downloading checksum file + log_info "Downloading checksums from ${CHECKSUM_URL}" + github_http_download "${TMPDIR}/${CHECKSUM_FILENAME}" "${CHECKSUM_URL}" + log_info "Verifying checksum ..." + hash_verify "${TMPDIR}/${ASSET_FILENAME}" "${TMPDIR}/${CHECKSUM_FILENAME}" + else + log_info "No checksum found, skipping verification." + fi + + if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then + log_debug "Target is raw binary" + else + log_info "Extracting ${ASSET_FILENAME}..." + (cd "${TMPDIR}" && untar "${ASSET_FILENAME}" "${STRIP_COMPONENTS}") + fi + BINARY_NAME='zk' + if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then + BINARY_PATH="${TMPDIR}/${ASSET_FILENAME}" + else + BINARY_PATH="${TMPDIR}/zk" + fi + + if [ "${UNAME_OS}" = "windows" ]; then + case "${BINARY_NAME}" in *.exe) ;; *) BINARY_NAME="${BINARY_NAME}.exe" ;; esac + case "${BINARY_PATH}" in *.exe) ;; *) BINARY_PATH="${BINARY_PATH}.exe" ;; esac + fi + + if [ ! -f "${BINARY_PATH}" ]; then + log_crit "Binary not found: ${BINARY_PATH}" + log_crit "Listing contents of ${TMPDIR} ..." + if command -v find >/dev/null 2>&1; then + cd "${TMPDIR}" && find . + else + cd "${TMPDIR}" && ls -R . + fi + return 1 + fi + + progress_clear + # Install the binary + INSTALL_PATH="${BINDIR}/${BINARY_NAME}" + + if [ "$DRY_RUN" = "1" ]; then + log_info "[DRY RUN] ${BINARY_NAME} dry-run installation succeeded! (Would install to: ${INSTALL_PATH})" + else + log_info "Installing binary to ${INSTALL_PATH}" + test ! -d "${BINDIR}" && install -d "${BINDIR}" + install "${BINARY_PATH}" "${INSTALL_PATH}" + log_info "${BINARY_NAME} installation complete!" + fi +} + +# --- Configuration --- +NAME='zk' +REPO='zk-org/zk' +EXT='.tar.gz' + +# use in logging routines +log_prefix() { + echo "${REPO}" +} + +parse_args "$@" + +progress_init +progress_pulse_start + +# --- Determine target platform --- +OS="${BINSTALLER_OS:-$(uname_os)}" +UNAME_OS="${OS}" + +ARCH="${BINSTALLER_ARCH:-$(uname_arch)}" +UNAME_ARCH="${ARCH}" +log_info "Detected Platform: ${OS}/${ARCH}" + +# --- Validate platform --- +uname_os_check "$OS" +uname_arch_check "$ARCH" + +tag_to_version + +resolve_asset_filename +execute From e9a66313cf61fc3db23d79f3ffb02133ab6d7bb9 Mon Sep 17 00:00:00 2001 From: Jason Swank <632526+jswank@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:45:08 -0500 Subject: [PATCH 2/2] Update README --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 038425f..f3538d2 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ A collection of installation scripts for binary tools, generated using [binstaller](https://github.com/binary-install/binstaller). These scripts are portable POSIX shell scripts that work across Linux, macOS, and Windows (Git Bash/WSL) and are designed to be a simple, secure way to install self-contained binaries from GitHub releases. +* [Quick Start](#quick-start) +* [Workflows](#workflows) + * [Creating New Installation Scripts](#creating-new-installation-scripts) +* [Notes](#notes) + * [Binary Names](#binary-names) + ## Quick Start Install any binary using its generated script: @@ -86,3 +92,6 @@ binaries: - name: thv path: thv ``` + +## License +MIT. See [LICENSE](LICENSE) for details.