├── .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 | 
4 | 
5 | 
6 | 
7 | 
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 | [](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 | [](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 | });
--------------------------------------------------------------------------------