├── .editorconfig ├── .github ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── commit_msg.yaml │ ├── dco_check.yaml │ ├── go.yml │ ├── package-builds-unstable.yml │ ├── release.yaml │ ├── reproducible-builds.yaml │ └── zizmor.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .vscode └── settings.json ├── Brewfile ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── cmd └── yeet │ └── main.go ├── confyg ├── LICENSE ├── README.md ├── allower.go ├── allower_test.go ├── flagconfyg │ ├── flagconfyg.go │ └── flagconfyg_test.go ├── map_output.go ├── map_output_test.go ├── print.go ├── read.go ├── read_test.go ├── reader.go ├── reader_test.go ├── rule.go └── testdata │ ├── block.golden │ ├── block.in │ ├── comment.golden │ ├── comment.in │ ├── empty.golden │ ├── empty.in │ ├── module.golden │ ├── module.in │ ├── replace.golden │ ├── replace.in │ ├── replace2.golden │ ├── replace2.in │ ├── rule1.golden │ └── url.golden ├── doc └── api.md ├── go.mod ├── go.sum ├── internal ├── git.go ├── git_test.go ├── gitea │ └── gitea.go ├── internal.go ├── mkdeb │ ├── mkdeb.go │ └── mkdeb_test.go ├── mkrpm │ ├── mkrpm.go │ └── mkrpm_test.go ├── mktarball │ ├── mktarball.go │ └── mktarball_test.go ├── pkgmeta │ └── package.go ├── testdata │ └── hello │ │ └── main.go ├── vfs │ └── modtimefs.go ├── yeet │ ├── doc.go │ └── yeet.go └── yeettest │ └── buildpackage.go ├── package-lock.json ├── package.json ├── var └── .gitignore ├── yeet.go ├── yeet_test.go └── yeetfile.js /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | insert_final_newline = true -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: cadey 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Checklist 8 | 9 | - [ ] Added test cases to [the relevant parts of the codebase](https://anubis.techaro.lol/docs/developer/code-quality) 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | groups: 8 | github-actions: 9 | patterns: 10 | - "*" 11 | 12 | - package-ecosystem: gomod 13 | directory: / 14 | schedule: 15 | interval: weekly 16 | groups: 17 | gomod: 18 | patterns: 19 | - "*" 20 | -------------------------------------------------------------------------------- /.github/workflows/commit_msg.yaml: -------------------------------------------------------------------------------- 1 | name: "Lint PR" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | - reopened 10 | 11 | jobs: 12 | main: 13 | name: Validate PR title 14 | runs-on: ubuntu-latest 15 | permissions: 16 | pull-requests: read 17 | steps: 18 | - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/dco_check.yaml: -------------------------------------------------------------------------------- 1 | name: DCO Check 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | check: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: tisonkun/actions-dco@f1024cd563550b5632e754df11b7d30b73be54a5 # v1.1 10 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | permissions: 10 | contents: read 11 | actions: write 12 | 13 | jobs: 14 | go_tests: 15 | #runs-on: alrest-techarohq 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | with: 20 | persist-credentials: false 21 | fetch-tags: true 22 | 23 | - name: build essential 24 | run: | 25 | sudo apt-get update 26 | sudo apt-get install -y build-essential 27 | 28 | - name: Set up Homebrew 29 | uses: Homebrew/actions/setup-homebrew@8bcbfa880644de056b8e6bb1c583cb2f4362c6bb 30 | 31 | - name: Setup Homebrew cellar cache 32 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 33 | with: 34 | path: | 35 | /home/linuxbrew/.linuxbrew/Cellar 36 | /home/linuxbrew/.linuxbrew/bin 37 | /home/linuxbrew/.linuxbrew/etc 38 | /home/linuxbrew/.linuxbrew/include 39 | /home/linuxbrew/.linuxbrew/lib 40 | /home/linuxbrew/.linuxbrew/opt 41 | /home/linuxbrew/.linuxbrew/sbin 42 | /home/linuxbrew/.linuxbrew/share 43 | /home/linuxbrew/.linuxbrew/var 44 | key: ${{ runner.os }}-go-homebrew-cellar-${{ hashFiles('go.sum') }} 45 | restore-keys: | 46 | ${{ runner.os }}-go-homebrew-cellar- 47 | 48 | - name: Install Brew dependencies 49 | run: | 50 | brew bundle 51 | 52 | - name: Setup Golang caches 53 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 54 | with: 55 | path: | 56 | ~/.cache/go-build 57 | ~/go/pkg/mod 58 | key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }} 59 | restore-keys: | 60 | ${{ runner.os }}-golang- 61 | 62 | - name: Build 63 | run: make build 64 | 65 | - name: Test 66 | run: make test 67 | 68 | - uses: dominikh/staticcheck-action@fe1dd0c3658873b46f8c9bb3291096a617310ca6 # v1.3.1 69 | with: 70 | version: "latest" 71 | -------------------------------------------------------------------------------- /.github/workflows/package-builds-unstable.yml: -------------------------------------------------------------------------------- 1 | name: Package builds (unstable) 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | permissions: 10 | contents: read 11 | actions: write 12 | 13 | jobs: 14 | package_builds: 15 | #runs-on: alrest-techarohq 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | with: 20 | persist-credentials: false 21 | fetch-tags: true 22 | fetch-depth: 0 23 | 24 | - name: build essential 25 | run: | 26 | sudo apt-get update 27 | sudo apt-get install -y build-essential 28 | 29 | - name: Set up Homebrew 30 | uses: Homebrew/actions/setup-homebrew@8bcbfa880644de056b8e6bb1c583cb2f4362c6bb 31 | 32 | - name: Setup Homebrew cellar cache 33 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 34 | with: 35 | path: | 36 | /home/linuxbrew/.linuxbrew/Cellar 37 | /home/linuxbrew/.linuxbrew/bin 38 | /home/linuxbrew/.linuxbrew/etc 39 | /home/linuxbrew/.linuxbrew/include 40 | /home/linuxbrew/.linuxbrew/lib 41 | /home/linuxbrew/.linuxbrew/opt 42 | /home/linuxbrew/.linuxbrew/sbin 43 | /home/linuxbrew/.linuxbrew/share 44 | /home/linuxbrew/.linuxbrew/var 45 | key: ${{ runner.os }}-go-homebrew-cellar-${{ hashFiles('go.sum') }} 46 | restore-keys: | 47 | ${{ runner.os }}-go-homebrew-cellar- 48 | 49 | - name: Install Brew dependencies 50 | run: | 51 | brew bundle 52 | 53 | - name: Setup Golang caches 54 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 55 | with: 56 | path: | 57 | ~/.cache/go-build 58 | ~/go/pkg/mod 59 | key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }} 60 | restore-keys: | 61 | ${{ runner.os }}-golang- 62 | 63 | - name: Build Packages 64 | run: | 65 | go run ./cmd/yeet 66 | 67 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 68 | with: 69 | name: packages 70 | path: var/* 71 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Cut Release 2 | on: 3 | workflow_dispatch: {} 4 | jobs: 5 | release: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 9 | with: 10 | persist-credentials: false 11 | fetch-tags: true 12 | fetch-depth: 0 13 | 14 | - name: build essential 15 | run: | 16 | sudo apt-get update 17 | sudo apt-get install -y build-essential 18 | 19 | - name: Set up Homebrew 20 | uses: Homebrew/actions/setup-homebrew@8bcbfa880644de056b8e6bb1c583cb2f4362c6bb 21 | 22 | - name: Setup Homebrew cellar cache 23 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 24 | with: 25 | path: | 26 | /home/linuxbrew/.linuxbrew/Cellar 27 | /home/linuxbrew/.linuxbrew/bin 28 | /home/linuxbrew/.linuxbrew/etc 29 | /home/linuxbrew/.linuxbrew/include 30 | /home/linuxbrew/.linuxbrew/lib 31 | /home/linuxbrew/.linuxbrew/opt 32 | /home/linuxbrew/.linuxbrew/sbin 33 | /home/linuxbrew/.linuxbrew/share 34 | /home/linuxbrew/.linuxbrew/var 35 | key: ${{ runner.os }}-go-homebrew-cellar-${{ hashFiles('go.sum') }} 36 | restore-keys: | 37 | ${{ runner.os }}-go-homebrew-cellar- 38 | 39 | - name: Install Brew dependencies 40 | run: | 41 | brew bundle 42 | 43 | - name: Setup Golang caches 44 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 45 | with: 46 | path: | 47 | ~/.cache/go-build 48 | ~/go/pkg/mod 49 | key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }} 50 | restore-keys: | 51 | ${{ runner.os }}-golang- 52 | 53 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 54 | - name: release 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.TECHAROHQ_HACK_WRITE_TOKEN }} 57 | run: | 58 | npm ci 59 | npx semantic-release --debug 60 | -------------------------------------------------------------------------------- /.github/workflows/reproducible-builds.yaml: -------------------------------------------------------------------------------- 1 | name: Reproducible builds 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | reproducible: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | with: 18 | persist-credentials: false 19 | fetch-tags: true 20 | 21 | - name: Setup Go environment 22 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 23 | with: 24 | go-version: "stable" 25 | 26 | - name: Setup Golang caches 27 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 28 | with: 29 | path: | 30 | ~/.cache/go-build 31 | ~/go/pkg/mod 32 | key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }} 33 | restore-keys: | 34 | ${{ runner.os }}-golang- 35 | 36 | - name: Setup Python environment 37 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 38 | with: 39 | python-version: "3.12" 40 | 41 | - name: Install diffoscope 42 | run: | 43 | pip install diffoscope==297 44 | 45 | sudo apt-get update 46 | sudo apt-get -y install 7zip abootimg acl apksigcopier apksigner apktool binutils-multiarch black bzip2 caca-utils colord db-util default-jdk default-jdk-headless device-tree-compiler docx2txt e2fsprogs enjarify ffmpeg file fontforge-extras fonttools fp-utils genisoimage gettext ghc ghostscript giflib-tools gnumeric gnupg-utils gpg hdf5-tools html2text imagemagick openjdk-21-jdk jsbeautifier libarchive-tools linux-image-generic llvm lz4 lzip mono-utils ocaml-nox odt2txt oggvideotools openssh-client openssl perl pgpdump poppler-utils procyon-decompiler python3-all python3-argcomplete python3-binwalk python3-debian python3-defusedxml python3-distro python3-guestfs python3-h5py python3-jsondiff python3-pdfminer python3-progressbar python3-pytest python3-pyxattr python3-rpm python3-tlsh r-base-core rpm2cpio sng sqlite3 squashfs-tools tcpdump u-boot-tools unzip wabt xmlbeans xxd xz-utils zip zstd 47 | 48 | - name: Build yeet packages twice 49 | run: | 50 | mkdir -p ./var/pass1 ./var/pass2 ./var/output 51 | go run ./cmd/yeet --force-git-version 1.0.0 --package-dest-dir ./var/pass1 52 | go run ./cmd/yeet --force-git-version 1.0.0 --package-dest-dir ./var/pass2 53 | 54 | for file in ./var/pass1/*; do 55 | diffoscope --text "${file/pass1/output}.txt" $file "${file/pass1/pass2}"; 56 | done 57 | 58 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 59 | with: 60 | name: pass1 61 | path: var/pass1/* 62 | 63 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 64 | with: 65 | name: pass2 66 | path: var/pass2/* 67 | 68 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 69 | with: 70 | name: output 71 | path: var/output/* 72 | -------------------------------------------------------------------------------- /.github/workflows/zizmor.yml: -------------------------------------------------------------------------------- 1 | name: zizmor 2 | 3 | on: 4 | push: 5 | paths: 6 | - '.github/workflows/*.ya?ml' 7 | pull_request: 8 | paths: 9 | - '.github/workflows/*.ya?ml' 10 | 11 | jobs: 12 | zizmor: 13 | name: zizmor latest via PyPI 14 | runs-on: ubuntu-24.04 15 | permissions: 16 | security-events: write 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | with: 21 | persist-credentials: false 22 | 23 | - name: Install the latest version of uv 24 | uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 25 | 26 | - name: Run zizmor 🌈 27 | run: uvx zizmor --format sarif . > results.sarif 28 | env: 29 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Upload SARIF file 32 | uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 33 | with: 34 | sarif_file: results.sarif 35 | category: zizmor 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | 27 | node_modules 28 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint --edit "$1" -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm test -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "github.copilot.enable": { 3 | "*": false 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew "go@1.24" 2 | brew "node" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [0.6.0](https://github.com/TecharoHQ/yeet/compare/v0.5.0...v0.6.0) (2025-06-01) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * use sh instead of bash ([#27](https://github.com/TecharoHQ/yeet/issues/27)) ([2e169ef](https://github.com/TecharoHQ/yeet/commit/2e169efafec110d71c8fb596c0a163f902c6f757)) 7 | 8 | 9 | ### Features 10 | 11 | * support SOURCE_DATE_EPOCH ([#26](https://github.com/TecharoHQ/yeet/issues/26)) ([c01c3db](https://github.com/TecharoHQ/yeet/commit/c01c3db54cddbed5faf1f32a0993620c7aaade9b)) 12 | * use mvdan.cc/sh/v3 instead of system sh ([#28](https://github.com/TecharoHQ/yeet/issues/28)) ([5459d92](https://github.com/TecharoHQ/yeet/commit/5459d922e1e3e0aaa3deef6cdf449c4cab3aeeea)) 13 | 14 | # [0.5.0](https://github.com/TecharoHQ/yeet/compare/v0.4.0...v0.5.0) (2025-05-30) 15 | 16 | 17 | ### Features 18 | 19 | * **deb,rpm,tarball:** implement reproducible builds ([49686d8](https://github.com/TecharoHQ/yeet/commit/49686d84f20a6df92378139a6705504621f7c9d9)) 20 | 21 | # [0.4.0](https://github.com/TecharoHQ/yeet/compare/v0.3.0...v0.4.0) (2025-05-29) 22 | 23 | 24 | ### Features 25 | 26 | * **yeetfile:** ppc64le builds ([bbc1e38](https://github.com/TecharoHQ/yeet/commit/bbc1e384f82724660365a4525262180241ec3f06)) 27 | 28 | # [0.3.0](https://github.com/TecharoHQ/yeet/compare/v0.2.3...v0.3.0) (2025-05-20) 29 | 30 | 31 | ### Features 32 | 33 | * **confyg:** export package publicly ([7abba3a](https://github.com/TecharoHQ/yeet/commit/7abba3a1ddcdd9eca4776a80d98851dcfc5005fc)) 34 | 35 | ## [0.2.3](https://github.com/TecharoHQ/yeet/compare/v0.2.2...v0.2.3) (2025-05-09) 36 | 37 | 38 | ### Bug Fixes 39 | 40 | * **cmd/yeet:** show build method in --version ([#20](https://github.com/TecharoHQ/yeet/issues/20)) ([ca06ce7](https://github.com/TecharoHQ/yeet/commit/ca06ce7d9247e1d18b8be346e191404e652bd6f9)) 41 | 42 | ## [0.2.2](https://github.com/TecharoHQ/yeet/compare/v0.2.1...v0.2.2) (2025-05-02) 43 | 44 | 45 | ### Bug Fixes 46 | 47 | * **mkdeb|mkrpm:** append package name to ${doc} folder ([4976741](https://github.com/TecharoHQ/yeet/commit/4976741c7dba9196d23e25d2fd1ae07af10673e3)) 48 | 49 | ## [0.2.1](https://github.com/TecharoHQ/yeet/compare/v0.2.0...v0.2.1) (2025-04-26) 50 | 51 | 52 | ### Bug Fixes 53 | 54 | * make build errors fatal ([#18](https://github.com/TecharoHQ/yeet/issues/18)) ([7a467e7](https://github.com/TecharoHQ/yeet/commit/7a467e7d2b8dc4dd6eb704a9940adb1c9711859e)) 55 | 56 | # [0.2.0](https://github.com/TecharoHQ/yeet/compare/v0.1.1...v0.2.0) (2025-04-26) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * **internal:** fix version string mangling logic ([#16](https://github.com/TecharoHQ/yeet/issues/16)) ([56e6fa9](https://github.com/TecharoHQ/yeet/commit/56e6fa973d89aa220b0a712c59a751fa8ccfa49c)) 62 | 63 | 64 | ### Features 65 | 66 | * enforce semver in package versions ([#17](https://github.com/TecharoHQ/yeet/issues/17)) ([178f179](https://github.com/TecharoHQ/yeet/commit/178f17969e17eaf26eb28b9c93a6c24600b5c98c)) 67 | 68 | # [0.2.0](https://github.com/TecharoHQ/yeet/compare/v0.1.1...v0.2.0) (2025-04-26) 69 | 70 | 71 | ### Bug Fixes 72 | 73 | * **internal:** fix version string mangling logic ([#16](https://github.com/TecharoHQ/yeet/issues/16)) ([56e6fa9](https://github.com/TecharoHQ/yeet/commit/56e6fa973d89aa220b0a712c59a751fa8ccfa49c)) 74 | 75 | 76 | ### Features 77 | 78 | * enforce semver in package versions ([#17](https://github.com/TecharoHQ/yeet/issues/17)) ([178f179](https://github.com/TecharoHQ/yeet/commit/178f17969e17eaf26eb28b9c93a6c24600b5c98c)) 79 | 80 | ## [0.1.1](https://github.com/TecharoHQ/yeet/compare/v0.1.0...v0.1.1) (2025-04-22) 81 | 82 | ### Bug Fixes 83 | 84 | - **internal/mkdeb:** set CGO_ENABLED=0 ([#13](https://github.com/TecharoHQ/yeet/issues/13)) ([5a90b17](https://github.com/TecharoHQ/yeet/commit/5a90b1744ed47e09c6786419f5ecaf172a817606)) 85 | 86 | # [0.1.0](https://github.com/TecharoHQ/yeet/compare/v0.0.10...v0.1.0) (2025-04-21) 87 | 88 | ### Features 89 | 90 | - **internal:** add --force-git-version flag to override git tag logic ([5f09e47](https://github.com/TecharoHQ/yeet/commit/5f09e4734b838bfcb3ffd99671f6aa280ea81e47)) 91 | 92 | ## [0.0.10](https://github.com/TecharoHQ/yeet/compare/v0.0.9...v0.0.10) (2025-04-21) 93 | 94 | ### Bug Fixes 95 | 96 | - automated release management ([d0efd92](https://github.com/TecharoHQ/yeet/commit/d0efd92f1bb77d2dc8f353dc793c8505e1ee7ddb)) 97 | - dispatch releases on main branch ([c1ce6db](https://github.com/TecharoHQ/yeet/commit/c1ce6db03f24e1a8288ae908bd276483933b4327)) 98 | - fix release flow? ([d4093e7](https://github.com/TecharoHQ/yeet/commit/d4093e77e7d122f27256b87bdc616884348d0752)) 99 | - hack a write token ([d57be0e](https://github.com/TecharoHQ/yeet/commit/d57be0e64ceb6a376578e27421881ae0d0f9e8ed)) 100 | - make package builds happen in the release running step ([360e99e](https://github.com/TecharoHQ/yeet/commit/360e99efa745639241806518805c89908e008c11)) 101 | - make stable package builds trigger on created ([c4c1955](https://github.com/TecharoHQ/yeet/commit/c4c1955db87004a5e4ab03e2452694439b17a203)) 102 | 103 | ## [0.0.10](https://github.com/TecharoHQ/yeet/compare/v0.0.9...v0.0.10) (2025-04-21) 104 | 105 | ### Bug Fixes 106 | 107 | - automated release management ([d0efd92](https://github.com/TecharoHQ/yeet/commit/d0efd92f1bb77d2dc8f353dc793c8505e1ee7ddb)) 108 | - dispatch releases on main branch ([c1ce6db](https://github.com/TecharoHQ/yeet/commit/c1ce6db03f24e1a8288ae908bd276483933b4327)) 109 | - fix release flow? ([d4093e7](https://github.com/TecharoHQ/yeet/commit/d4093e77e7d122f27256b87bdc616884348d0752)) 110 | - hack a write token ([d57be0e](https://github.com/TecharoHQ/yeet/commit/d57be0e64ceb6a376578e27421881ae0d0f9e8ed)) 111 | - make stable package builds trigger on created ([c4c1955](https://github.com/TecharoHQ/yeet/commit/c4c1955db87004a5e4ab03e2452694439b17a203)) 112 | 113 | ## [0.0.10](https://github.com/TecharoHQ/yeet/compare/v0.0.9...v0.0.10) (2025-04-21) 114 | 115 | ### Bug Fixes 116 | 117 | - automated release management ([d0efd92](https://github.com/TecharoHQ/yeet/commit/d0efd92f1bb77d2dc8f353dc793c8505e1ee7ddb)) 118 | 119 | ## v0.0.9 120 | 121 | - Enable Gitea package uploading 122 | 123 | ## v0.0.8 124 | 125 | - Add configuration via confyg for package signing 126 | - Added installation instructions to the `README.md` 127 | - Set mtime for deb/rpm package files to unix time 0. 128 | 129 | ## v0.0.7 130 | 131 | Make configuration files for OS packages have mode 0600 by default. 132 | 133 | ## v0.0.6 134 | 135 | - Exit when `--version` is passed. 136 | - Fix CI package autobuilds. 137 | 138 | ## v0.0.4 139 | 140 | Fix go.mod name for project. 141 | 142 | ## v0.0.3 143 | 144 | Fix CI for package builds. 145 | 146 | ## v0.0.2 147 | 148 | - Document package build settings and introduce `yeet.getenv`. 149 | 150 | ## v0.0.1 151 | 152 | - Import source code from [/x/](https://github.com/Xe/x). 153 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Techaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build clean deps all lint test package 2 | 3 | build: 4 | go build -o ./var/yeet ./cmd/yeet 5 | 6 | clean: 7 | rm ./var/yeet* ||: 8 | 9 | deps: 10 | go mod download 11 | 12 | all: build lint test clean package 13 | 14 | lint: 15 | go vet ./... 16 | go tool staticcheck ./... 17 | 18 | test: 19 | go test ./... 20 | 21 | package: 22 | go run ./cmd/yeet -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yeet 2 | 3 | ![enbyware](https://pride-badges.pony.workers.dev/static/v1?label=enbyware&labelColor=%23555&stripeWidth=8&stripeColors=FCF434%2CFFFFFF%2C9C59D1%2C2C2C2C) 4 | ![GitHub Issues or Pull Requests by label](https://img.shields.io/github/issues/TecharoHQ/yeet) 5 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/TecharoHQ/yeet) 6 | ![language count](https://img.shields.io/github/languages/count/TecharoHQ/yeet) 7 | ![repo size](https://img.shields.io/github/repo-size/TecharoHQ/yeet) 8 | 9 | Yeet out actions with maximum haste! Declare your build instructions as small JavaScript snippets and let er rip! 10 | 11 | For example, here's how you build a Go program into an RPM for x86_64 Linux: 12 | 13 | ```js 14 | // yeetfile.js 15 | const platform = "linux"; 16 | const goarch = "amd64"; 17 | 18 | rpm.build({ 19 | name: "hello", 20 | description: "Hello, world!", 21 | license: "CC0", 22 | platform, 23 | goarch, 24 | 25 | build: ({ bin }) => { 26 | $`go build ./cmd/hello ${bin}/hello`; 27 | }, 28 | }); 29 | ``` 30 | 31 | Yeetfiles MUST obey the following rules: 32 | 33 | 1. Thou shalt never import thine code from another file nor require npm for any reason. 34 | 1. If thy task requires common functionality, thou shalt use native interfaces when at all possible. 35 | 1. If thy task hath been copied and pasted multiple times, yon task belongeth in a native interface. 36 | 37 | See [the API documentation](./doc/api.md) for more information about the exposed API. 38 | 39 | ## Installation 40 | 41 | To install `yeet`, use the following command: 42 | 43 | ```sh 44 | go install github.com/TecharoHQ/yeet/cmd/yeet@latest 45 | ``` 46 | 47 | ## Development 48 | 49 | To get started developing for `yeet`, install Go and Node from [Homebrew](https://brew.sh). 50 | 51 | ```text 52 | brew bundle 53 | npm ci 54 | npm run prepare 55 | ``` 56 | 57 | ## Support 58 | 59 | For support, please [subscribe to me on Patreon](https://patreon.com/cadey) and ask in the `#yeet` channel in the patron Discord. 60 | 61 | ## Packaging Status 62 | 63 | [![Packaging status](https://repology.org/badge/vertical-allrepos/yeet-js-build-tool.svg?columns=3)](https://repology.org/project/yeet-js-build-tool/versions) 64 | 65 | ## Contributors 66 | 67 | 68 | 69 | 70 | 71 | Made with [contrib.rocks](https://contrib.rocks). 72 | -------------------------------------------------------------------------------- /cmd/yeet/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "log/slog" 10 | "net/http" 11 | "os" 12 | "path/filepath" 13 | "runtime" 14 | "strings" 15 | 16 | "al.essio.dev/pkg/shellescape" 17 | yeetver "github.com/TecharoHQ/yeet" 18 | "github.com/TecharoHQ/yeet/confyg/flagconfyg" 19 | "github.com/TecharoHQ/yeet/internal/gitea" 20 | "github.com/TecharoHQ/yeet/internal/mkdeb" 21 | "github.com/TecharoHQ/yeet/internal/mkrpm" 22 | "github.com/TecharoHQ/yeet/internal/mktarball" 23 | "github.com/TecharoHQ/yeet/internal/pkgmeta" 24 | "github.com/TecharoHQ/yeet/internal/yeet" 25 | "github.com/dop251/goja" 26 | "mvdan.cc/sh/v3/interp" 27 | "mvdan.cc/sh/v3/syntax" 28 | ) 29 | 30 | var ( 31 | config = flag.String("config", configFileLocation(), "configuration file, if set (see flagconfyg(4))") 32 | fname = flag.String("fname", "yeetfile.js", "filename for the yeetfile") 33 | version = flag.Bool("version", false, "if set, print version of yeet and exit") 34 | ) 35 | 36 | func configFileLocation() string { 37 | dir, err := os.UserConfigDir() 38 | if err != nil { 39 | //ln.Error(context.Background(), err, ln.Debug("can't read config dir")) 40 | return "" 41 | } 42 | 43 | dir = filepath.Join(dir, "techaro.lol", "yeet") 44 | os.MkdirAll(dir, 0700) 45 | 46 | return filepath.Join(dir, filepath.Base(os.Args[0])+".config") 47 | } 48 | 49 | func runcmd(cmdName string, args ...string) string { 50 | ctx := context.Background() 51 | 52 | slog.Debug("running command", "cmd", cmdName, "args", args) 53 | 54 | result, err := yeet.Output(ctx, cmdName, args...) 55 | if err != nil { 56 | panic(err) 57 | } 58 | 59 | return result 60 | } 61 | 62 | func dockerload(fname string) { 63 | if fname == "" { 64 | fname = "./result" 65 | } 66 | yeet.DockerLoadResult(context.Background(), fname) 67 | } 68 | 69 | func dockerbuild(tag string, args ...string) { 70 | yeet.DockerBuild(context.Background(), yeet.WD, tag, args...) 71 | } 72 | 73 | func dockerpush(image string) { 74 | yeet.DockerPush(context.Background(), image) 75 | } 76 | 77 | func buildShellCommand(literals []string, exprs ...any) string { 78 | var sb strings.Builder 79 | fmt.Fprintln(&sb, "set -e") 80 | 81 | for i, value := range exprs { 82 | sb.WriteString(literals[i]) 83 | sb.WriteString(shellescape.Quote(fmt.Sprint(value))) 84 | } 85 | 86 | sb.WriteString(literals[len(literals)-1]) 87 | 88 | return sb.String() 89 | } 90 | 91 | func runShellCommand(ctx context.Context, literals []string, exprs ...any) (string, error) { 92 | src := buildShellCommand(literals, exprs...) 93 | 94 | slog.Debug("running command", "src", src) 95 | 96 | file, err := syntax.NewParser().Parse(strings.NewReader(src), "") 97 | if err != nil { 98 | return "", err 99 | } 100 | 101 | var buf bytes.Buffer 102 | 103 | runner, err := interp.New( 104 | interp.StdIO(nil, &buf, os.Stderr), 105 | ) 106 | if err != nil { 107 | return "", err 108 | } 109 | 110 | if err := runner.Run(ctx, file); err != nil { 111 | return "", err 112 | } 113 | 114 | slog.Debug("command output", "src", src, "output", buf.String()) 115 | 116 | return buf.String(), nil 117 | } 118 | 119 | func hostname() string { 120 | result, err := os.Hostname() 121 | if err != nil { 122 | panic(err) 123 | } 124 | return result 125 | } 126 | 127 | func gitVersion() string { 128 | vers, err := yeet.GitTag(context.Background()) 129 | if err != nil { 130 | panic(err) 131 | } 132 | return vers 133 | } 134 | 135 | func main() { 136 | flag.Parse() 137 | ctx := context.Background() 138 | 139 | if *config != "" { 140 | flagconfyg.CmdParse(ctx, *config) 141 | } 142 | flag.Parse() 143 | 144 | if *version { 145 | fmt.Printf("yeet version %s, built via %s\n", yeetver.Version, yeetver.BuildMethod) 146 | return 147 | } 148 | 149 | vm := goja.New() 150 | vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true)) 151 | 152 | defer func() { 153 | if r := recover(); r != nil { 154 | slog.Error("error in JS", "err", r) 155 | os.Exit(1) 156 | } 157 | }() 158 | 159 | data, err := os.ReadFile(*fname) 160 | if err != nil { 161 | log.Fatal(err) 162 | } 163 | 164 | vm.Set("$", func(literals []string, exprs ...any) string { 165 | result, err := runShellCommand(ctx, literals, exprs...) 166 | if err != nil { 167 | panic(err) 168 | } 169 | return result 170 | }) 171 | 172 | vm.Set("deb", map[string]any{ 173 | "build": func(p pkgmeta.Package) string { 174 | foutpath, err := mkdeb.Build(p) 175 | if err != nil { 176 | panic(err) 177 | } 178 | return foutpath 179 | }, 180 | "name": "debian", 181 | }) 182 | 183 | vm.Set("docker", map[string]any{ 184 | "build": dockerbuild, 185 | "load": dockerload, 186 | "push": dockerpush, 187 | }) 188 | 189 | vm.Set("file", map[string]any{ 190 | "install": func(src, dst string) { 191 | if err := mktarball.Copy(src, dst); err != nil { 192 | panic(err) 193 | } 194 | }, 195 | }) 196 | 197 | vm.Set("git", map[string]any{ 198 | "repoRoot": func() string { 199 | return runcmd("git", "rev-parse", "--show-toplevel") 200 | }, 201 | "tag": gitVersion, 202 | }) 203 | 204 | vm.Set("gitea", map[string]any{ 205 | "uploadPackage": func(owner, distro, component, fname string) { 206 | if err := gitea.UploadPackage(ctx, http.DefaultClient, owner, distro, component, fname); err != nil { 207 | panic(err) 208 | } 209 | }, 210 | }) 211 | 212 | vm.Set("go", map[string]any{ 213 | "build": func(args ...string) { 214 | args = append([]string{"build"}, args...) 215 | runcmd("go", args...) 216 | }, 217 | "install": func() { runcmd("go", "install") }, 218 | }) 219 | 220 | vm.Set("log", map[string]any{ 221 | "println": fmt.Println, 222 | }) 223 | 224 | vm.Set("rpm", map[string]any{ 225 | "build": func(p pkgmeta.Package) string { 226 | foutpath, err := mkrpm.Build(p) 227 | if err != nil { 228 | panic(err) 229 | } 230 | return foutpath 231 | }, 232 | "name": "rpm", 233 | }) 234 | 235 | vm.Set("tarball", map[string]any{ 236 | "build": func(p pkgmeta.Package) string { 237 | foutpath, err := mktarball.Build(p) 238 | if err != nil { 239 | panic(err) 240 | } 241 | return foutpath 242 | }, 243 | "name": "tarball", 244 | }) 245 | 246 | vm.Set("yeet", map[string]any{ 247 | "cwd": yeet.WD, 248 | "datetag": yeet.DateTag, 249 | "hostname": hostname(), 250 | "runcmd": runcmd, 251 | "run": runcmd, 252 | "setenv": os.Setenv, 253 | "getenv": os.Getenv, 254 | "goos": runtime.GOOS, 255 | "goarch": runtime.GOARCH, 256 | }) 257 | 258 | if _, err := vm.RunScript(*fname, string(data)); err != nil { 259 | fmt.Fprintf(os.Stderr, "error running %s: %v", *fname, err) 260 | os.Exit(1) 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /confyg/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /confyg/README.md: -------------------------------------------------------------------------------- 1 | # confyg 2 | 3 | A suitably generic form of the Go module configuration file parser. 4 | 5 | [![GoDoc](https://godoc.org/within.website/confyg?status.svg)](https://godoc.org/within.website/confyg) 6 | 7 | Usage is simple: 8 | 9 | ```go 10 | type server struct { 11 | port string 12 | keys *crypto.Keypair 13 | db *storm.DB 14 | } 15 | 16 | func (s *server) Allow(verb string, block bool) bool { 17 | switch verb { 18 | case "port": 19 | return !block 20 | case "dbfile": 21 | return !block 22 | case "keys": 23 | return !block 24 | } 25 | 26 | return false 27 | } 28 | 29 | func (s *server) Read(errs *bytes.Buffer, fs *confyg.FileSyntax, line *confyg.Line, verb string, args []string) { 30 | switch verb { 31 | case "port": 32 | _, err := strconv.Atoi(args[0]) 33 | if err != nil { 34 | fmt.Fprintf(errs, "%s:%d value is not a number: %s: %v\n", fs.Name, line.Start.Line, args[0], err) 35 | return 36 | } 37 | 38 | s.port = args[0] 39 | 40 | case "dbfile": 41 | dbFile := args[0][1 : len(args[0])-1] // shuck off quotes 42 | 43 | db, err := storm.Open(dbFile) 44 | if err != nil { 45 | fmt.Fprintf(errs, "%s:%d failed to open storm database: %s: %v\n", fs.Name, line.Start.Line, args[0], err) 46 | return 47 | } 48 | 49 | s.db = db 50 | 51 | case "keys": 52 | kp := &crypto.Keypair{} 53 | 54 | pubk, err := hex.DecodeString(args[0]) 55 | if err != nil { 56 | fmt.Fprintf(errs, "%s:%d invalid public key: %v\n", fs.Name, line.Start.Line, err) 57 | return 58 | } 59 | 60 | privk, err := hex.DecodeString(args[1]) 61 | if err != nil { 62 | fmt.Fprintf(errs, "%s:%d invalid private key: %v\n", fs.Name, line.Start.Line, err) 63 | return 64 | } 65 | 66 | copy(kp.Public[:], pubk[0:32]) 67 | copy(kp.Private[:], privk[0:32]) 68 | 69 | s.keys = kp 70 | } 71 | } 72 | 73 | var ( 74 | configFile = flag.String("cfg", "./apig.cfg", "apig config file location") 75 | ) 76 | 77 | func main() { 78 | flag.Parse() 79 | 80 | data, err := ioutil.ReadFile(*configFile) 81 | if err != nil { 82 | log.Fatal(err) 83 | } 84 | 85 | s := &server{} 86 | _, err = confyg.Parse(*configFile, data, s, s) 87 | if err != nil { 88 | log.Fatal(err) 89 | } 90 | 91 | _ = s 92 | } 93 | ``` 94 | 95 | Or use [`flagconfyg`](https://godoc.org/within.website/confyg/flagconfyg): 96 | 97 | ```go 98 | var ( 99 | config = flag.Config("cfg", "", "if set, configuration file to load (see https://github.com/Xe/x/blob/master/docs/man/flagconfyg.5)") 100 | ) 101 | 102 | func main() { 103 | flag.Parse() 104 | 105 | if *config != "" { 106 | flagconfyg.CmdParse(*config) 107 | } 108 | } 109 | ``` 110 | -------------------------------------------------------------------------------- /confyg/allower.go: -------------------------------------------------------------------------------- 1 | package confyg 2 | 3 | // Allower defines if a given verb and block combination is valid for 4 | // configuration parsing. 5 | // 6 | // If this is intended to be a statement-like verb, block should be set 7 | // to false. If this is intended to be a block-like verb, block should 8 | // be set to true. 9 | type Allower interface { 10 | Allow(verb string, block bool) bool 11 | } 12 | 13 | // AllowerFunc implements Allower for inline definitions. 14 | type AllowerFunc func(verb string, block bool) bool 15 | 16 | // Allow implements Allower. 17 | func (a AllowerFunc) Allow(verb string, block bool) bool { 18 | return a(verb, block) 19 | } 20 | -------------------------------------------------------------------------------- /confyg/allower_test.go: -------------------------------------------------------------------------------- 1 | package confyg 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestAllower(t *testing.T) { 9 | al := AllowerFunc(func(verb string, block bool) bool { 10 | switch verb { 11 | case "project": 12 | if block { 13 | return false 14 | } 15 | 16 | return true 17 | } 18 | 19 | return false 20 | }) 21 | 22 | cases := []struct { 23 | verb string 24 | block bool 25 | want bool 26 | }{ 27 | { 28 | verb: "project", 29 | block: false, 30 | want: true, 31 | }, 32 | { 33 | verb: "nonsense", 34 | block: true, 35 | want: false, 36 | }, 37 | } 38 | 39 | for _, cs := range cases { 40 | t.Run(fmt.Sprint(cs), func(t *testing.T) { 41 | result := al.Allow(cs.verb, cs.block) 42 | 43 | if result != cs.want { 44 | t.Fatalf("wanted Allow(%q, %v) == %v, got: %v", cs.verb, cs.block, cs.want, result) 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /confyg/flagconfyg/flagconfyg.go: -------------------------------------------------------------------------------- 1 | // Package flagconfyg is a hack around confyg. This will blindly convert config 2 | // verbs to flag values. 3 | package flagconfyg 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "flag" 9 | "log" 10 | "os" 11 | "strings" 12 | 13 | "github.com/TecharoHQ/yeet/confyg" 14 | ) 15 | 16 | // CmdParse is a quick wrapper for command usage. It explodes on errors. 17 | func CmdParse(ctx context.Context, path string) { 18 | data, err := os.ReadFile(path) 19 | if err != nil { 20 | return 21 | } 22 | 23 | err = Parse(path, data, flag.CommandLine) 24 | if err != nil { 25 | log.Printf("can't parse %s: %v", path, err) 26 | return 27 | } 28 | } 29 | 30 | // Parse parses the config file in the given file by name, bytes data and into 31 | // the given flagset. 32 | func Parse(name string, data []byte, fs *flag.FlagSet) error { 33 | lineRead := func(errs *bytes.Buffer, fs_ *confyg.FileSyntax, line *confyg.Line, verb string, args []string) { 34 | err := fs.Set(verb, strings.Join(args, " ")) 35 | if err != nil { 36 | errs.WriteString(err.Error()) 37 | } 38 | } 39 | 40 | _, err := confyg.Parse(name, data, confyg.ReaderFunc(lineRead), confyg.AllowerFunc(allower)) 41 | return err 42 | } 43 | 44 | func allower(verb string, block bool) bool { 45 | return true 46 | } 47 | -------------------------------------------------------------------------------- /confyg/flagconfyg/flagconfyg_test.go: -------------------------------------------------------------------------------- 1 | package flagconfyg 2 | 3 | import ( 4 | "flag" 5 | "testing" 6 | ) 7 | 8 | func TestFlagConfyg(t *testing.T) { 9 | fs := flag.NewFlagSet("test", flag.PanicOnError) 10 | sc := fs.String("subscribe", "", "to pewdiepie") 11 | us := fs.String("unsubscribe", "all the time", "from t-series") 12 | 13 | const configFile = `subscribe pewdiepie 14 | 15 | unsubscribe ( 16 | t-series 17 | )` 18 | 19 | err := Parse("test.cfg", []byte(configFile), fs) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | if *sc != "pewdiepie" { 25 | t.Errorf("wanted subscribe->pewdiepie, got: %s", *sc) 26 | } 27 | 28 | if *us != "t-series" { 29 | t.Errorf("wanted unsubscribe->t-series, got: %s", *us) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /confyg/map_output.go: -------------------------------------------------------------------------------- 1 | package confyg 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | ) 7 | 8 | // MapConfig is a simple wrapper around a map. 9 | type MapConfig map[string][]string 10 | 11 | // Allow accepts everything. 12 | func (mc MapConfig) Allow(verb string, block bool) bool { 13 | return true 14 | } 15 | 16 | func (mc MapConfig) Read(errs *bytes.Buffer, fs *FileSyntax, line *Line, verb string, args []string) { 17 | mc[verb] = append(mc[verb], strings.Join(args, " ")) 18 | } 19 | -------------------------------------------------------------------------------- /confyg/map_output_test.go: -------------------------------------------------------------------------------- 1 | package confyg 2 | 3 | import "testing" 4 | 5 | func TestMapConfig(t *testing.T) { 6 | mc := MapConfig{} 7 | 8 | const configFile = `subscribe pewdiepie 9 | 10 | unsubscribe ( 11 | t-series 12 | )` 13 | 14 | _, err := Parse("test.cfg", []byte(configFile), mc, mc) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | if mc["subscribe"][0] != "pewdiepie" { 20 | t.Errorf("wanted subscribe->pewdiepie, got: %s", mc["subscribe"][0]) 21 | } 22 | 23 | if mc["unsubscribe"][0] != "t-series" { 24 | t.Errorf("wanted unsubscribe->t-series, got: %s", mc["unsubscribe"][0]) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /confyg/print.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Module file printer. 6 | 7 | package confyg 8 | 9 | import ( 10 | "bytes" 11 | "fmt" 12 | "strings" 13 | ) 14 | 15 | func Format(f *FileSyntax) []byte { 16 | pr := &printer{} 17 | pr.file(f) 18 | return pr.Bytes() 19 | } 20 | 21 | // A printer collects the state during printing of a file or expression. 22 | type printer struct { 23 | bytes.Buffer // output buffer 24 | comment []Comment // pending end-of-line comments 25 | margin int // left margin (indent), a number of tabs 26 | } 27 | 28 | // printf prints to the buffer. 29 | func (p *printer) printf(format string, args ...interface{}) { 30 | fmt.Fprintf(p, format, args...) 31 | } 32 | 33 | // indent returns the position on the current line, in bytes, 0-indexed. 34 | func (p *printer) indent() int { 35 | b := p.Bytes() 36 | n := 0 37 | for n < len(b) && b[len(b)-1-n] != '\n' { 38 | n++ 39 | } 40 | return n 41 | } 42 | 43 | // newline ends the current line, flushing end-of-line comments. 44 | func (p *printer) newline() { 45 | if len(p.comment) > 0 { 46 | p.printf(" ") 47 | for i, com := range p.comment { 48 | if i > 0 { 49 | p.trim() 50 | p.printf("\n") 51 | for i := 0; i < p.margin; i++ { 52 | p.printf("\t") 53 | } 54 | } 55 | p.printf("%s", strings.TrimSpace(com.Token)) 56 | } 57 | p.comment = p.comment[:0] 58 | } 59 | 60 | p.trim() 61 | p.printf("\n") 62 | for i := 0; i < p.margin; i++ { 63 | p.printf("\t") 64 | } 65 | } 66 | 67 | // trim removes trailing spaces and tabs from the current line. 68 | func (p *printer) trim() { 69 | // Remove trailing spaces and tabs from line we're about to end. 70 | b := p.Bytes() 71 | n := len(b) 72 | for n > 0 && (b[n-1] == '\t' || b[n-1] == ' ') { 73 | n-- 74 | } 75 | p.Truncate(n) 76 | } 77 | 78 | // file formats the given file into the print buffer. 79 | func (p *printer) file(f *FileSyntax) { 80 | for _, com := range f.Before { 81 | p.printf("%s", strings.TrimSpace(com.Token)) 82 | p.newline() 83 | } 84 | 85 | for i, stmt := range f.Stmt { 86 | switch x := stmt.(type) { 87 | case *CommentBlock: 88 | // comments already handled 89 | p.expr(x) 90 | 91 | default: 92 | p.expr(x) 93 | p.newline() 94 | } 95 | 96 | for _, com := range stmt.Comment().After { 97 | p.printf("%s", strings.TrimSpace(com.Token)) 98 | p.newline() 99 | } 100 | 101 | if i+1 < len(f.Stmt) { 102 | p.newline() 103 | } 104 | } 105 | } 106 | 107 | func (p *printer) expr(x Expr) { 108 | // Emit line-comments preceding this expression. 109 | if before := x.Comment().Before; len(before) > 0 { 110 | // Want to print a line comment. 111 | // Line comments must be at the current margin. 112 | p.trim() 113 | if p.indent() > 0 { 114 | // There's other text on the line. Start a new line. 115 | p.printf("\n") 116 | } 117 | // Re-indent to margin. 118 | for i := 0; i < p.margin; i++ { 119 | p.printf("\t") 120 | } 121 | for _, com := range before { 122 | p.printf("%s", strings.TrimSpace(com.Token)) 123 | p.newline() 124 | } 125 | } 126 | 127 | switch x := x.(type) { 128 | default: 129 | panic(fmt.Errorf("printer: unexpected type %T", x)) 130 | 131 | case *CommentBlock: 132 | // done 133 | 134 | case *LParen: 135 | p.printf("(") 136 | case *RParen: 137 | p.printf(")") 138 | 139 | case *Line: 140 | sep := "" 141 | for _, tok := range x.Token { 142 | p.printf("%s%s", sep, tok) 143 | sep = " " 144 | } 145 | 146 | case *LineBlock: 147 | for _, tok := range x.Token { 148 | p.printf("%s ", tok) 149 | } 150 | p.expr(&x.LParen) 151 | p.margin++ 152 | for _, l := range x.Line { 153 | p.newline() 154 | p.expr(l) 155 | } 156 | p.margin-- 157 | p.newline() 158 | p.expr(&x.RParen) 159 | } 160 | 161 | // Queue end-of-line comments for printing when we 162 | // reach the end of the line. 163 | p.comment = append(p.comment, x.Comment().Suffix...) 164 | } 165 | -------------------------------------------------------------------------------- /confyg/read.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Module file parser. 6 | // This is a simplified copy of Google's buildifier parser. 7 | 8 | package confyg 9 | 10 | import ( 11 | "bytes" 12 | "fmt" 13 | "os" 14 | "strings" 15 | "unicode" 16 | "unicode/utf8" 17 | ) 18 | 19 | // A Position describes the position between two bytes of input. 20 | type Position struct { 21 | Line int // line in input (starting at 1) 22 | LineRune int // rune in line (starting at 1) 23 | Byte int // byte in input (starting at 0) 24 | } 25 | 26 | // add returns the position at the end of s, assuming it starts at p. 27 | func (p Position) add(s string) Position { 28 | p.Byte += len(s) 29 | if n := strings.Count(s, "\n"); n > 0 { 30 | p.Line += n 31 | s = s[strings.LastIndex(s, "\n")+1:] 32 | p.LineRune = 1 33 | } 34 | p.LineRune += utf8.RuneCountInString(s) 35 | return p 36 | } 37 | 38 | // An Expr represents an input element. 39 | type Expr interface { 40 | // Span returns the start and end position of the expression, 41 | // excluding leading or trailing comments. 42 | Span() (start, end Position) 43 | 44 | // Comment returns the comments attached to the expression. 45 | // This method would normally be named 'Comments' but that 46 | // would interfere with embedding a type of the same name. 47 | Comment() *Comments 48 | } 49 | 50 | // A Comment represents a single // comment. 51 | type Comment struct { 52 | Start Position 53 | Token string // without trailing newline 54 | Suffix bool // an end of line (not whole line) comment 55 | } 56 | 57 | // Comments collects the comments associated with an expression. 58 | type Comments struct { 59 | Before []Comment // whole-line comments before this expression 60 | Suffix []Comment // end-of-line comments after this expression 61 | 62 | // For top-level expressions only, After lists whole-line 63 | // comments following the expression. 64 | After []Comment 65 | } 66 | 67 | // Comment returns the receiver. This isn't useful by itself, but 68 | // a Comments struct is embedded into all the expression 69 | // implementation types, and this gives each of those a Comment 70 | // method to satisfy the Expr interface. 71 | func (c *Comments) Comment() *Comments { 72 | return c 73 | } 74 | 75 | // A FileSyntax represents an entire go.mod file. 76 | type FileSyntax struct { 77 | Name string // file path 78 | Comments 79 | Stmt []Expr 80 | } 81 | 82 | func (x *FileSyntax) Span() (start, end Position) { 83 | if len(x.Stmt) == 0 { 84 | return 85 | } 86 | start, _ = x.Stmt[0].Span() 87 | _, end = x.Stmt[len(x.Stmt)-1].Span() 88 | return start, end 89 | } 90 | 91 | // A CommentBlock represents a top-level block of comments separate 92 | // from any rule. 93 | type CommentBlock struct { 94 | Comments 95 | Start Position 96 | } 97 | 98 | func (x *CommentBlock) Span() (start, end Position) { 99 | return x.Start, x.Start 100 | } 101 | 102 | // A Line is a single line of tokens. 103 | type Line struct { 104 | Comments 105 | Start Position 106 | Token []string 107 | End Position 108 | } 109 | 110 | func (x *Line) Span() (start, end Position) { 111 | return x.Start, x.End 112 | } 113 | 114 | // A LineBlock is a factored block of lines, like 115 | // 116 | // require ( 117 | // "x" 118 | // "y" 119 | // ) 120 | type LineBlock struct { 121 | Comments 122 | Start Position 123 | LParen LParen 124 | Token []string 125 | Line []*Line 126 | RParen RParen 127 | } 128 | 129 | func (x *LineBlock) Span() (start, end Position) { 130 | return x.Start, x.RParen.Pos.add(")") 131 | } 132 | 133 | // An LParen represents the beginning of a parenthesized line block. 134 | // It is a place to store suffix comments. 135 | type LParen struct { 136 | Comments 137 | Pos Position 138 | } 139 | 140 | func (x *LParen) Span() (start, end Position) { 141 | return x.Pos, x.Pos.add(")") 142 | } 143 | 144 | // An RParen represents the end of a parenthesized line block. 145 | // It is a place to store whole-line (before) comments. 146 | type RParen struct { 147 | Comments 148 | Pos Position 149 | } 150 | 151 | func (x *RParen) Span() (start, end Position) { 152 | return x.Pos, x.Pos.add(")") 153 | } 154 | 155 | // An input represents a single input file being parsed. 156 | type input struct { 157 | // Lexing state. 158 | filename string // name of input file, for errors 159 | complete []byte // entire input 160 | remaining []byte // remaining input 161 | token []byte // token being scanned 162 | lastToken string // most recently returned token, for error messages 163 | pos Position // current input position 164 | comments []Comment // accumulated comments 165 | 166 | // Parser state. 167 | file *FileSyntax // returned top-level syntax tree 168 | parseError error // error encountered during parsing 169 | 170 | // Comment assignment state. 171 | pre []Expr // all expressions, in preorder traversal 172 | post []Expr // all expressions, in postorder traversal 173 | } 174 | 175 | func newInput(filename string, data []byte) *input { 176 | return &input{ 177 | filename: filename, 178 | complete: data, 179 | remaining: data, 180 | pos: Position{Line: 1, LineRune: 1, Byte: 0}, 181 | } 182 | } 183 | 184 | // parse parses the input file. 185 | func parse(file string, data []byte) (f *FileSyntax, err error) { 186 | in := newInput(file, data) 187 | // The parser panics for both routine errors like syntax errors 188 | // and for programmer bugs like array index errors. 189 | // Turn both into error returns. Catching bug panics is 190 | // especially important when processing many files. 191 | defer func() { 192 | if e := recover(); e != nil { 193 | if e == in.parseError { 194 | err = in.parseError 195 | } else { 196 | err = fmt.Errorf("%s:%d:%d: internal error: %v", in.filename, in.pos.Line, in.pos.LineRune, e) 197 | } 198 | } 199 | }() 200 | 201 | // Invoke the parser. 202 | in.parseFile() 203 | if in.parseError != nil { 204 | return nil, in.parseError 205 | } 206 | in.file.Name = in.filename 207 | 208 | // Assign comments to nearby syntax. 209 | in.assignComments() 210 | 211 | return in.file, nil 212 | } 213 | 214 | // Error is called to report an error. 215 | // The reason s is often "syntax error". 216 | // Error does not return: it panics. 217 | func (in *input) Error(s string) { 218 | if s == "syntax error" && in.lastToken != "" { 219 | s += " near " + in.lastToken 220 | } 221 | in.parseError = fmt.Errorf("%s:%d:%d: %v", in.filename, in.pos.Line, in.pos.LineRune, s) 222 | panic(in.parseError) 223 | } 224 | 225 | // eof reports whether the input has reached end of file. 226 | func (in *input) eof() bool { 227 | return len(in.remaining) == 0 228 | } 229 | 230 | // peekRune returns the next rune in the input without consuming it. 231 | func (in *input) peekRune() int { 232 | if len(in.remaining) == 0 { 233 | return 0 234 | } 235 | r, _ := utf8.DecodeRune(in.remaining) 236 | return int(r) 237 | } 238 | 239 | // readRune consumes and returns the next rune in the input. 240 | func (in *input) readRune() int { 241 | if len(in.remaining) == 0 { 242 | in.Error("internal lexer error: readRune at EOF") 243 | } 244 | r, size := utf8.DecodeRune(in.remaining) 245 | in.remaining = in.remaining[size:] 246 | if r == '\n' { 247 | in.pos.Line++ 248 | in.pos.LineRune = 1 249 | } else { 250 | in.pos.LineRune++ 251 | } 252 | in.pos.Byte += size 253 | return int(r) 254 | } 255 | 256 | type symType struct { 257 | pos Position 258 | endPos Position 259 | text string 260 | } 261 | 262 | // startToken marks the beginning of the next input token. 263 | // It must be followed by a call to endToken, once the token has 264 | // been consumed using readRune. 265 | func (in *input) startToken(sym *symType) { 266 | in.token = in.remaining 267 | sym.text = "" 268 | sym.pos = in.pos 269 | } 270 | 271 | // endToken marks the end of an input token. 272 | // It records the actual token string in sym.text if the caller 273 | // has not done that already. 274 | func (in *input) endToken(sym *symType) { 275 | if sym.text == "" { 276 | tok := string(in.token[:len(in.token)-len(in.remaining)]) 277 | sym.text = tok 278 | in.lastToken = sym.text 279 | } 280 | sym.endPos = in.pos 281 | } 282 | 283 | // lex is called from the parser to obtain the next input token. 284 | // It returns the token value (either a rune like '+' or a symbolic token _FOR) 285 | // and sets val to the data associated with the token. 286 | // For all our input tokens, the associated data is 287 | // val.Pos (the position where the token begins) 288 | // and val.Token (the input string corresponding to the token). 289 | func (in *input) lex(sym *symType) int { 290 | // Skip past spaces, stopping at non-space or EOF. 291 | countNL := 0 // number of newlines we've skipped past 292 | for !in.eof() { 293 | // Skip over spaces. Count newlines so we can give the parser 294 | // information about where top-level blank lines are, 295 | // for top-level comment assignment. 296 | c := in.peekRune() 297 | if c == ' ' || c == '\t' || c == '\r' { 298 | in.readRune() 299 | continue 300 | } 301 | 302 | // Comment runs to end of line. 303 | if c == '#' { 304 | in.startToken(sym) 305 | 306 | // Is this comment the only thing on its line? 307 | // Find the last \n before this // and see if it's all 308 | // spaces from there to here. 309 | i := bytes.LastIndex(in.complete[:in.pos.Byte], []byte("\n")) 310 | suffix := len(bytes.TrimSpace(in.complete[i+1:in.pos.Byte])) > 0 311 | in.readRune() 312 | c = in.peekRune() 313 | if c != '#' { 314 | in.Error(fmt.Sprintf("unexpected input character %#q", c)) 315 | } 316 | 317 | // Consume comment. 318 | for len(in.remaining) > 0 && in.readRune() != '\n' { 319 | } 320 | in.endToken(sym) 321 | 322 | sym.text = strings.TrimRight(sym.text, "\n") 323 | in.lastToken = "comment" 324 | 325 | // If we are at top level (not in a statement), hand the comment to 326 | // the parser as a _COMMENT token. The grammar is written 327 | // to handle top-level comments itself. 328 | if !suffix { 329 | // Not in a statement. Tell parser about top-level comment. 330 | return _COMMENT 331 | } 332 | 333 | // Otherwise, save comment for later attachment to syntax tree. 334 | if countNL > 1 { 335 | in.comments = append(in.comments, Comment{sym.pos, "", false}) 336 | } 337 | in.comments = append(in.comments, Comment{sym.pos, sym.text, suffix}) 338 | countNL = 1 339 | return _EOL 340 | } 341 | 342 | // Found non-space non-comment. 343 | break 344 | } 345 | 346 | // Found the beginning of the next token. 347 | in.startToken(sym) 348 | defer in.endToken(sym) 349 | 350 | // End of file. 351 | if in.eof() { 352 | in.lastToken = "EOF" 353 | return _EOF 354 | } 355 | 356 | // Punctuation tokens. 357 | switch c := in.peekRune(); c { 358 | case '\n': 359 | in.readRune() 360 | return c 361 | 362 | case '(': 363 | in.readRune() 364 | return c 365 | 366 | case ')': 367 | in.readRune() 368 | return c 369 | 370 | case '"', '`': // quoted string 371 | quote := c 372 | in.readRune() 373 | for { 374 | if in.eof() { 375 | in.pos = sym.pos 376 | in.Error("unexpected EOF in string") 377 | } 378 | if in.peekRune() == '\n' { 379 | in.Error("unexpected newline in string") 380 | } 381 | c := in.readRune() 382 | if c == quote { 383 | break 384 | } 385 | if c == '\\' && quote != '`' { 386 | if in.eof() { 387 | in.pos = sym.pos 388 | in.Error("unexpected EOF in string") 389 | } 390 | in.readRune() 391 | } 392 | } 393 | in.endToken(sym) 394 | return _STRING 395 | } 396 | 397 | // Checked all punctuation. Must be identifier token. 398 | if c := in.peekRune(); !isIdent(c) { 399 | in.Error(fmt.Sprintf("unexpected input character %#q", c)) 400 | } 401 | 402 | // Scan over identifier. 403 | for isIdent(in.peekRune()) { 404 | in.readRune() 405 | } 406 | return _IDENT 407 | } 408 | 409 | // isIdent reports whether c is an identifier rune. 410 | // We treat nearly all runes as identifier runes. 411 | func isIdent(c int) bool { 412 | return c != 0 && !unicode.IsSpace(rune(c)) && c != '(' && c != ')' && c != '"' && c != '`' 413 | } 414 | 415 | // Comment assignment. 416 | // We build two lists of all subexpressions, preorder and postorder. 417 | // The preorder list is ordered by start location, with outer expressions first. 418 | // The postorder list is ordered by end location, with outer expressions last. 419 | // We use the preorder list to assign each whole-line comment to the syntax 420 | // immediately following it, and we use the postorder list to assign each 421 | // end-of-line comment to the syntax immediately preceding it. 422 | 423 | // order walks the expression adding it and its subexpressions to the 424 | // preorder and postorder lists. 425 | func (in *input) order(x Expr) { 426 | if x != nil { 427 | in.pre = append(in.pre, x) 428 | } 429 | switch x := x.(type) { 430 | default: 431 | panic(fmt.Errorf("order: unexpected type %T", x)) 432 | case nil: 433 | // nothing 434 | case *LParen, *RParen: 435 | // nothing 436 | case *CommentBlock: 437 | // nothing 438 | case *Line: 439 | // nothing 440 | case *FileSyntax: 441 | for _, stmt := range x.Stmt { 442 | in.order(stmt) 443 | } 444 | case *LineBlock: 445 | in.order(&x.LParen) 446 | for _, l := range x.Line { 447 | in.order(l) 448 | } 449 | in.order(&x.RParen) 450 | } 451 | if x != nil { 452 | in.post = append(in.post, x) 453 | } 454 | } 455 | 456 | // assignComments attaches comments to nearby syntax. 457 | func (in *input) assignComments() { 458 | const debug = false 459 | 460 | // Generate preorder and postorder lists. 461 | in.order(in.file) 462 | 463 | // Split into whole-line comments and suffix comments. 464 | var line, suffix []Comment 465 | for _, com := range in.comments { 466 | if com.Suffix { 467 | suffix = append(suffix, com) 468 | } else { 469 | line = append(line, com) 470 | } 471 | } 472 | 473 | if debug { 474 | for _, c := range line { 475 | fmt.Fprintf(os.Stderr, "LINE %q :%d:%d #%d\n", c.Token, c.Start.Line, c.Start.LineRune, c.Start.Byte) 476 | } 477 | } 478 | 479 | // Assign line comments to syntax immediately following. 480 | for _, x := range in.pre { 481 | start, _ := x.Span() 482 | if debug { 483 | fmt.Printf("pre %T :%d:%d #%d\n", x, start.Line, start.LineRune, start.Byte) 484 | } 485 | xcom := x.Comment() 486 | for len(line) > 0 && start.Byte >= line[0].Start.Byte { 487 | if debug { 488 | fmt.Fprintf(os.Stderr, "ASSIGN LINE %q #%d\n", line[0].Token, line[0].Start.Byte) 489 | } 490 | xcom.Before = append(xcom.Before, line[0]) 491 | line = line[1:] 492 | } 493 | } 494 | 495 | // Remaining line comments go at end of file. 496 | in.file.After = append(in.file.After, line...) 497 | 498 | if debug { 499 | for _, c := range suffix { 500 | fmt.Fprintf(os.Stderr, "SUFFIX %q :%d:%d #%d\n", c.Token, c.Start.Line, c.Start.LineRune, c.Start.Byte) 501 | } 502 | } 503 | 504 | // Assign suffix comments to syntax immediately before. 505 | for i := len(in.post) - 1; i >= 0; i-- { 506 | x := in.post[i] 507 | 508 | start, end := x.Span() 509 | if debug { 510 | fmt.Printf("post %T :%d:%d #%d :%d:%d #%d\n", x, start.Line, start.LineRune, start.Byte, end.Line, end.LineRune, end.Byte) 511 | } 512 | 513 | // Do not assign suffix comments to end of line block or whole file. 514 | // Instead assign them to the last element inside. 515 | switch x.(type) { 516 | case *FileSyntax: 517 | continue 518 | } 519 | 520 | // Do not assign suffix comments to something that starts 521 | // on an earlier line, so that in 522 | // 523 | // x ( y 524 | // z ) // comment 525 | // 526 | // we assign the comment to z and not to x ( ... ). 527 | if start.Line != end.Line { 528 | continue 529 | } 530 | xcom := x.Comment() 531 | for len(suffix) > 0 && end.Byte <= suffix[len(suffix)-1].Start.Byte { 532 | if debug { 533 | fmt.Fprintf(os.Stderr, "ASSIGN SUFFIX %q #%d\n", suffix[len(suffix)-1].Token, suffix[len(suffix)-1].Start.Byte) 534 | } 535 | xcom.Suffix = append(xcom.Suffix, suffix[len(suffix)-1]) 536 | suffix = suffix[:len(suffix)-1] 537 | } 538 | } 539 | 540 | // We assigned suffix comments in reverse. 541 | // If multiple suffix comments were appended to the same 542 | // expression node, they are now in reverse. Fix that. 543 | for _, x := range in.post { 544 | reverseComments(x.Comment().Suffix) 545 | } 546 | 547 | // Remaining suffix comments go at beginning of file. 548 | in.file.Before = append(in.file.Before, suffix...) 549 | } 550 | 551 | // reverseComments reverses the []Comment list. 552 | func reverseComments(list []Comment) { 553 | for i, j := 0, len(list)-1; i < j; i, j = i+1, j-1 { 554 | list[i], list[j] = list[j], list[i] 555 | } 556 | } 557 | 558 | func (in *input) parseFile() { 559 | in.file = new(FileSyntax) 560 | var sym symType 561 | var cb *CommentBlock 562 | for { 563 | tok := in.lex(&sym) 564 | switch tok { 565 | case '\n': 566 | if cb != nil { 567 | in.file.Stmt = append(in.file.Stmt, cb) 568 | cb = nil 569 | } 570 | case _COMMENT: 571 | if cb == nil { 572 | cb = &CommentBlock{Start: sym.pos} 573 | } 574 | com := cb.Comment() 575 | com.Before = append(com.Before, Comment{Start: sym.pos, Token: sym.text}) 576 | case _EOF: 577 | if cb != nil { 578 | in.file.Stmt = append(in.file.Stmt, cb) 579 | } 580 | return 581 | default: 582 | in.parseStmt(&sym) 583 | if cb != nil { 584 | in.file.Stmt[len(in.file.Stmt)-1].Comment().Before = cb.Before 585 | cb = nil 586 | } 587 | } 588 | } 589 | } 590 | 591 | func (in *input) parseStmt(sym *symType) { 592 | start := sym.pos 593 | end := sym.endPos 594 | token := []string{sym.text} 595 | for { 596 | tok := in.lex(sym) 597 | switch tok { 598 | case '\n', _EOF, _EOL: 599 | in.file.Stmt = append(in.file.Stmt, &Line{ 600 | Start: start, 601 | Token: token, 602 | End: end, 603 | }) 604 | return 605 | case '(': 606 | in.file.Stmt = append(in.file.Stmt, in.parseLineBlock(start, token, sym)) 607 | return 608 | default: 609 | token = append(token, sym.text) 610 | end = sym.endPos 611 | } 612 | } 613 | } 614 | 615 | func (in *input) parseLineBlock(start Position, token []string, sym *symType) *LineBlock { 616 | x := &LineBlock{ 617 | Start: start, 618 | Token: token, 619 | LParen: LParen{Pos: sym.pos}, 620 | } 621 | var comments []Comment 622 | for { 623 | tok := in.lex(sym) 624 | switch tok { 625 | case _EOL: 626 | // ignore 627 | case '\n': 628 | if len(comments) == 0 && len(x.Line) > 0 || len(comments) > 0 && comments[len(comments)-1].Token != "" { 629 | comments = append(comments, Comment{}) 630 | } 631 | case _COMMENT: 632 | comments = append(comments, Comment{Start: sym.pos, Token: sym.text}) 633 | case _EOF: 634 | in.Error(fmt.Sprintf("syntax error (unterminated block started at %s:%d:%d)", in.filename, x.Start.Line, x.Start.LineRune)) 635 | case ')': 636 | x.RParen.Before = comments 637 | x.RParen.Pos = sym.pos 638 | tok = in.lex(sym) 639 | if tok != '\n' && tok != _EOF && tok != _EOL { 640 | in.Error("syntax error (expected newline after closing paren)") 641 | } 642 | return x 643 | default: 644 | l := in.parseLine(sym) 645 | x.Line = append(x.Line, l) 646 | l.Comment().Before = comments 647 | comments = nil 648 | } 649 | } 650 | } 651 | 652 | func (in *input) parseLine(sym *symType) *Line { 653 | start := sym.pos 654 | end := sym.endPos 655 | token := []string{sym.text} 656 | for { 657 | tok := in.lex(sym) 658 | switch tok { 659 | case '\n', _EOF, _EOL: 660 | return &Line{ 661 | Start: start, 662 | Token: token, 663 | End: end, 664 | } 665 | default: 666 | token = append(token, sym.text) 667 | end = sym.endPos 668 | } 669 | } 670 | } 671 | 672 | const ( 673 | _EOF = -(1 + iota) 674 | _EOL 675 | _IDENT 676 | _STRING 677 | _COMMENT 678 | ) 679 | -------------------------------------------------------------------------------- /confyg/read_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package confyg 6 | 7 | import ( 8 | "bytes" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "testing" 13 | ) 14 | 15 | // Test that reading and then writing the golden files 16 | // does not change their output. 17 | func TestPrintGolden(t *testing.T) { 18 | outs, err := filepath.Glob("testdata/*.golden") 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | for _, out := range outs { 23 | testPrint(t, out, out) 24 | } 25 | } 26 | 27 | // testPrint is a helper for testing the printer. 28 | // It reads the file named in, reformats it, and compares 29 | // the result to the file named out. 30 | func testPrint(t *testing.T, in, out string) { 31 | data, err := os.ReadFile(in) 32 | if err != nil { 33 | t.Error(err) 34 | return 35 | } 36 | 37 | golden, err := os.ReadFile(out) 38 | if err != nil { 39 | t.Error(err) 40 | return 41 | } 42 | 43 | base := "testdata/" + filepath.Base(in) 44 | f, err := parse(in, data) 45 | if err != nil { 46 | t.Error(err) 47 | return 48 | } 49 | 50 | ndata := Format(f) 51 | 52 | if !bytes.Equal(ndata, golden) { 53 | t.Errorf("formatted %s incorrectly: diff shows -golden, +ours", base) 54 | tdiff(t, string(golden), string(ndata)) 55 | return 56 | } 57 | } 58 | 59 | // diff returns the output of running diff on b1 and b2. 60 | func diff(b1, b2 []byte) (data []byte, err error) { 61 | f1, err := os.CreateTemp("", "testdiff") 62 | if err != nil { 63 | return nil, err 64 | } 65 | defer os.Remove(f1.Name()) 66 | defer f1.Close() 67 | 68 | f2, err := os.CreateTemp("", "testdiff") 69 | if err != nil { 70 | return nil, err 71 | } 72 | defer os.Remove(f2.Name()) 73 | defer f2.Close() 74 | 75 | f1.Write(b1) 76 | f2.Write(b2) 77 | 78 | data, err = exec.Command("diff", "-u", f1.Name(), f2.Name()).CombinedOutput() 79 | if len(data) > 0 { 80 | // diff exits with a non-zero status when the files don't match. 81 | // Ignore that failure as long as we get output. 82 | err = nil 83 | } 84 | return 85 | } 86 | 87 | // tdiff logs the diff output to t.Error. 88 | func tdiff(t *testing.T, a, b string) { 89 | data, err := diff([]byte(a), []byte(b)) 90 | if err != nil { 91 | t.Error(err) 92 | return 93 | } 94 | t.Error(string(data)) 95 | } 96 | -------------------------------------------------------------------------------- /confyg/reader.go: -------------------------------------------------------------------------------- 1 | package confyg 2 | 3 | import "bytes" 4 | 5 | // Reader is called when individual lines of the configuration file are being read. 6 | // This is where you should populate any relevant structures with information. 7 | // 8 | // If something goes wrong in the file parsing step, add data to the errs buffer 9 | // describing what went wrong. 10 | type Reader interface { 11 | Read(errs *bytes.Buffer, fs *FileSyntax, line *Line, verb string, args []string) 12 | } 13 | 14 | // ReaderFunc implements Reader for inline definitions. 15 | type ReaderFunc func(errs *bytes.Buffer, fs *FileSyntax, line *Line, verb string, args []string) 16 | 17 | func (r ReaderFunc) Read(errs *bytes.Buffer, fs *FileSyntax, line *Line, verb string, args []string) { 18 | r(errs, fs, line, verb, args) 19 | } 20 | -------------------------------------------------------------------------------- /confyg/reader_test.go: -------------------------------------------------------------------------------- 1 | package confyg 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestReader(t *testing.T) { 10 | done := false 11 | acc := 0 12 | 13 | al := AllowerFunc(func(verb string, block bool) bool { 14 | switch verb { 15 | case "test": 16 | return !block 17 | 18 | case "acc": 19 | return true 20 | default: 21 | return false 22 | } 23 | }) 24 | 25 | r := ReaderFunc(func(errs *bytes.Buffer, fs *FileSyntax, line *Line, verb string, args []string) { 26 | switch verb { 27 | case "test": 28 | done = len(args) == 1 29 | case "acc": 30 | acc++ 31 | default: 32 | fmt.Fprintf(errs, "%s:%d unknown verb %s\n", fs.Name, line.Start.Line, verb) 33 | } 34 | }) 35 | const configFile = `test "42" 36 | 37 | acc ( 38 | 1 39 | 2 40 | 3 41 | )` 42 | 43 | fs, err := Parse("test.cfg", []byte(configFile), r, al) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | _ = fs 49 | 50 | t.Logf("done: %v", done) 51 | if !done { 52 | t.Fatal("done was not flagged") 53 | } 54 | 55 | t.Logf("acc: %v", acc) 56 | if acc != 3 { 57 | t.Fatal("acc was not changed") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /confyg/rule.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package confyg 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "fmt" 11 | "strings" 12 | ) 13 | 14 | func Parse(file string, data []byte, r Reader, al Allower) (*FileSyntax, error) { 15 | fs, err := parse(file, data) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | var errs bytes.Buffer 21 | for _, x := range fs.Stmt { 22 | switch x := x.(type) { 23 | case *Line: 24 | ok := al.Allow(x.Token[0], false) 25 | if ok { 26 | r.Read(&errs, fs, x, x.Token[0], x.Token[1:]) 27 | continue 28 | } 29 | 30 | fmt.Fprintf(&errs, "%s:%d: can't allow line verb %s", file, x.Start.Line, x.Token[0]) 31 | 32 | case *LineBlock: 33 | if len(x.Token) > 1 { 34 | fmt.Fprintf(&errs, "%s:%d: unknown block type: %s\n", file, x.Start.Line, strings.Join(x.Token, " ")) 35 | continue 36 | } 37 | ok := al.Allow(x.Token[0], true) 38 | if ok { 39 | for _, l := range x.Line { 40 | r.Read(&errs, fs, l, x.Token[0], l.Token) 41 | } 42 | continue 43 | } 44 | 45 | fmt.Fprintf(&errs, "%s:%d: can't allow line block verb %s", file, x.Start.Line, x.Token[0]) 46 | } 47 | } 48 | 49 | if errs.Len() > 0 { 50 | return nil, errors.New(strings.TrimRight(errs.String(), "\n")) 51 | } 52 | return fs, nil 53 | } 54 | -------------------------------------------------------------------------------- /confyg/testdata/block.golden: -------------------------------------------------------------------------------- 1 | ## comment 2 | x "y" z 3 | 4 | ## block 5 | block ( ## block-eol 6 | ## x-before-line 7 | 8 | "x" ( y ## x-eol 9 | "x1" 10 | "x2" 11 | ## line 12 | "x3" 13 | "x4" 14 | 15 | "x5" 16 | 17 | ## y-line 18 | "y" ## y-eol 19 | 20 | "z" ## z-eol 21 | ) ## block-eol2 22 | 23 | block2 ( 24 | x 25 | y 26 | z 27 | ) 28 | 29 | ## eof 30 | -------------------------------------------------------------------------------- /confyg/testdata/block.in: -------------------------------------------------------------------------------- 1 | ## comment 2 | x "y" z 3 | 4 | ## block 5 | block ( ## block-eol 6 | ## x-before-line 7 | 8 | "x" ( y ## x-eol 9 | "x1" 10 | "x2" 11 | ## line 12 | "x3" 13 | "x4" 14 | 15 | "x5" 16 | 17 | ## y-line 18 | "y" ## y-eol 19 | 20 | "z" ## z-eol 21 | ) ## block-eol2 22 | 23 | 24 | block2 (x 25 | y 26 | z 27 | ) 28 | 29 | ## eof 30 | -------------------------------------------------------------------------------- /confyg/testdata/comment.golden: -------------------------------------------------------------------------------- 1 | ## comment 2 | module "x" ## eol 3 | 4 | ## mid comment 5 | 6 | ## comment 2 7 | ## comment 2 line 2 8 | module "y" ## eoy 9 | 10 | ## comment 3 11 | -------------------------------------------------------------------------------- /confyg/testdata/comment.in: -------------------------------------------------------------------------------- 1 | ## comment 2 | module "x" ## eol 3 | ## mid comment 4 | 5 | ## comment 2 6 | ## comment 2 line 2 7 | module "y" ## eoy 8 | ## comment 3 9 | -------------------------------------------------------------------------------- /confyg/testdata/empty.golden: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TecharoHQ/yeet/955c4bbd100af4e859ef6affd0994cb46cafcc1e/confyg/testdata/empty.golden -------------------------------------------------------------------------------- /confyg/testdata/empty.in: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TecharoHQ/yeet/955c4bbd100af4e859ef6affd0994cb46cafcc1e/confyg/testdata/empty.in -------------------------------------------------------------------------------- /confyg/testdata/module.golden: -------------------------------------------------------------------------------- 1 | module "abc" 2 | -------------------------------------------------------------------------------- /confyg/testdata/module.in: -------------------------------------------------------------------------------- 1 | module "abc" 2 | -------------------------------------------------------------------------------- /confyg/testdata/replace.golden: -------------------------------------------------------------------------------- 1 | module "abc" 2 | 3 | replace "xyz" v1.2.3 => "/tmp/z" 4 | 5 | replace "xyz" v1.3.4 => "my/xyz" v1.3.4-me 6 | -------------------------------------------------------------------------------- /confyg/testdata/replace.in: -------------------------------------------------------------------------------- 1 | module "abc" 2 | 3 | replace "xyz" v1.2.3 => "/tmp/z" 4 | 5 | replace "xyz" v1.3.4 => "my/xyz" v1.3.4-me 6 | -------------------------------------------------------------------------------- /confyg/testdata/replace2.golden: -------------------------------------------------------------------------------- 1 | module "abc" 2 | 3 | replace ( 4 | "xyz" v1.2.3 => "/tmp/z" 5 | "xyz" v1.3.4 => "my/xyz" v1.3.4-me 6 | ) 7 | -------------------------------------------------------------------------------- /confyg/testdata/replace2.in: -------------------------------------------------------------------------------- 1 | module "abc" 2 | 3 | replace ( 4 | "xyz" v1.2.3 => "/tmp/z" 5 | "xyz" v1.3.4 => "my/xyz" v1.3.4-me 6 | ) 7 | -------------------------------------------------------------------------------- /confyg/testdata/rule1.golden: -------------------------------------------------------------------------------- 1 | module "x" 2 | 3 | module "y" 4 | 5 | require "x" 6 | 7 | require x 8 | -------------------------------------------------------------------------------- /confyg/testdata/url.golden: -------------------------------------------------------------------------------- 1 | url https://foo.bar 2 | -------------------------------------------------------------------------------- /doc/api.md: -------------------------------------------------------------------------------- 1 | # Yeetfile API 2 | 3 | Yeet uses [goja](https://pkg.go.dev/github.com/dop251/goja#section-readme) to execute JavaScript. As such, it does not have access to NPM or other external JavaScript libraries. You also cannot import code/data from other files. These are not planned for inclusion into yeet. If functionality is required, it should be added to yeet itself. 4 | 5 | To make it useful, yeet exposes a bunch of helper objects full of tools. These tools fall in a few categories, each has its own section. 6 | 7 | ## `$` 8 | 9 | `$` lets you construct shell commands using [tagged templates](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals). This lets you build whatever shell commands you want by mixing Go and JavaScript values freely. 10 | 11 | Example: 12 | 13 | ```js 14 | $`CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -s -w -extldflags "-static" -X "within.website/x.Version=${git.tag()}"`; 15 | ``` 16 | 17 | ## `deb` 18 | 19 | Helpers for building Debian packages. 20 | 21 | ### `deb.build` 22 | 23 | Builds a Debian package with a descriptor object. See the native packages section for more information. The important part of this is your `build` function. The `build` function is what will turn your package source code into an executable in `out` somehow. 24 | 25 | The resulting Debian package path will be returned as a string. 26 | 27 | Usage: 28 | 29 | `deb.build(package);` 30 | 31 | ```js 32 | ["amd64", "arm64"].forEach((goarch) => 33 | deb.build({ 34 | name: "yeet", 35 | description: "Yeet out actions with maximum haste!", 36 | homepage: "https://techaro.lol", 37 | license: "MIT", 38 | goarch, 39 | 40 | build: ({ bin }) => { 41 | go.build("-o", `${bin}/yeet`, "./cmd/yeet"); 42 | }, 43 | }), 44 | ); 45 | ``` 46 | 47 | ## `docker` 48 | 49 | Aliases for `docker` commands. 50 | 51 | ### `docker.build` 52 | 53 | An alias for the `docker build` command. Builds a docker image in the current working directory's Dockerfile. 54 | 55 | Usage: 56 | 57 | `docker.build(tag);` 58 | 59 | ```js 60 | docker.build("ghcr.io/xe/site/bin"); 61 | docker.push("ghcr.io/xe/site/bin"); 62 | ``` 63 | 64 | ### `docker.push` 65 | 66 | Pushes a docker image to a registry. Analogous to `docker push` in the CLI. 67 | 68 | Usage: 69 | 70 | `docker.push(tag);` 71 | 72 | ```js 73 | docker.build("ghcr.io/xe/site/bin"); 74 | docker.push("ghcr.io/xe/site/bin"); 75 | ``` 76 | 77 | ## `file` 78 | 79 | ### `file.install` 80 | 81 | Copies from a file from one place to another whilst preserving the file mode, analogous to `install -d` on Linux. Automatically creates directories in the `dest` path if they don't exist already. 82 | 83 | Usage: 84 | 85 | `file.install(src, dest);` 86 | 87 | ```js 88 | file.install("LICENSE", `${doc}/LICENSE`); 89 | ``` 90 | 91 | ## `git` 92 | 93 | Helpers for the Git version control system. 94 | 95 | ### `git.repoRoot` 96 | 97 | Returns the repository root as a string. 98 | 99 | `git.repoRoot();` 100 | 101 | ```js 102 | const repoRoot = git.repoRoot(); 103 | 104 | file.copy(`${repoRoot}/LICENSE`, `${doc}/LICENSE`); 105 | ``` 106 | 107 | ### `git.tag` 108 | 109 | Returns the output of `git describe --tags`. Useful for getting the "current version" of the repo, where the current version will likely be different forward in time than it is backwards in time. 110 | 111 | Usage: 112 | 113 | `git.tag();` 114 | 115 | ```js 116 | const version = git.tag(); 117 | ``` 118 | 119 | ## `gitea` 120 | 121 | Helpers for integrating with Gitea servers. 122 | 123 | ### `gitea.uploadPackage` 124 | 125 | Uploads a binary package to Gitea, silently failing if the package is not a `.deb` or `.rpm` file. Gitea configuration is done with flags or the configuration file. 126 | 127 | Usage: 128 | 129 | `gitea.uploadPackage(owner, distro, component, fname)` 130 | 131 | ```js 132 | gitea.uploadPackage( 133 | "Techaro", 134 | "yeet", 135 | "unstable", 136 | "./var/yeet-0.0.8.x86_64.rpm", 137 | ); 138 | ``` 139 | 140 | ## `go` 141 | 142 | Helpers for the Go programming language. 143 | 144 | ### `go.build` 145 | 146 | Runs `go build` in the current working directory with any extra arguments passed in. This is useful for building and installing Go programs in an RPM build context. 147 | 148 | Usage: 149 | 150 | `go.build(args);` 151 | 152 | ```js 153 | go.build("-o", `${out}/usr/bin/`); 154 | ``` 155 | 156 | ### `go.install` 157 | 158 | Runs `go install`. Not useful for cross-compilation. 159 | 160 | Usage: 161 | 162 | `go.install();` 163 | 164 | ```js 165 | go.install(); 166 | ``` 167 | 168 | ## `log` 169 | 170 | Logging functions. 171 | 172 | ### `log.println` 173 | 174 | Prints log data to standard output. 175 | 176 | Usage: 177 | 178 | `log.println(...);` 179 | 180 | ```js 181 | log.println(`built package ${pkgPath}`); 182 | ``` 183 | 184 | ## `rpm` 185 | 186 | Helpers for building RPM packages and docker images out of a constellation of RPM packages. 187 | 188 | ### `rpm.build` 189 | 190 | Builds an RPM package with a descriptor object. See the RPM packages section for more information. The important part of this is your `build` function. The `build` function is what will turn your package source code into an executable in `out` somehow. Everything in `out` corresponds 1:1 with paths in the resulting RPM. 191 | 192 | The resulting RPM path will be returned as a string. 193 | 194 | Usage: 195 | 196 | `rpm.build(package);` 197 | 198 | ```js 199 | ["amd64", "arm64"].forEach((goarch) => 200 | rpm.build({ 201 | name: "yeet", 202 | description: "Yeet out actions with maximum haste!", 203 | homepage: "https://techaro.lol", 204 | license: "MIT", 205 | goarch, 206 | 207 | build: ({ bin }) => { 208 | go.build("-o", `${bin}/yeet`, "./cmd/yeet"); 209 | }, 210 | }), 211 | ); 212 | ``` 213 | 214 | ## `yeet` 215 | 216 | This contains various "other" functions that don't have a good place to put them. 217 | 218 | ### `yeet.cwd` 219 | 220 | The current working directory. This is a constant value and is not updated at runtime. 221 | 222 | Usage: 223 | 224 | ```js 225 | log.println(yeet.cwd); 226 | ``` 227 | 228 | ### `yeet.dateTag` 229 | 230 | A constant string representing the time that yeet was started in UTC. It is formatted in terms of `YYYYmmDDhhMM`. This is not updated at runtime. You can use it for a "unique" value per invocation of yeet (assuming you aren't a time traveler). 231 | 232 | Usage: 233 | 234 | ```js 235 | docker.build(`ghcr.io/xe/site/bin:${git.tag()}-${yeet.dateTag}`); 236 | ``` 237 | 238 | ### `yeet.getenv` 239 | 240 | Gets an environment variable and returns it as a string, optionally returning an empty string if the variable is not found. 241 | 242 | Usage: 243 | 244 | `yeet.getenv(name);` 245 | 246 | ```js 247 | const someValue = yeet.getenv("SOME_VALUE"); 248 | ``` 249 | 250 | ### `yeet.run` / `yeet.runcmd` 251 | 252 | Runs an arbitrary command and returns any output as a string. 253 | 254 | Usage: 255 | 256 | `yeet.run(cmd, arg1, arg2, ...);` 257 | 258 | ```js 259 | yeet.run( 260 | "protoc", 261 | "--proto-path=.", 262 | `--proto-path=${git.repoRoot()}/proto`, 263 | "foo.proto", 264 | ); 265 | ``` 266 | 267 | ### `yeet.setenv` 268 | 269 | Sets an environment variable for the process yeet is running in and all children. 270 | 271 | Usage: 272 | 273 | `yeet.setenv(key, val);` 274 | 275 | ```js 276 | yeet.setenv("GOOS", "linux"); 277 | ``` 278 | 279 | ### `yeet.goos` / `yeet.goarch` 280 | 281 | The GOOS/GOARCH value that yeet was built for. This typically corresponds with the OS and CPU architecture that yeet is running on. 282 | 283 | ## Building native packages 284 | 285 | When using the `deb.build`, `rpm.build`, or `tarball.build` functions, you can create native packages from arbitrary yeet expressions. This allows you to cross-compile native packages from a macOS or other Linux system. As an example, here is how the yeet packages are built: 286 | 287 | ```js 288 | ["amd64", "arm64"].forEach((goarch) => 289 | [deb, rpm, tarball].forEach((method) => 290 | method.build({ 291 | name: "yeet", 292 | description: "Yeet out scripts with maximum haste!", 293 | homepage: "https://techaro.lol", 294 | license: "MIT", 295 | goarch, 296 | 297 | build: ({ bin }) => { 298 | go.build("-o", `${bin}/yeet`, "./cmd/yeet"); 299 | }, 300 | }), 301 | ), 302 | ); 303 | ``` 304 | 305 | ### Build settings 306 | 307 | The following settings are supported: 308 | 309 | | Name | Example | Description | 310 | | :-------------- | :----------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 311 | | `name` | `xeiaso.net-yeet` | The name of the package. This should be unique across the system. | 312 | | `version` | `1.0.0` | The version of the package, if not set then it will be inferred from the git version. | 313 | | `description` | `Yeet out scripts with haste!` | The human-readable description of the package. | 314 | | `homepage` | `https://xeiaso.net` | The URL for the homepage of the package. | 315 | | `group` | `Network` | If set, the RPM group that this package belongs to. | 316 | | `license` | `MIT` | The license that the contents of this package is under. | 317 | | `goarch` | `amd64` / `arm64` | The GOARCH value corresponding to the architecture that the RPM is being built for. If you want to build a `noarch` package, put `any` here. | 318 | | `replaces` | `["foo", "bar"]` | Any packages that this package conflicts with or replaces. | 319 | | `depends` | `["foo", "bar"]` | Any packages that this package depends on (such as C libraries for CGo code). | 320 | | `emptyDirs` | `["/var/lib/yeet"]` | Any empty directories that should be created when the package is installed. | 321 | | `configFiles` | `{"./.env.example": "/var/lib/yeet/.env"}` | Any configuration files that should be copied over on install, but managed by administrators after installation. | 322 | | `documentation` | `{"./README.md": "README.md"}` | Any documentation files that should be copied to the `doc` folder of a tarball or be put in `/usr/share/doc` in an OS package. Try to include enough documentation that users can troubleshoot the program completely offline. | 323 | | `files` | `{}` | Any other static files that should be copied in-place to a path in the target filesystem. | 324 | 325 | Packages MUST define a `build` function and tarball packages MAY define a `mkFilename` function. 326 | 327 | ### `build` function 328 | 329 | Every package definition MUST contain a `build` function that describes how to build the software. The build function takes one argument and returns nothing. If the build fails, throw an Exception with `throw`. 330 | 331 | The signature of `build` roughly follows this TypeScript type: 332 | 333 | ```ts 334 | interface BuildInput { 335 | // output folder, usually the package root 336 | out: string; 337 | // binary folder, ${out}/bin for tarballs or ${out}/usr/bin for OS packages 338 | bin: string; 339 | // documentation folder, ${out}/doc for tarballs or ${out}/usr/share/${pkg.name}/doc for OS packages 340 | doc: string; 341 | // configuration folder, ${out}/run for tarballs or ${out}/etc/${pkg.name} for OS packages 342 | etc: string; 343 | // systemd unit folder, ${out}/run for tarballs or ${out}/usr/lib/systemd/system for OS packages 344 | systemd: string; 345 | } 346 | 347 | function build({...}: BuildInput) => { 348 | // ... 349 | }; 350 | ``` 351 | 352 | ### `mkFilename` function 353 | 354 | When building a tarball, you MAY define a `mkFilename` function to customize the generated filename. If no `mkFilename` function is specified, the filename defaults to: 355 | 356 | ```js 357 | const mkFilename = ({name, version, platform, goarch}) = 358 | `${name}-${version}-${platform}-${goarch}`; 359 | ``` 360 | 361 | For example, to reduce the filename to the name and the version: 362 | 363 | ```js 364 | tarball.build({ 365 | // ... 366 | mkFilename: ({ name, version }) => `${name}-${version}`, 367 | // ... 368 | }); 369 | ``` 370 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/TecharoHQ/yeet 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | al.essio.dev/pkg/shellescape v1.6.0 7 | github.com/Masterminds/semver/v3 v3.3.1 8 | github.com/Songmu/gitconfig v0.2.0 9 | github.com/cavaliergopher/rpm v1.3.0 10 | github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c 11 | github.com/goreleaser/nfpm/v2 v2.42.1 12 | github.com/pkg/errors v0.9.1 13 | mvdan.cc/sh/v3 v3.11.0 14 | pault.ag/go/debian v0.18.0 15 | ) 16 | 17 | require ( 18 | dario.cat/mergo v1.0.2 // indirect 19 | github.com/AlekSi/pointer v1.2.0 // indirect 20 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect 21 | github.com/Masterminds/goutils v1.1.1 // indirect 22 | github.com/Masterminds/sprig/v3 v3.3.0 // indirect 23 | github.com/Microsoft/go-winio v0.6.2 // indirect 24 | github.com/ProtonMail/go-crypto v1.2.0 // indirect 25 | github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect 26 | github.com/cavaliergopher/cpio v1.0.1 // indirect 27 | github.com/cli/go-gh v0.1.0 // indirect 28 | github.com/cloudflare/circl v1.6.0 // indirect 29 | github.com/cyphar/filepath-securejoin v0.4.1 // indirect 30 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 31 | github.com/dlclark/regexp2 v1.11.4 // indirect 32 | github.com/emirpasic/gods v1.18.1 // indirect 33 | github.com/fatih/color v1.17.0 // indirect 34 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 35 | github.com/go-git/go-billy/v5 v5.6.2 // indirect 36 | github.com/go-git/go-git/v5 v5.14.0 // indirect 37 | github.com/go-playground/validator/v10 v10.10.0 // indirect 38 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect 39 | github.com/gobwas/glob v0.2.3 // indirect 40 | github.com/goccy/go-yaml v1.12.0 // indirect 41 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 42 | github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect 43 | github.com/google/rpmpack v0.6.1-0.20250405124433-758cc6896cbc // indirect 44 | github.com/google/uuid v1.6.0 // indirect 45 | github.com/goreleaser/chglog v0.7.0 // indirect 46 | github.com/goreleaser/fileglob v1.3.0 // indirect 47 | github.com/huandu/xstrings v1.5.0 // indirect 48 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 49 | github.com/kevinburke/ssh_config v1.2.0 // indirect 50 | github.com/kjk/lzma v0.0.0-20161016003348-3fd93898850d // indirect 51 | github.com/klauspost/compress v1.18.0 // indirect 52 | github.com/klauspost/pgzip v1.2.6 // indirect 53 | github.com/mattn/go-colorable v0.1.13 // indirect 54 | github.com/mattn/go-isatty v0.0.20 // indirect 55 | github.com/mitchellh/copystructure v1.2.0 // indirect 56 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 57 | github.com/pjbgf/sha1cd v0.3.2 // indirect 58 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 59 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 60 | github.com/shopspring/decimal v1.4.0 // indirect 61 | github.com/skeema/knownhosts v1.3.1 // indirect 62 | github.com/spf13/cast v1.7.1 // indirect 63 | github.com/ulikunitz/xz v0.5.12 // indirect 64 | github.com/xanzy/ssh-agent v0.3.3 // indirect 65 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect 66 | gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect 67 | golang.org/x/crypto v0.37.0 // indirect 68 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 69 | golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect 70 | golang.org/x/mod v0.24.0 // indirect 71 | golang.org/x/net v0.39.0 // indirect 72 | golang.org/x/sync v0.13.0 // indirect 73 | golang.org/x/sys v0.32.0 // indirect 74 | golang.org/x/term v0.31.0 // indirect 75 | golang.org/x/text v0.24.0 // indirect 76 | golang.org/x/tools v0.32.0 // indirect 77 | golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect 78 | gopkg.in/warnings.v0 v0.1.2 // indirect 79 | gopkg.in/yaml.v3 v3.0.1 // indirect 80 | honnef.co/go/tools v0.6.1 // indirect 81 | pault.ag/go/topsort v0.1.1 // indirect 82 | ) 83 | 84 | tool ( 85 | golang.org/x/tools/cmd/goimports 86 | honnef.co/go/tools/cmd/staticcheck 87 | ) 88 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA= 2 | al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= 3 | dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= 4 | dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= 5 | github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= 6 | github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= 7 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= 8 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 9 | github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= 10 | github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= 11 | github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= 12 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 13 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 14 | github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= 15 | github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 16 | github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= 17 | github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= 18 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 19 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 20 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 21 | github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs= 22 | github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= 23 | github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= 24 | github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= 25 | github.com/ProtonMail/gopenpgp/v2 v2.7.1 h1:Awsg7MPc2gD3I7IFac2qE3Gdls0lZW8SzrFZ3k1oz0s= 26 | github.com/ProtonMail/gopenpgp/v2 v2.7.1/go.mod h1:/BU5gfAVwqyd8EfC3Eu7zmuhwYQpKs+cGD8M//iiaxs= 27 | github.com/Songmu/gitconfig v0.2.0 h1:pX2++u4KUq+K2k/ZCzGXLtkD3ceCqIdi0tDyb+IbSyo= 28 | github.com/Songmu/gitconfig v0.2.0/go.mod h1:cB5bYJer+pl7W8g6RHFwL/0X6aJROVrYuHlvc7PT+hE= 29 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 30 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 31 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 32 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 33 | github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= 34 | github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= 35 | github.com/caarlos0/testfs v0.4.4 h1:3PHvzHi5Lt+g332CiShwS8ogTgS3HjrmzZxCm6JCDr8= 36 | github.com/caarlos0/testfs v0.4.4/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk= 37 | github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= 38 | github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= 39 | github.com/cavaliergopher/rpm v1.3.0 h1:UHX46sasX8MesUXXQ+UbkFLUX4eUWTlEcX8jcnRBIgI= 40 | github.com/cavaliergopher/rpm v1.3.0/go.mod h1:vEumo1vvtrHM1Ov86f6+k8j7zNKOxQfHDCAIcR/36ZI= 41 | github.com/cli/browser v1.1.0/go.mod h1:HKMQAt9t12kov91Mn7RfZxyJQQgWgyS/3SZswlZ5iTI= 42 | github.com/cli/go-gh v0.1.0 h1:kMqFmC3ECBrV2UKzlOHjNOTTchExVc5tjNHtCqk/zYk= 43 | github.com/cli/go-gh v0.1.0/go.mod h1:eTGWl99EMZ+3Iau5C6dHyGAJRRia65MtdBtuhWc+84o= 44 | github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= 45 | github.com/cli/shurcooL-graphql v0.0.1/go.mod h1:U7gCSuMZP/Qy7kbqkk5PrqXEeDgtfG5K+W+u8weorps= 46 | github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= 47 | github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 48 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 49 | github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= 50 | github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 51 | github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= 52 | github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 53 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 54 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 55 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 56 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 57 | github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= 58 | github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 59 | github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c h1:mxWGS0YyquJ/ikZOjSrRjjFIbUqIP9ojyYQ+QZTU3Rg= 60 | github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= 61 | github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= 62 | github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= 63 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 64 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 65 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 66 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 67 | github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 68 | github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 69 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 70 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 71 | github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= 72 | github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= 73 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 74 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 75 | github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= 76 | github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= 77 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= 78 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= 79 | github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= 80 | github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= 81 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 82 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 83 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 84 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 85 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 86 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 87 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 88 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 89 | github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= 90 | github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 91 | github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= 92 | github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= 93 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= 94 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= 95 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 96 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 97 | github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= 98 | github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM= 99 | github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= 100 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 101 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 102 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 103 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 104 | github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= 105 | github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= 106 | github.com/google/rpmpack v0.6.1-0.20250405124433-758cc6896cbc h1:qES+d3PvR9CN+zARQQH/bNXH0ybzmdjNMHICrBwXD28= 107 | github.com/google/rpmpack v0.6.1-0.20250405124433-758cc6896cbc/go.mod h1:uqVAUVQLq8UY2hCDfmJ/+rtO3aw7qyhc90rCVEabEfI= 108 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 109 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 110 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 111 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 112 | github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= 113 | github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= 114 | github.com/goreleaser/chglog v0.7.0 h1:/KzXWAeg4DrEz4r3OI6K2Yb8RAsVGeInCUfLWFXL9C0= 115 | github.com/goreleaser/chglog v0.7.0/go.mod h1:2h/yyq9xvTUeM9tOoucBP+jri8Dj28splx+SjlYkklc= 116 | github.com/goreleaser/fileglob v1.3.0 h1:/X6J7U8lbDpQtBvGcwwPS6OpzkNVlVEsFUVRx9+k+7I= 117 | github.com/goreleaser/fileglob v1.3.0/go.mod h1:Jx6BoXv3mbYkEzwm9THo7xbr5egkAraxkGorbJb4RxU= 118 | github.com/goreleaser/nfpm/v2 v2.42.1 h1:xu2pLRgQuz2ab+YZFoeIzwU/M5jjjCKDGwv1lRbVGvk= 119 | github.com/goreleaser/nfpm/v2 v2.42.1/go.mod h1:dY53KWYKebkOocxgkmpM7SRX0Nv5hU+jEu2kIaM4/LI= 120 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= 121 | github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= 122 | github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= 123 | github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 124 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 125 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 126 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 127 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 128 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 129 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 130 | github.com/kjk/lzma v0.0.0-20161016003348-3fd93898850d h1:RnWZeH8N8KXfbwMTex/KKMYMj0FJRCF6tQubUuQ02GM= 131 | github.com/kjk/lzma v0.0.0-20161016003348-3fd93898850d/go.mod h1:phT/jsRPBAEqjAibu1BurrabCBNTYiVI+zbmyCZJY6Q= 132 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 133 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 134 | github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= 135 | github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= 136 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 137 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 138 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 139 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 140 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 141 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 142 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 143 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 144 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 145 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 146 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 147 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 148 | github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= 149 | github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= 150 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 151 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 152 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 153 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 154 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 155 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 156 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 157 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 158 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 159 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 160 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 161 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 162 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 163 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 164 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= 165 | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 166 | github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 167 | github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= 168 | github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= 169 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 170 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 171 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 172 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 173 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 174 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 175 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 176 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 177 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 178 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 179 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 180 | github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg= 181 | github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI= 182 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 183 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 184 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 185 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 186 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 187 | github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= 188 | github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= 189 | github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= 190 | github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= 191 | github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= 192 | github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= 193 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 194 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 195 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 196 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 197 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 198 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 199 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 200 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 201 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 202 | github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= 203 | github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= 204 | github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 205 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 206 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 207 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= 208 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= 209 | gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8= 210 | gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0= 211 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 212 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 213 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 214 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 215 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 216 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 217 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 218 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 219 | golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ= 220 | golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= 221 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 222 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 223 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 224 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 225 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 226 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 227 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 228 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 229 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 230 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 231 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 232 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 233 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 234 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 235 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 236 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 237 | golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 238 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 239 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 240 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 241 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 242 | golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 243 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 244 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 245 | golang.org/x/sys v0.0.0-20220818161305-2296e01440c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 246 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 247 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 248 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 249 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 250 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 251 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 252 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 253 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 254 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 255 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 256 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 257 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 258 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 259 | golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= 260 | golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= 261 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 262 | golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= 263 | golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk= 264 | golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 265 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 266 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 267 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 268 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 269 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 270 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 271 | gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= 272 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 273 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 274 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 275 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 276 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 277 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 278 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 279 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 280 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 281 | honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= 282 | honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= 283 | mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw= 284 | mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg= 285 | pault.ag/go/debian v0.18.0 h1:nr0iiyOU5QlG1VPnhZLNhnCcHx58kukvBJp+dvaM6CQ= 286 | pault.ag/go/debian v0.18.0/go.mod h1:JFl0XWRCv9hWBrB5MDDZjA5GSEs1X3zcFK/9kCNIUmE= 287 | pault.ag/go/topsort v0.1.1 h1:L0QnhUly6LmTv0e3DEzbN2q6/FGgAcQvaEw65S53Bg4= 288 | pault.ag/go/topsort v0.1.1/go.mod h1:r1kc/L0/FZ3HhjezBIPaNVhkqv8L0UJ9bxRuHRVZ0q4= 289 | -------------------------------------------------------------------------------- /internal/git.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "log/slog" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/Songmu/gitconfig" 15 | "github.com/TecharoHQ/yeet/internal/yeet" 16 | ) 17 | 18 | var ( 19 | GPGKeyFile = flag.String("gpg-key-file", gpgKeyFileLocation(), "GPG key file to sign the package") 20 | GPGKeyID = flag.String("gpg-key-id", "", "GPG key ID to sign the package") 21 | GPGKeyPassword = flag.String("gpg-key-password", "", "GPG key password to sign the package") 22 | UserName = flag.String("git-user-name", GitUserName(), "user name in Git") 23 | UserEmail = flag.String("git-user-email", GitUserEmail(), "user email in Git") 24 | SourceDateEpoch = flag.Int64("source-date-epoch", GetSourceDateEpoch(), "Timestamp to use for all files in packages") 25 | ) 26 | 27 | const ( 28 | fallbackName = "Mimi Yasomi" 29 | fallbackEmail = "mimi@xeserv.us" 30 | ) 31 | 32 | func gpgKeyFileLocation() string { 33 | folder, err := os.UserConfigDir() 34 | if err != nil { 35 | return "" 36 | } 37 | 38 | return filepath.Join(folder, "techaro.lol", "yeet", "key.asc") 39 | } 40 | 41 | func GitUserName() string { 42 | name, err := gitconfig.User() 43 | if err != nil { 44 | return fallbackName 45 | } 46 | 47 | return name 48 | } 49 | 50 | func GitUserEmail() string { 51 | email, err := gitconfig.Email() 52 | if err != nil { 53 | return fallbackEmail 54 | } 55 | 56 | return email 57 | } 58 | 59 | func GitVersion() string { 60 | vers, err := yeet.GitTag(context.Background()) 61 | if err != nil { 62 | panic(err) 63 | } 64 | 65 | return vers 66 | } 67 | 68 | func GetSourceDateEpoch() int64 { 69 | // fallback needs to be 1 because some software thinks unix time 0 means "no time" 70 | const fallback = 1 71 | 72 | gitPath, err := exec.LookPath("git") 73 | if err != nil { 74 | slog.Warn("git not found in $PATH", "err", err) 75 | return fallback 76 | } 77 | 78 | epochFromGitStr, err := yeet.Output(context.Background(), gitPath, "log", "-1", "--format=%ct") 79 | if err == nil { 80 | num, _ := strconv.ParseInt(strings.TrimSpace(epochFromGitStr), 10, 64) 81 | if num != 0 { 82 | return num 83 | } 84 | } 85 | 86 | return fallback 87 | } 88 | 89 | func SourceEpoch() time.Time { 90 | return time.Unix(*SourceDateEpoch, 0) 91 | } 92 | -------------------------------------------------------------------------------- /internal/git_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/TecharoHQ/yeet/internal/yeet" 7 | ) 8 | 9 | func TestGitVersion(t *testing.T) { 10 | for _, tt := range []struct { 11 | name string 12 | input string 13 | want string 14 | }{ 15 | { 16 | name: "base test", 17 | }, 18 | { 19 | name: "with version starts with v", 20 | input: "v1.0.0", 21 | want: "1.0.0", 22 | }, 23 | { 24 | name: "with version without v", 25 | input: "1.0.0", 26 | want: "1.0.0", 27 | }, 28 | { 29 | name: "with version with v and -", 30 | input: "v1.0.0-abc123", 31 | want: "1.0.0-abc123", 32 | }, 33 | } { 34 | t.Run(tt.name, func(t *testing.T) { 35 | yeet.ForceGitVersion = &tt.input 36 | got := GitVersion() 37 | 38 | if tt.input != "" { 39 | if got != tt.want { 40 | t.Errorf("GitVersion() = %v, want %v", got, tt.want) 41 | } 42 | } 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/gitea/gitea.go: -------------------------------------------------------------------------------- 1 | package gitea 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log/slog" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | ) 12 | 13 | var ( 14 | giteaHost = flag.String("gitea-host", "", "URL of the gitea instance you are deploying to without a trailing slash, if not set then gitea integrations are no-ops") 15 | giteaToken = flag.String("gitea-token", "", "Gitea token") 16 | giteaUsername = flag.String("gitea-username", "", "Gitea username") 17 | ) 18 | 19 | func UploadPackage(ctx context.Context, c *http.Client, owner, distro, component, fname string) error { 20 | if *giteaHost == "" { 21 | slog.Debug("gitea config not set, bailing") 22 | return nil 23 | } 24 | 25 | kind := "" 26 | 27 | switch filepath.Ext(fname) { 28 | case ".deb": 29 | kind = "debian/pool" 30 | case ".rpm": 31 | kind = "rpm" 32 | default: 33 | slog.Debug("wrong package kind", "fname", fname) 34 | return nil 35 | } 36 | 37 | fin, err := os.Open(fname) 38 | if err != nil { 39 | return fmt.Errorf("can't open %s: %w", fname, err) 40 | } 41 | defer fin.Close() 42 | 43 | req, err := http.NewRequestWithContext(ctx, http.MethodPut, fmt.Sprintf("%s/api/packages/%s/%s/%s/%s/upload", *giteaHost, owner, kind, distro, component), fin) 44 | if err != nil { 45 | return fmt.Errorf("[unexpected] can't make request: %w", err) 46 | } 47 | 48 | req.SetBasicAuth(*giteaUsername, *giteaToken) 49 | 50 | resp, err := c.Do(req) 51 | if err != nil { 52 | return fmt.Errorf("can't do request: %w", err) 53 | } 54 | defer resp.Body.Close() 55 | 56 | if resp.StatusCode != http.StatusCreated { 57 | return fmt.Errorf("got wrong status code from gitea: %s", resp.Status) 58 | } 59 | 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /internal/internal.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "flag" 4 | 5 | var ( 6 | PackageDestDir = flag.String("package-dest-dir", "./var", "directory to store built packages") 7 | ) 8 | -------------------------------------------------------------------------------- /internal/mkdeb/mkdeb.go: -------------------------------------------------------------------------------- 1 | package mkdeb 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | 10 | "github.com/Masterminds/semver/v3" 11 | "github.com/TecharoHQ/yeet/internal" 12 | "github.com/TecharoHQ/yeet/internal/pkgmeta" 13 | "github.com/goreleaser/nfpm/v2" 14 | _ "github.com/goreleaser/nfpm/v2/deb" 15 | "github.com/goreleaser/nfpm/v2/files" 16 | ) 17 | 18 | func Build(p pkgmeta.Package) (foutpath string, err error) { 19 | defer func() { 20 | if r := recover(); r != nil { 21 | if err, ok := r.(error); ok { 22 | slog.Error("mkrpm: error while building", "err", err) 23 | } else { 24 | err = fmt.Errorf("%v", r) 25 | slog.Error("mkrpm: error while building", "err", err) 26 | } 27 | } 28 | }() 29 | 30 | os.MkdirAll(*internal.PackageDestDir, 0755) 31 | os.WriteFile(filepath.Join(*internal.PackageDestDir, ".gitignore"), []byte("*\n!.gitignore"), 0644) 32 | 33 | if p.Version == "" { 34 | p.Version = internal.GitVersion() 35 | } 36 | 37 | if _, err := semver.NewVersion(p.Version); err != nil { 38 | return "", fmt.Errorf("invalid version %q: %w", p.Version, err) 39 | } 40 | 41 | dir, err := os.MkdirTemp("", "yeet-mkdeb") 42 | if err != nil { 43 | return "", fmt.Errorf("mkrpm: can't make temporary directory") 44 | } 45 | defer os.RemoveAll(dir) 46 | os.MkdirAll(dir, 0755) 47 | 48 | cgoEnabled := os.Getenv("CGO_ENABLED") 49 | defer func() { 50 | os.Setenv("GOARCH", runtime.GOARCH) 51 | os.Setenv("GOOS", runtime.GOOS) 52 | os.Setenv("CGO_ENABLED", cgoEnabled) 53 | }() 54 | os.Setenv("GOARCH", p.Goarch) 55 | os.Setenv("GOOS", "linux") 56 | os.Setenv("CGO_ENABLED", "0") 57 | 58 | p.Build(pkgmeta.BuildInput{ 59 | Output: dir, 60 | Bin: filepath.Join(dir, "usr", "bin"), 61 | Doc: filepath.Join(dir, "usr", "share", "doc", p.Name), 62 | Etc: filepath.Join(dir, "etc", p.Name), 63 | Man: filepath.Join(dir, "usr", "share", "man"), 64 | Systemd: filepath.Join(dir, "usr", "lib", "systemd", "system"), 65 | }) 66 | 67 | var contents files.Contents 68 | 69 | for _, d := range p.EmptyDirs { 70 | if d == "" { 71 | continue 72 | } 73 | 74 | contents = append(contents, &files.Content{ 75 | Type: files.TypeDir, 76 | Destination: d, 77 | FileInfo: &files.ContentFileInfo{ 78 | MTime: internal.SourceEpoch(), 79 | Mode: os.FileMode(0600), 80 | }, 81 | }) 82 | } 83 | 84 | for repoPath, osPath := range p.ConfigFiles { 85 | contents = append(contents, &files.Content{ 86 | Type: files.TypeConfig, 87 | Source: repoPath, 88 | Destination: osPath, 89 | FileInfo: &files.ContentFileInfo{ 90 | Mode: os.FileMode(0600), 91 | MTime: internal.SourceEpoch(), 92 | }, 93 | }) 94 | } 95 | 96 | for repoPath, rpmPath := range p.Documentation { 97 | contents = append(contents, &files.Content{ 98 | Type: files.TypeFile, 99 | Source: repoPath, 100 | Destination: filepath.Join("/usr/share/doc", p.Name, rpmPath), 101 | FileInfo: &files.ContentFileInfo{ 102 | MTime: internal.SourceEpoch(), 103 | }, 104 | }) 105 | } 106 | 107 | for repoPath, rpmPath := range p.Files { 108 | contents = append(contents, &files.Content{ 109 | Type: files.TypeFile, 110 | Source: repoPath, 111 | Destination: rpmPath, 112 | FileInfo: &files.ContentFileInfo{ 113 | MTime: internal.SourceEpoch(), 114 | }, 115 | }) 116 | } 117 | 118 | if err := filepath.Walk(dir, func(path string, stat os.FileInfo, err error) error { 119 | if err != nil { 120 | return err 121 | } 122 | 123 | if stat.IsDir() { 124 | return nil 125 | } 126 | 127 | contents = append(contents, &files.Content{ 128 | Type: files.TypeFile, 129 | Source: path, 130 | Destination: path[len(dir)+1:], 131 | FileInfo: &files.ContentFileInfo{ 132 | MTime: internal.SourceEpoch(), 133 | }, 134 | }) 135 | 136 | return nil 137 | }); err != nil { 138 | return "", fmt.Errorf("mkdeb: can't walk output directory: %w", err) 139 | } 140 | 141 | contents, err = files.PrepareForPackager(contents, 0o002, "deb", true, internal.SourceEpoch()) 142 | if err != nil { 143 | return "", fmt.Errorf("mkdeb: can't prepare for packager: %w", err) 144 | } 145 | 146 | for _, content := range contents { 147 | content.FileInfo.MTime = internal.SourceEpoch() 148 | } 149 | 150 | info := nfpm.WithDefaults(&nfpm.Info{ 151 | Name: p.Name, 152 | Version: p.Version, 153 | Arch: p.Goarch, 154 | Platform: "linux", 155 | Description: p.Description, 156 | Maintainer: fmt.Sprintf("%s <%s>", *internal.UserName, *internal.UserEmail), 157 | Homepage: p.Homepage, 158 | License: p.License, 159 | MTime: internal.SourceEpoch(), 160 | Overridables: nfpm.Overridables{ 161 | Contents: contents, 162 | Depends: p.Depends, 163 | Recommends: p.Recommends, 164 | Replaces: p.Replaces, 165 | Conflicts: p.Replaces, 166 | }, 167 | }) 168 | 169 | info.Overridables.RPM.Group = p.Group 170 | 171 | if *internal.GPGKeyID != "" { 172 | slog.Debug("using GPG key", "file", *internal.GPGKeyFile, "id", *internal.GPGKeyID) 173 | info.Overridables.Deb.Signature.KeyFile = *internal.GPGKeyFile 174 | info.Overridables.Deb.Signature.KeyID = internal.GPGKeyID 175 | info.Overridables.Deb.Signature.KeyPassphrase = *internal.GPGKeyPassword 176 | } 177 | 178 | pkg, err := nfpm.Get("deb") 179 | if err != nil { 180 | return "", fmt.Errorf("mkdeb: can't get RPM packager: %w", err) 181 | } 182 | 183 | foutpath = pkg.ConventionalFileName(info) 184 | fout, err := os.Create(filepath.Join(*internal.PackageDestDir, foutpath)) 185 | if err != nil { 186 | return "", fmt.Errorf("mkdeb: can't create output file: %w", err) 187 | } 188 | defer fout.Close() 189 | 190 | if err := pkg.Package(info, fout); err != nil { 191 | return "", fmt.Errorf("mkdeb: can't build package: %w", err) 192 | } 193 | 194 | slog.Info("built package", "name", p.Name, "arch", p.Goarch, "version", p.Version, "path", fout.Name()) 195 | 196 | return fout.Name(), err 197 | } 198 | -------------------------------------------------------------------------------- /internal/mkdeb/mkdeb_test.go: -------------------------------------------------------------------------------- 1 | package mkdeb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/TecharoHQ/yeet/internal/yeettest" 7 | "pault.ag/go/debian/deb" 8 | ) 9 | 10 | func TestBuild(t *testing.T) { 11 | fname := yeettest.BuildHello(t, Build, "1.0.0", true) 12 | 13 | debFile, close, err := deb.LoadFile(fname) 14 | if err != nil { 15 | t.Fatalf("failed to load deb file: %v", err) 16 | } 17 | defer close() 18 | 19 | if debFile.Control.Version.Empty() { 20 | t.Error("version is empty") 21 | } 22 | } 23 | 24 | func TestBuildError(t *testing.T) { 25 | yeettest.BuildHello(t, Build, ".0.0", false) 26 | } 27 | -------------------------------------------------------------------------------- /internal/mkrpm/mkrpm.go: -------------------------------------------------------------------------------- 1 | package mkrpm 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | "time" 10 | 11 | "github.com/Masterminds/semver/v3" 12 | "github.com/TecharoHQ/yeet/internal" 13 | "github.com/TecharoHQ/yeet/internal/pkgmeta" 14 | "github.com/goreleaser/nfpm/v2" 15 | "github.com/goreleaser/nfpm/v2/files" 16 | _ "github.com/goreleaser/nfpm/v2/rpm" 17 | ) 18 | 19 | func Build(p pkgmeta.Package) (foutpath string, err error) { 20 | defer func() { 21 | if r := recover(); r != nil { 22 | if err, ok := r.(error); ok { 23 | slog.Error("mkrpm: error while building", "err", err) 24 | } else { 25 | err = fmt.Errorf("%v", r) 26 | slog.Error("mkrpm: error while building", "err", err) 27 | } 28 | } 29 | }() 30 | 31 | os.MkdirAll(*internal.PackageDestDir, 0755) 32 | os.WriteFile(filepath.Join(*internal.PackageDestDir, ".gitignore"), []byte("*\n!.gitignore"), 0644) 33 | 34 | if p.Version == "" { 35 | p.Version = internal.GitVersion() 36 | } 37 | 38 | if _, err := semver.NewVersion(p.Version); err != nil { 39 | return "", fmt.Errorf("invalid version %q: %w", p.Version, err) 40 | } 41 | 42 | if p.Platform == "" { 43 | p.Platform = "linux" 44 | } 45 | 46 | dir, err := os.MkdirTemp("", "yeet-mkrpm") 47 | if err != nil { 48 | return "", fmt.Errorf("mkrpm: can't make temporary directory") 49 | } 50 | defer os.RemoveAll(dir) 51 | os.MkdirAll(dir, 0755) 52 | 53 | cgoEnabled := os.Getenv("CGO_ENABLED") 54 | defer func() { 55 | os.Setenv("GOARCH", runtime.GOARCH) 56 | os.Setenv("GOOS", runtime.GOOS) 57 | os.Setenv("CGO_ENABLED", cgoEnabled) 58 | }() 59 | os.Setenv("GOARCH", p.Goarch) 60 | os.Setenv("GOOS", p.Platform) 61 | os.Setenv("CGO_ENABLED", "0") 62 | 63 | p.Build(pkgmeta.BuildInput{ 64 | Output: dir, 65 | Bin: filepath.Join(dir, "usr", "bin"), 66 | Doc: filepath.Join(dir, "usr", "share", "doc", p.Name), 67 | Etc: filepath.Join(dir, "etc", p.Name), 68 | Man: filepath.Join(dir, "usr", "share", "man"), 69 | Systemd: filepath.Join(dir, "usr", "lib", "systemd", "system"), 70 | }) 71 | 72 | var contents files.Contents 73 | 74 | for _, d := range p.EmptyDirs { 75 | if d == "" { 76 | continue 77 | } 78 | 79 | contents = append(contents, &files.Content{ 80 | Type: files.TypeDir, 81 | Destination: d, 82 | FileInfo: &files.ContentFileInfo{ 83 | MTime: internal.SourceEpoch(), 84 | }, 85 | }) 86 | } 87 | 88 | for repoPath, rpmPath := range p.ConfigFiles { 89 | contents = append(contents, &files.Content{ 90 | Type: files.TypeConfig, 91 | Source: repoPath, 92 | Destination: rpmPath, 93 | FileInfo: &files.ContentFileInfo{ 94 | Mode: os.FileMode(0600), 95 | MTime: internal.SourceEpoch(), 96 | }, 97 | }) 98 | } 99 | 100 | for repoPath, rpmPath := range p.Documentation { 101 | contents = append(contents, &files.Content{ 102 | Type: files.TypeRPMDoc, 103 | Source: repoPath, 104 | Destination: filepath.Join("/usr/share/doc", p.Name, rpmPath), 105 | FileInfo: &files.ContentFileInfo{ 106 | MTime: internal.SourceEpoch(), 107 | }, 108 | }) 109 | } 110 | 111 | for repoPath, rpmPath := range p.Files { 112 | contents = append(contents, &files.Content{ 113 | Type: files.TypeFile, 114 | Source: repoPath, 115 | Destination: rpmPath, 116 | FileInfo: &files.ContentFileInfo{ 117 | MTime: internal.SourceEpoch(), 118 | }, 119 | }) 120 | } 121 | 122 | if err := filepath.Walk(dir, func(path string, stat os.FileInfo, err error) error { 123 | if err != nil { 124 | return err 125 | } 126 | 127 | if stat.IsDir() { 128 | return nil 129 | } 130 | 131 | contents = append(contents, &files.Content{ 132 | Type: files.TypeFile, 133 | Source: path, 134 | Destination: path[len(dir)+1:], 135 | FileInfo: &files.ContentFileInfo{ 136 | MTime: internal.SourceEpoch(), 137 | }, 138 | }) 139 | 140 | return nil 141 | }); err != nil { 142 | return "", fmt.Errorf("mkrpm: can't walk output directory: %w", err) 143 | } 144 | 145 | contents, err = files.PrepareForPackager(contents, 0o002, "rpm", true, time.Unix(0, 0)) 146 | if err != nil { 147 | return "", fmt.Errorf("mkdeb: can't prepare for packager: %w", err) 148 | } 149 | 150 | for _, content := range contents { 151 | content.FileInfo.MTime = internal.SourceEpoch() 152 | } 153 | 154 | info := nfpm.WithDefaults(&nfpm.Info{ 155 | Name: p.Name, 156 | Version: p.Version, 157 | Arch: p.Goarch, 158 | Platform: p.Platform, 159 | Description: p.Description, 160 | Maintainer: fmt.Sprintf("%s <%s>", *internal.UserName, *internal.UserEmail), 161 | Homepage: p.Homepage, 162 | License: p.License, 163 | MTime: internal.SourceEpoch(), 164 | Overridables: nfpm.Overridables{ 165 | Contents: contents, 166 | Depends: p.Depends, 167 | Recommends: p.Recommends, 168 | Replaces: p.Replaces, 169 | Conflicts: p.Replaces, 170 | }, 171 | }) 172 | 173 | info.Overridables.RPM.Group = p.Group 174 | 175 | if *internal.GPGKeyPassword != "" { 176 | slog.Debug("using GPG key", "file", *internal.GPGKeyFile, "id", *internal.GPGKeyID, "password", *internal.GPGKeyPassword) 177 | info.Overridables.RPM.Signature.KeyFile = *internal.GPGKeyFile 178 | info.Overridables.RPM.Signature.KeyID = internal.GPGKeyID 179 | info.Overridables.RPM.Signature.KeyPassphrase = *internal.GPGKeyPassword 180 | } 181 | 182 | pkg, err := nfpm.Get("rpm") 183 | if err != nil { 184 | return "", fmt.Errorf("mkrpm: can't get RPM packager: %w", err) 185 | } 186 | 187 | foutpath = pkg.ConventionalFileName(info) 188 | fout, err := os.Create(filepath.Join(*internal.PackageDestDir, foutpath)) 189 | if err != nil { 190 | return "", fmt.Errorf("mkrpm: can't create output file: %w", err) 191 | } 192 | defer fout.Close() 193 | 194 | if err := pkg.Package(info, fout); err != nil { 195 | return "", fmt.Errorf("mkrpm: can't build package: %w", err) 196 | } 197 | 198 | slog.Info("built package", "name", p.Name, "arch", p.Goarch, "version", p.Version, "path", fout.Name()) 199 | 200 | return fout.Name(), err 201 | } 202 | -------------------------------------------------------------------------------- /internal/mkrpm/mkrpm_test.go: -------------------------------------------------------------------------------- 1 | package mkrpm 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/Masterminds/semver/v3" 8 | "github.com/TecharoHQ/yeet/internal/yeettest" 9 | "github.com/cavaliergopher/rpm" 10 | ) 11 | 12 | func TestBuild(t *testing.T) { 13 | fname := yeettest.BuildHello(t, Build, "1.0.0", true) 14 | 15 | pkg, err := rpm.Open(fname) 16 | if err != nil { 17 | t.Fatalf("failed to open rpm file: %v", err) 18 | } 19 | 20 | version, err := semver.NewVersion(pkg.Version()) 21 | if err != nil { 22 | t.Fatalf("failed to parse version: %v", err) 23 | } 24 | if version == nil { 25 | t.Error("version is nil") 26 | } 27 | 28 | fin, err := os.Open(fname) 29 | if err != nil { 30 | t.Fatalf("failed to open rpm file: %v", err) 31 | } 32 | defer fin.Close() 33 | } 34 | 35 | func TestBuildError(t *testing.T) { 36 | yeettest.BuildHello(t, Build, ".0.0", false) 37 | } 38 | -------------------------------------------------------------------------------- /internal/mktarball/mktarball.go: -------------------------------------------------------------------------------- 1 | package mktarball 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "fmt" 7 | "io" 8 | "log/slog" 9 | "os" 10 | "path/filepath" 11 | "runtime" 12 | 13 | "github.com/Masterminds/semver/v3" 14 | "github.com/TecharoHQ/yeet/internal" 15 | "github.com/TecharoHQ/yeet/internal/pkgmeta" 16 | "github.com/TecharoHQ/yeet/internal/vfs" 17 | ) 18 | 19 | func defaultFname(p pkgmeta.Package) string { 20 | return fmt.Sprintf("%s-%s-%s-%s", p.Name, p.Version, p.Platform, p.Goarch) 21 | } 22 | 23 | func Build(p pkgmeta.Package) (foutpath string, err error) { 24 | defer func() { 25 | if r := recover(); r != nil { 26 | if err, ok := r.(error); ok { 27 | slog.Error("mkrpm: error while building", "err", err) 28 | } else { 29 | err = fmt.Errorf("%v", r) 30 | slog.Error("mkrpm: error while building", "err", err) 31 | } 32 | } 33 | }() 34 | 35 | os.MkdirAll(*internal.PackageDestDir, 0755) 36 | os.WriteFile(filepath.Join(*internal.PackageDestDir, ".gitignore"), []byte("*\n!.gitignore"), 0644) 37 | 38 | if p.Version == "" { 39 | p.Version = internal.GitVersion() 40 | } 41 | 42 | if _, err := semver.NewVersion(p.Version); err != nil { 43 | return "", fmt.Errorf("invalid version %q: %w", p.Version, err) 44 | } 45 | 46 | if p.Platform == "" { 47 | p.Platform = "linux" 48 | } 49 | 50 | dir, err := os.MkdirTemp("", "yeet-mktarball") 51 | if err != nil { 52 | return "", fmt.Errorf("can't make temporary directory") 53 | } 54 | defer os.RemoveAll(dir) 55 | 56 | folderName := defaultFname(p) 57 | if p.Filename != nil { 58 | folderName = p.Filename(p) 59 | } 60 | 61 | pkgDir := filepath.Join(dir, folderName) 62 | os.MkdirAll(pkgDir, 0755) 63 | 64 | fname := filepath.Join(*internal.PackageDestDir, folderName+".tar.gz") 65 | fout, err := os.Create(fname) 66 | if err != nil { 67 | return "", fmt.Errorf("can't make output file: %w", err) 68 | } 69 | defer fout.Close() 70 | 71 | gw, err := gzip.NewWriterLevel(fout, 9) 72 | if err != nil { 73 | return "", fmt.Errorf("can't make gzip writer: %w", err) 74 | } 75 | defer gw.Close() 76 | 77 | tw := tar.NewWriter(gw) 78 | defer tw.Close() 79 | 80 | cgoEnabled := os.Getenv("CGO_ENABLED") 81 | defer func() { 82 | os.Setenv("GOARCH", runtime.GOARCH) 83 | os.Setenv("GOOS", runtime.GOOS) 84 | os.Setenv("CGO_ENABLED", cgoEnabled) 85 | }() 86 | os.Setenv("GOARCH", p.Goarch) 87 | os.Setenv("GOOS", p.Platform) 88 | os.Setenv("CGO_ENABLED", "0") 89 | 90 | bi := pkgmeta.BuildInput{ 91 | Output: pkgDir, 92 | Bin: filepath.Join(pkgDir, "bin"), 93 | Doc: filepath.Join(pkgDir, "doc"), 94 | Etc: filepath.Join(pkgDir, "run"), 95 | Man: filepath.Join(pkgDir, "man"), 96 | Systemd: filepath.Join(pkgDir, "run"), 97 | } 98 | 99 | os.MkdirAll(bi.Doc, 0755) 100 | os.WriteFile(filepath.Join(bi.Doc, "VERSION"), []byte(p.Version+"\n"), 0666) 101 | 102 | p.Build(bi) 103 | 104 | for src, dst := range p.ConfigFiles { 105 | if err := Copy(src, filepath.Join(bi.Etc, dst)); err != nil { 106 | return "", fmt.Errorf("can't copy %s to %s: %w", src, dst, err) 107 | } 108 | } 109 | 110 | for src, dst := range p.Documentation { 111 | fname := filepath.Join(bi.Doc, dst) 112 | if filepath.Base(fname) == "README.md" { 113 | fname = filepath.Join(pkgDir, "README.md") 114 | } 115 | 116 | if err := Copy(src, fname); err != nil { 117 | return "", fmt.Errorf("can't copy %s to %s: %w", src, dst, err) 118 | } 119 | } 120 | 121 | root, err := os.OpenRoot(dir) 122 | if err != nil { 123 | return "", fmt.Errorf("can't open root FS %s: %w", dir, err) 124 | } 125 | 126 | if err := tw.AddFS(vfs.ModTimeFS{FS: root.FS(), Time: internal.SourceEpoch()}); err != nil { 127 | return "", fmt.Errorf("can't copy built files to tarball: %w", err) 128 | } 129 | 130 | slog.Info("built package", "name", p.Name, "arch", p.Goarch, "version", p.Version, "path", fout.Name()) 131 | 132 | return fname, nil 133 | } 134 | 135 | // Copy copies the contents of the file at srcpath to a regular file 136 | // at dstpath. If the file named by dstpath already exists, it is 137 | // truncated. The function does not copy the file mode, file 138 | // permission bits, or file attributes. 139 | func Copy(srcpath, dstpath string) (err error) { 140 | r, err := os.Open(srcpath) 141 | if err != nil { 142 | return err 143 | } 144 | defer r.Close() // ignore error: file was opened read-only. 145 | 146 | st, err := r.Stat() 147 | if err != nil { 148 | return err 149 | } 150 | 151 | os.MkdirAll(filepath.Dir(dstpath), 0755) 152 | 153 | w, err := os.Create(dstpath) 154 | if err != nil { 155 | return err 156 | } 157 | 158 | if err := w.Chmod(st.Mode()); err != nil { 159 | return err 160 | } 161 | 162 | defer func() { 163 | // Report the error, if any, from Close, but do so 164 | // only if there isn't already an outgoing error. 165 | if c := w.Close(); err == nil { 166 | err = c 167 | } 168 | }() 169 | 170 | _, err = io.Copy(w, r) 171 | return err 172 | } 173 | -------------------------------------------------------------------------------- /internal/mktarball/mktarball_test.go: -------------------------------------------------------------------------------- 1 | package mktarball 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "io" 7 | "os" 8 | "testing" 9 | 10 | "github.com/TecharoHQ/yeet/internal" 11 | "github.com/TecharoHQ/yeet/internal/yeettest" 12 | ) 13 | 14 | func TestBuild(t *testing.T) { 15 | yeettest.BuildHello(t, Build, "1.0.0", true) 16 | } 17 | 18 | func TestBuildError(t *testing.T) { 19 | yeettest.BuildHello(t, Build, ".0.0", false) 20 | } 21 | 22 | func TestTimestampsNotZero(t *testing.T) { 23 | pkg := yeettest.BuildHello(t, Build, "1.0.0", true) 24 | 25 | fin, err := os.Open(pkg) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | defer fin.Close() 30 | 31 | gzr, err := gzip.NewReader(fin) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | defer gzr.Close() 36 | 37 | tr := tar.NewReader(gzr) 38 | 39 | for { 40 | header, err := tr.Next() 41 | switch { 42 | case err == io.EOF: 43 | return 44 | case err != nil: 45 | t.Fatal(err) 46 | } 47 | 48 | expect := internal.SourceEpoch() 49 | 50 | t.Run(header.Name, func(t *testing.T) { 51 | header := header 52 | if !header.ModTime.Equal(expect) { 53 | t.Errorf("file has wrong timestamp %s, wanted: %s", header.ModTime, expect) 54 | } 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /internal/pkgmeta/package.go: -------------------------------------------------------------------------------- 1 | package pkgmeta 2 | 3 | type Package struct { 4 | Name string `json:"name"` 5 | Version string `json:"version"` 6 | Description string `json:"description"` 7 | Homepage string `json:"homepage"` 8 | Group string `json:"group"` 9 | License string `json:"license"` 10 | Platform string `json:"platform"` // if not set, default to linux 11 | Goarch string `json:"goarch"` 12 | Replaces []string `json:"replaces"` 13 | Depends []string `json:"depends"` 14 | Recommends []string `json:"recommends"` 15 | 16 | EmptyDirs []string `json:"emptyDirs"` // rpm destination path 17 | ConfigFiles map[string]string `json:"configFiles"` // pwd-relative source path, rpm destination path 18 | Documentation map[string]string `json:"documentation"` // pwd-relative source path, file in /usr/share/doc/$Name 19 | Files map[string]string `json:"files"` // pwd-relative source path, rpm destination path 20 | 21 | Build func(BuildInput) `json:"build"` 22 | Filename func(Package) string `json:"mkFilename"` 23 | } 24 | 25 | type BuildInput struct { 26 | Output string `json:"out"` 27 | Bin string `json:"bin"` 28 | Doc string `json:"doc"` 29 | Etc string `json:"etc"` 30 | Man string `json:"man"` 31 | Systemd string `json:"systemd"` 32 | } 33 | 34 | func (b BuildInput) String() string { 35 | return b.Output 36 | } 37 | -------------------------------------------------------------------------------- /internal/testdata/hello/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("Hello, world!") 7 | } 8 | -------------------------------------------------------------------------------- /internal/vfs/modtimefs.go: -------------------------------------------------------------------------------- 1 | package vfs 2 | 3 | import ( 4 | "io/fs" 5 | "time" 6 | ) 7 | 8 | // ModTimeFS wraps an fs.FS and overrides all file mtimes with a fixed time. 9 | type ModTimeFS struct { 10 | fs.FS 11 | Time time.Time 12 | } 13 | 14 | // Open overrides the FS.Open method to wrap returned files. 15 | func (m ModTimeFS) Open(name string) (fs.File, error) { 16 | f, err := m.FS.Open(name) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return &modTimeFile{File: f, Time: m.Time}, nil 21 | } 22 | 23 | // ReadDir implements fs.ReadDirFS if the underlying FS supports it. 24 | func (m ModTimeFS) ReadDir(name string) ([]fs.DirEntry, error) { 25 | readDirFS, ok := m.FS.(fs.ReadDirFS) 26 | if !ok { 27 | return nil, &fs.PathError{Op: "ReadDir", Path: name, Err: fs.ErrInvalid} 28 | } 29 | 30 | entries, err := readDirFS.ReadDir(name) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | wrapped := make([]fs.DirEntry, len(entries)) 36 | for i, entry := range entries { 37 | wrapped[i] = modTimeDirEntry{DirEntry: entry, Time: m.Time} 38 | } 39 | return wrapped, nil 40 | } 41 | 42 | // modTimeFile wraps fs.File to override Stat().ModTime(). 43 | type modTimeFile struct { 44 | fs.File 45 | Time time.Time 46 | } 47 | 48 | func (f *modTimeFile) Stat() (fs.FileInfo, error) { 49 | info, err := f.File.Stat() 50 | if err != nil { 51 | return nil, err 52 | } 53 | return modTimeFileInfo{FileInfo: info, Time: f.Time}, nil 54 | } 55 | 56 | // modTimeFileInfo overrides ModTime to return a fixed time. 57 | type modTimeFileInfo struct { 58 | fs.FileInfo 59 | Time time.Time 60 | } 61 | 62 | func (fi modTimeFileInfo) ModTime() time.Time { 63 | return fi.Time 64 | } 65 | 66 | // modTimeDirEntry wraps fs.DirEntry to override Info().ModTime(). 67 | type modTimeDirEntry struct { 68 | fs.DirEntry 69 | Time time.Time 70 | } 71 | 72 | func (d modTimeDirEntry) Info() (fs.FileInfo, error) { 73 | info, err := d.DirEntry.Info() 74 | if err != nil { 75 | return nil, err 76 | } 77 | return modTimeFileInfo{FileInfo: info, Time: d.Time}, nil 78 | } 79 | -------------------------------------------------------------------------------- /internal/yeet/doc.go: -------------------------------------------------------------------------------- 1 | // Package yeet is a set of small helper functions useful for yeeting out scripts. 2 | package yeet 3 | -------------------------------------------------------------------------------- /internal/yeet/yeet.go: -------------------------------------------------------------------------------- 1 | package yeet 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log/slog" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | "time" 12 | 13 | "github.com/Masterminds/semver/v3" 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | var ( 18 | ForceGitVersion = flag.String("force-git-version", os.Getenv("FORCE_GIT_VERSION"), "if set, force git.tag to return this value") 19 | ) 20 | 21 | // current working directory and date:time tag of app boot (useful for tagging slugs) 22 | var ( 23 | WD string 24 | DateTag string 25 | ) 26 | 27 | func init() { 28 | lwd, err := os.Getwd() 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | WD = lwd 34 | DateTag = time.Now().UTC().Format("200601021504") 35 | } 36 | 37 | // ShouldWork explodes if the given command with the given env, working dir and context fails. 38 | func ShouldWork(ctx context.Context, env []string, dir string, cmdName string, args ...string) { 39 | loc, err := exec.LookPath(cmdName) 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | cmd := exec.CommandContext(ctx, loc, args...) 45 | cmd.Dir = dir 46 | cmd.Env = env 47 | 48 | cmd.Stdout = os.Stdout 49 | cmd.Stderr = os.Stderr 50 | 51 | slog.Info("starting process", "pwd", dir, "cmd", loc, "args", args) 52 | 53 | err = cmd.Run() 54 | if err != nil { 55 | panic(err) 56 | } 57 | } 58 | 59 | // Output returns the output of a command or an error. 60 | func Output(ctx context.Context, cmd string, args ...string) (string, error) { 61 | c := exec.CommandContext(ctx, cmd, args...) 62 | c.Env = os.Environ() 63 | c.Stderr = os.Stderr 64 | b, err := c.Output() 65 | if err != nil { 66 | return "", errors.Wrapf(err, `failed to run %v %q`, cmd, args) 67 | } 68 | return string(b), nil 69 | } 70 | 71 | // GitTag returns the curreng git tag. 72 | func GitTag(ctx context.Context) (string, error) { 73 | s, err := Output(ctx, "git", "describe", "--tags", "--dirty=-dev", "--always") 74 | if err != nil { 75 | ee, ok := errors.Cause(err).(*exec.ExitError) 76 | if ok && ee.Exited() { 77 | // probably no git tag 78 | return "dev", nil 79 | } 80 | return "", err 81 | } 82 | 83 | if *ForceGitVersion != "" { 84 | s = *ForceGitVersion 85 | } 86 | 87 | ver, err := semver.NewVersion(strings.TrimSuffix(s, "\n")) 88 | if err != nil { 89 | // probably no git tag 90 | return "devel", nil 91 | } 92 | 93 | return ver.String(), nil 94 | } 95 | 96 | // DockerTag tags a docker image 97 | func DockerTag(ctx context.Context, org, repo, image string) string { 98 | tag, err := GitTag(ctx) 99 | if err != nil { 100 | panic(err) 101 | } 102 | 103 | repoTag := fmt.Sprintf("%s/%s:%s", org, repo, tag) 104 | 105 | ShouldWork(ctx, nil, WD, "docker", "tag", image, repoTag) 106 | 107 | return repoTag 108 | } 109 | 110 | // DockerBuild builds a docker image with the given working directory and tag. 111 | func DockerBuild(ctx context.Context, dir, tag string, args ...string) { 112 | args = append([]string{"build", "-t", tag}, args...) 113 | args = append(args, ".") 114 | ShouldWork(ctx, nil, dir, "docker", args...) 115 | } 116 | 117 | // DockerLoadResult loads a nix-built docker image 118 | func DockerLoadResult(ctx context.Context, at string) { 119 | c := exec.CommandContext(ctx, "docker", "load") 120 | c.Env = os.Environ() 121 | fin, err := os.Open(at) 122 | if err != nil { 123 | panic(err) 124 | } 125 | defer fin.Close() 126 | c.Stdin = fin 127 | 128 | if err := c.Run(); err != nil { 129 | panic(err) 130 | } 131 | } 132 | 133 | // DockerPush pushes a docker image to a given host 134 | func DockerPush(ctx context.Context, image string) { 135 | ShouldWork(ctx, nil, WD, "docker", "push", image) 136 | } 137 | -------------------------------------------------------------------------------- /internal/yeettest/buildpackage.go: -------------------------------------------------------------------------------- 1 | package yeettest 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | "testing" 8 | 9 | "github.com/TecharoHQ/yeet/internal" 10 | "github.com/TecharoHQ/yeet/internal/pkgmeta" 11 | "github.com/TecharoHQ/yeet/internal/yeet" 12 | ) 13 | 14 | type Impl func(p pkgmeta.Package) (string, error) 15 | 16 | func BuildHello(t *testing.T, build Impl, version string, fatal bool) string { 17 | t.Helper() 18 | 19 | dir := t.TempDir() 20 | internal.PackageDestDir = &dir 21 | 22 | p := pkgmeta.Package{ 23 | Name: "hello", 24 | Version: version, 25 | Description: "Hello world", 26 | Homepage: "https://example.com", 27 | License: "MIT", 28 | Platform: runtime.GOOS, 29 | Goarch: runtime.GOARCH, 30 | Build: func(p pkgmeta.BuildInput) { 31 | yeet.ShouldWork(t.Context(), nil, yeet.WD, "go", "build", "-o", filepath.Join(p.Bin, "hello"), "../testdata/hello") 32 | }, 33 | } 34 | 35 | foutpath, err := build(p) 36 | switch fatal { 37 | case true: 38 | if err != nil { 39 | t.Fatalf("Build() error = %v", err) 40 | } 41 | case false: 42 | if err != nil { 43 | t.Logf("Build() error = %v", err) 44 | } 45 | return "" 46 | } 47 | 48 | if foutpath == "" { 49 | t.Fatal("Build() returned empty path") 50 | } 51 | 52 | t.Cleanup(func() { 53 | os.RemoveAll(filepath.Dir(foutpath)) 54 | }) 55 | 56 | return foutpath 57 | } 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@techaro/yeet", 3 | "version": "0.6.0", 4 | "description": "Yeet those actions out with JavaScript!", 5 | "directories": { 6 | "doc": "doc" 7 | }, 8 | "scripts": { 9 | "test": "go test ./...", 10 | "release": "gh workflow run 'Cut Release'", 11 | "format": "prettier --write . && go tool goimports -w ./...", 12 | "prepare": "husky" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/TecharoHQ/yeet.git" 17 | }, 18 | "author": "Xe Iaso ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/TecharoHQ/yeet/issues" 22 | }, 23 | "homepage": "https://github.com/TecharoHQ/yeet#readme", 24 | "commitlint": { 25 | "extends": [ 26 | "@commitlint/config-conventional" 27 | ], 28 | "rules": { 29 | "body-max-line-length": [ 30 | 2, 31 | "always", 32 | 99999 33 | ], 34 | "footer-max-line-length": [ 35 | 2, 36 | "always", 37 | 99999 38 | ], 39 | "signed-off-by": [ 40 | 2, 41 | "always" 42 | ] 43 | } 44 | }, 45 | "lint-staged": { 46 | "**/*.{js,ts,html,json,css,scss,md,mdx}": [ 47 | "prettier -w" 48 | ], 49 | "**/*.{go}": [ 50 | "go tool goimports -w" 51 | ] 52 | }, 53 | "prettier": { 54 | "singleQuote": false, 55 | "tabWidth": 2, 56 | "semi": true, 57 | "trailingComma": "all", 58 | "printWidth": 80 59 | }, 60 | "release": { 61 | "branches": [ 62 | "main" 63 | ], 64 | "plugins": [ 65 | "@semantic-release/commit-analyzer", 66 | "@semantic-release/release-notes-generator", 67 | [ 68 | "@semantic-release/exec", 69 | { 70 | "verifyReleaseCmd": "echo ${nextRelease.version} > .VERSION" 71 | } 72 | ], 73 | [ 74 | "@semantic-release/exec", 75 | { 76 | "verifyReleaseCmd": "go run ./cmd/yeet --force-git-version=$(cat .VERSION)" 77 | } 78 | ], 79 | [ 80 | "@semantic-release/github", 81 | { 82 | "assets": [ 83 | "var/**" 84 | ] 85 | } 86 | ], 87 | [ 88 | "@semantic-release/npm", 89 | { 90 | "npmPublish": false 91 | } 92 | ], 93 | [ 94 | "@semantic-release/changelog", 95 | { 96 | "changeLogFile": "CHANGLOG.md" 97 | } 98 | ], 99 | [ 100 | "@semantic-release/git", 101 | { 102 | "assets": [ 103 | "CHANGELOG.md", 104 | "package.json" 105 | ], 106 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}\n\nSigned-Off-By: Mimi Yasomi " 107 | } 108 | ] 109 | ] 110 | }, 111 | "devDependencies": { 112 | "@commitlint/cli": "^19.8.0", 113 | "@commitlint/config-conventional": "^19.8.0", 114 | "@semantic-release/changelog": "^6.0.3", 115 | "@semantic-release/commit-analyzer": "^13.0.1", 116 | "@semantic-release/git": "^10.0.1", 117 | "@semantic-release/github": "^11.0.1", 118 | "@semantic-release/release-notes-generator": "^14.0.3", 119 | "husky": "^9.1.7", 120 | "lint-staged": "^15.5.1", 121 | "prettier": "^3.5.3", 122 | "semantic-release": "^24.2.3" 123 | }, 124 | "dependencies": { 125 | "@semantic-release/exec": "^7.0.3" 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /var/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /yeet.go: -------------------------------------------------------------------------------- 1 | // Package yeet contains the version number of Yeet. 2 | package yeet 3 | 4 | // Version is the current version of yeet. 5 | // 6 | // This variable is set at build time using the -X linker flag. If not set, 7 | // it defaults to "devel". 8 | var Version = "devel" 9 | 10 | // BuildMethod contains the method used to build the yeet binary. 11 | // 12 | // This variable is set at build time using the -X linker flag. If not set, 13 | // it defaults to "go-build". 14 | var BuildMethod = "go-build" 15 | -------------------------------------------------------------------------------- /yeet_test.go: -------------------------------------------------------------------------------- 1 | package yeet 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "testing" 7 | 8 | "github.com/TecharoHQ/yeet/internal" 9 | "github.com/TecharoHQ/yeet/internal/yeet" 10 | ) 11 | 12 | func TestBuildOwnPackages(t *testing.T) { 13 | if os.Getenv("CI") == "" { 14 | t.Skip("Skipping test in non-CI environment") 15 | } 16 | 17 | type packageJSON struct { 18 | Version string `json:"version"` 19 | } 20 | 21 | fin, err := os.ReadFile("package.json") 22 | if err != nil { 23 | t.Fatalf("can't read package.json: %v", err) 24 | } 25 | 26 | var pkg packageJSON 27 | if err := json.Unmarshal(fin, &pkg); err != nil { 28 | t.Fatalf("can't unmarshal package.json: %v", err) 29 | } 30 | 31 | dir := t.TempDir() 32 | internal.PackageDestDir = &dir 33 | yeet.ShouldWork(t.Context(), nil, yeet.WD, "go", "run", "./cmd/yeet", "--force-git-version", pkg.Version, "--package-dest-dir", t.TempDir()) 34 | } 35 | -------------------------------------------------------------------------------- /yeetfile.js: -------------------------------------------------------------------------------- 1 | const pkgs = []; 2 | 3 | [ 4 | "amd64", 5 | "arm64", 6 | "ppc64le", 7 | ].forEach((goarch) => 8 | [deb, rpm, tarball].forEach((method) => { 9 | pkgs.push( 10 | method.build({ 11 | name: "yeet", 12 | description: "Yeet out scripts with maximum haste!", 13 | homepage: "https://techaro.lol", 14 | license: "MIT", 15 | goarch, 16 | 17 | documentation: { 18 | "README.md": "README.md", 19 | "doc/api.md": "api.md", 20 | }, 21 | 22 | build: ({ bin }) => { 23 | $`go build -o ${bin}/yeet -ldflags '-s -w -extldflags "-static" -X "github.com/TecharoHQ/yeet.Version=${git.tag()}" -X "github.com/TecharoHQ/yeet.BuildMethod=${method.name}"' ./cmd/yeet`; 24 | }, 25 | }), 26 | ); 27 | }), 28 | ); 29 | 30 | tarball.build({ 31 | name: "yeet-src-vendor", 32 | license: "MIT", 33 | // XXX(Xe): This is needed otherwise go will be very sad. 34 | platform: yeet.goos, 35 | goarch: yeet.goarch, 36 | 37 | build: ({ out }) => { 38 | // prepare clean checkout in $out 39 | $`git archive --format=tar HEAD | tar xC ${out}`; 40 | // vendor Go dependencies 41 | $`cd ${out} && go mod vendor`; 42 | // write VERSION file 43 | $`echo ${git.tag()} > ${out}/VERSION`; 44 | }, 45 | 46 | mkFilename: ({ name, version }) => `${name}-${version}`, 47 | }); --------------------------------------------------------------------------------