├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── lint.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── LICENSE ├── README.md ├── builder.go ├── builder_test.go ├── cmd ├── cobra.go ├── commands.go ├── main.go ├── main_test.go ├── main_unix_test.go ├── main_windows_test.go └── xcaddy │ └── main.go ├── environment.go ├── environment_test.go ├── go.mod ├── go.sum ├── internal └── utils │ ├── environment.go │ ├── resource.go │ └── resources │ └── ico │ ├── caddy-bg.ico │ └── caddy.ico ├── io.go └── platforms.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [mholt] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Used as inspiration: https://github.com/mvdan/github-actions-golang 2 | 3 | name: Tests 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | test: 15 | strategy: 16 | # Default is true, cancels jobs for other platforms in the matrix if one fails 17 | fail-fast: false 18 | matrix: 19 | os: [ ubuntu-latest, macos-latest, windows-latest ] 20 | go: [ '1.22', '1.23' ] 21 | 22 | # Set some variables per OS, usable via ${{ matrix.VAR }} 23 | # XCADDY_BIN_PATH: the path to the compiled xcaddy binary, for artifact publishing 24 | # SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True') 25 | include: 26 | - os: ubuntu-latest 27 | XCADDY_BIN_PATH: ./cmd/xcaddy/xcaddy 28 | SUCCESS: 0 29 | 30 | - os: macos-latest 31 | XCADDY_BIN_PATH: ./cmd/xcaddy/xcaddy 32 | SUCCESS: 0 33 | 34 | - os: windows-latest 35 | XCADDY_BIN_PATH: ./cmd/xcaddy/xcaddy.exe 36 | SUCCESS: 'True' 37 | 38 | runs-on: ${{ matrix.os }} 39 | 40 | steps: 41 | - name: Install Go 42 | uses: actions/setup-go@v5 43 | with: 44 | go-version: ${{ matrix.go }} 45 | 46 | - name: Checkout code 47 | uses: actions/checkout@v4 48 | 49 | - name: Print Go version and environment 50 | id: vars 51 | shell: bash 52 | run: | 53 | printf "Using go at: $(which go)\n" 54 | printf "Go version: $(go version)\n" 55 | printf "\n\nGo environment:\n\n" 56 | go env 57 | printf "\n\nSystem environment:\n\n" 58 | env 59 | # Calculate the short SHA1 hash of the git commit 60 | echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT 61 | 62 | - name: Cache the build cache 63 | uses: actions/cache@v4 64 | with: 65 | # In order: 66 | # * Module download cache 67 | # * Build cache (Linux) 68 | # * Build cache (Mac) 69 | # * Build cache (Windows) 70 | path: | 71 | ~/go/pkg/mod 72 | ~/.cache/go-build 73 | ~/Library/Caches/go-build 74 | ~\AppData\Local\go-build 75 | key: ${{ runner.os }}-${{ matrix.go }}-go-ci-${{ hashFiles('**/go.sum') }} 76 | restore-keys: | 77 | ${{ runner.os }}-${{ matrix.go }}-go-ci 78 | 79 | - name: Get dependencies 80 | run: | 81 | go get -v -t -d ./... 82 | 83 | - name: Build xcaddy 84 | working-directory: ./cmd/xcaddy 85 | env: 86 | CGO_ENABLED: 0 87 | GOARCH: amd64 88 | run: | 89 | go build -trimpath -ldflags="-w -s" -v 90 | 91 | - name: Output version 92 | run: | 93 | ${{ matrix.XCADDY_BIN_PATH }} version 94 | 95 | - name: Publish Build Artifact 96 | uses: actions/upload-artifact@v4 97 | with: 98 | name: xcaddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }} 99 | path: ${{ matrix.XCADDY_BIN_PATH }} 100 | compression-level: 0 101 | 102 | - name: Run tests 103 | run: | 104 | go test -v -coverprofile="cover-profile.out" -short -race ./... 105 | 106 | goreleaser-check: 107 | runs-on: ubuntu-latest 108 | steps: 109 | - name: checkout 110 | uses: actions/checkout@v4 111 | 112 | - name: Print Go version and environment 113 | id: vars 114 | shell: bash 115 | run: | 116 | printf "Using go at: $(which go)\n" 117 | printf "Go version: $(go version)\n" 118 | printf "\n\nGo environment:\n\n" 119 | go env 120 | printf "\n\nSystem environment:\n\n" 121 | env 122 | # Calculate the short SHA1 hash of the git commit 123 | echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT 124 | 125 | - uses: goreleaser/goreleaser-action@v6 126 | with: 127 | version: latest 128 | args: check 129 | env: 130 | TAG: ${{ steps.vars.outputs.short_sha }} 131 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | # From https://github.com/golangci/golangci-lint-action 13 | golangci: 14 | name: lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-go@v5 19 | with: 20 | go-version: '1.23.x' 21 | 22 | - name: golangci-lint 23 | uses: golangci/golangci-lint-action@v8 24 | with: 25 | version: 'latest' 26 | # Optional: show only new issues if it's a pull request. The default value is `false`. 27 | # only-new-issues: true 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | strategy: 12 | matrix: 13 | os: [ ubuntu-latest ] 14 | go: [ '1.23' ] 15 | runs-on: ${{ matrix.os }} 16 | 17 | steps: 18 | - name: Install Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: ${{ matrix.go }} 22 | 23 | - name: Checkout code 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | 28 | # Force fetch upstream tags -- because 65 minutes 29 | # tl;dr: actions/checkout@v2 runs this line: 30 | # git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/ 31 | # which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran: 32 | # git fetch --prune --unshallow 33 | # which doesn't overwrite that tag because that would be destructive. 34 | # Credit to @francislavoie for the investigation. 35 | # https://github.com/actions/checkout/issues/290#issuecomment-680260080 36 | - name: Force fetch upstream tags 37 | run: git fetch --tags --force 38 | 39 | # https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027 40 | - name: Print Go version and environment 41 | id: vars 42 | run: | 43 | printf "Using go at: $(which go)\n" 44 | printf "Go version: $(go version)\n" 45 | printf "\n\nGo environment:\n\n" 46 | go env 47 | printf "\n\nSystem environment:\n\n" 48 | env 49 | echo "version_tag=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT 50 | echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT 51 | 52 | # Add "pip install" CLI tools to PATH 53 | echo ~/.local/bin >> $GITHUB_PATH 54 | 55 | # Parse semver 56 | TAG=${GITHUB_REF/refs\/tags\//} 57 | SEMVER_RE='[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z\.-]*\)' 58 | TAG_MAJOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\1#"` 59 | TAG_MINOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\2#"` 60 | TAG_PATCH=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\3#"` 61 | TAG_SPECIAL=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\4#"` 62 | echo "tag_major=${TAG_MAJOR}" >> $GITHUB_OUTPUT 63 | echo "tag_minor=${TAG_MINOR}" >> $GITHUB_OUTPUT 64 | echo "tag_patch=${TAG_PATCH}" >> $GITHUB_OUTPUT 65 | echo "tag_special=${TAG_SPECIAL}" >> $GITHUB_OUTPUT 66 | 67 | # Cloudsmith CLI tooling for pushing releases 68 | # See https://help.cloudsmith.io/docs/cli 69 | - name: Install Cloudsmith CLI 70 | run: pip install --upgrade cloudsmith-cli 71 | 72 | - name: Validate commits and tag signatures 73 | run: | 74 | 75 | # Import Matt Holt's key 76 | curl 'https://github.com/mholt.gpg' | gpg --import 77 | 78 | echo "Verifying the tag: ${{ steps.vars.outputs.version_tag }}" 79 | # tags are only accepted if signed by Matt's key 80 | git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1 81 | 82 | - name: Cache the build cache 83 | uses: actions/cache@v4 84 | with: 85 | # In order: 86 | # * Module download cache 87 | # * Build cache (Linux) 88 | # * Build cache (Mac) 89 | # * Build cache (Windows) 90 | path: | 91 | ~/go/pkg/mod 92 | ~/.cache/go-build 93 | ~/Library/Caches/go-build 94 | ~\AppData\Local\go-build 95 | key: ${{ runner.os }}-go${{ matrix.go }}-release-${{ hashFiles('**/go.sum') }} 96 | restore-keys: | 97 | ${{ runner.os }}-go${{ matrix.go }}-release 98 | 99 | # GoReleaser will take care of publishing those artifacts into the release 100 | - name: Run GoReleaser 101 | uses: goreleaser/goreleaser-action@v6 102 | with: 103 | version: latest 104 | args: release --clean 105 | env: 106 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 107 | TAG: ${{ steps.vars.outputs.version_tag }} 108 | 109 | # Publish only stable releases to Cloudsmith 110 | # See https://cloudsmith.io/~caddy/repos/xcaddy/ 111 | - name: Publish .deb to Cloudsmith 112 | if: ${{ steps.vars.outputs.tag_special == '' }} 113 | env: 114 | CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} 115 | run: | 116 | for filename in dist/*.deb; do 117 | # armv6 and armv7 are both "armhf" so we can skip the duplicate 118 | if [[ "$filename" == *"armv6"* ]]; then 119 | echo "Skipping $filename" 120 | continue 121 | fi 122 | 123 | echo "Pushing $filename to 'xcaddy'" 124 | cloudsmith push deb caddy/xcaddy/any-distro/any-version $filename 125 | done 126 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | buildenv_*/ 2 | _gitignore/ 3 | 4 | # build artifacts 5 | cmd/xcaddy/xcaddy 6 | cmd/xcaddy/xcaddy.exe 7 | cmd/xcaddy/caddy 8 | cmd/xcaddy/caddy.exe 9 | 10 | # mac 11 | .DS_Store 12 | 13 | # goreleaser artifacts 14 | dist/ 15 | 16 | # jb 17 | .idea 18 | .fleet -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | issues-exit-code: 1 4 | tests: false 5 | output: 6 | formats: 7 | text: 8 | path: stdout 9 | print-linter-name: true 10 | print-issued-lines: true 11 | linters: 12 | enable: 13 | - bodyclose 14 | - gosec 15 | - misspell 16 | - prealloc 17 | - unconvert 18 | settings: 19 | errcheck: 20 | exclude-functions: 21 | - fmt.* 22 | - Read.* 23 | misspell: 24 | locale: US 25 | staticcheck: 26 | checks: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-QF1006", "-QF1008"] # default, and exclude 1 more undesired check 27 | exclusions: 28 | generated: lax 29 | presets: 30 | - comments 31 | - common-false-positives 32 | - legacy 33 | - std-error-handling 34 | rules: 35 | - linters: 36 | - gosec 37 | text: G107 38 | - linters: 39 | - gosec 40 | text: G204 41 | - linters: 42 | - gosec 43 | text: G306 44 | - linters: 45 | - gosec 46 | text: G115 47 | formatters: 48 | enable: 49 | - gofmt 50 | - goimports 51 | exclusions: 52 | generated: lax 53 | 54 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | before: 4 | hooks: 5 | # The build is done in this particular way to build xcaddy in a designated directory named in .gitignore. 6 | # This is so we can run goreleaser on tag without Git complaining of being dirty. The main.go in cmd/xcaddy directory 7 | # cannot be built within that directory due to changes necessary for the build causing Git to be dirty, which 8 | # subsequently causes gorleaser to refuse running. 9 | - mkdir -p xcaddy-build 10 | - cp cmd/xcaddy/main.go xcaddy-build/main.go 11 | - cp ./go.mod xcaddy-build/go.mod 12 | - sed -i.bkp 's|github.com/caddyserver/xcaddy|xcaddy|g' ./xcaddy-build/go.mod 13 | # GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env 14 | # so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate 15 | - go mod edit -require=github.com/caddyserver/xcaddy@{{.Env.TAG}} ./xcaddy-build/go.mod 16 | # as of Go 1.16, `go` commands no longer automatically change go.{mod,sum}. We now have to explicitly 17 | # run `go mod tidy`. The `/bin/sh -c '...'` is because goreleaser can't find cd in PATH without shell invocation. 18 | - /bin/sh -c 'cd ./xcaddy-build && go mod tidy' 19 | - go mod download 20 | 21 | builds: 22 | - env: 23 | - CGO_ENABLED=0 24 | main: main.go 25 | dir: ./xcaddy-build 26 | binary: xcaddy 27 | goos: 28 | - darwin 29 | - linux 30 | - windows 31 | - freebsd 32 | goarch: 33 | - amd64 34 | - arm 35 | - arm64 36 | - s390x 37 | - ppc64le 38 | - riscv64 39 | goarm: 40 | - "5" 41 | - "6" 42 | - "7" 43 | ignore: 44 | - goos: darwin 45 | goarch: arm 46 | - goos: darwin 47 | goarch: ppc64le 48 | - goos: darwin 49 | goarch: riscv64 50 | - goos: darwin 51 | goarch: s390x 52 | - goos: windows 53 | goarch: ppc64le 54 | - goos: windows 55 | goarch: riscv64 56 | - goos: windows 57 | goarch: s390x 58 | - goos: freebsd 59 | goarch: ppc64le 60 | - goos: freebsd 61 | goarch: riscv64 62 | - goos: freebsd 63 | goarch: s390x 64 | - goos: freebsd 65 | goarch: arm 66 | goarm: "5" 67 | flags: 68 | - -trimpath 69 | ldflags: 70 | - -s -w 71 | 72 | archives: 73 | - format_overrides: 74 | - goos: windows 75 | formats: zip 76 | name_template: >- 77 | {{ .ProjectName }}_ 78 | {{- .Version }}_ 79 | {{- if eq .Os "darwin" }}mac{{ else }}{{ .Os }}{{ end }}_ 80 | {{- .Arch }} 81 | {{- with .Arm }}v{{ . }}{{ end }} 82 | {{- with .Mips }}_{{ . }}{{ end }} 83 | {{- if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }} 84 | 85 | checksum: 86 | algorithm: sha512 87 | 88 | nfpms: 89 | - id: default 90 | package_name: xcaddy 91 | 92 | vendor: Dyanim 93 | homepage: https://caddyserver.com 94 | maintainer: Matthew Holt 95 | description: | 96 | Build Caddy with plugins 97 | license: Apache 2.0 98 | 99 | formats: 100 | - deb 101 | # - rpm 102 | 103 | bindir: /usr/bin 104 | 105 | release: 106 | github: 107 | owner: caddyserver 108 | name: xcaddy 109 | draft: false 110 | prerelease: auto 111 | 112 | changelog: 113 | sort: asc 114 | filters: 115 | exclude: 116 | - '^readme:' 117 | - '^chore:' 118 | - '^ci:' 119 | - '^docs?:' 120 | - '^tests?:' 121 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `xcaddy` - Custom Caddy Builder 2 | =============================== 3 | 4 | This command line tool and associated Go package makes it easy to make custom builds of the [Caddy Web Server](https://github.com/caddyserver/caddy). 5 | 6 | It is used heavily by Caddy plugin developers as well as anyone who wishes to make custom `caddy` binaries (with or without plugins). 7 | 8 | Stay updated, be aware of changes, and please submit feedback! Thanks! 9 | 10 | ## Requirements 11 | 12 | - [Go installed](https://golang.org/doc/install) 13 | 14 | ## Install 15 | 16 | You can [download binaries](https://github.com/caddyserver/xcaddy/releases) that are already compiled for your platform from the Release tab. 17 | 18 | You may also build `xcaddy` from source: 19 | 20 | ```bash 21 | go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest 22 | ``` 23 | 24 | For Debian, Ubuntu, and Raspbian, an `xcaddy` package is available from our [Cloudsmith repo](https://cloudsmith.io/~caddy/repos/xcaddy/packages/): 25 | 26 | ```bash 27 | sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https 28 | curl -1sLf 'https://dl.cloudsmith.io/public/caddy/xcaddy/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-xcaddy-archive-keyring.gpg 29 | curl -1sLf 'https://dl.cloudsmith.io/public/caddy/xcaddy/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-xcaddy.list 30 | sudo apt update 31 | sudo apt install xcaddy 32 | ``` 33 | 34 | ## :warning: Pro tip 35 | 36 | If you find yourself fighting xcaddy in relation to your custom or proprietary build or development process, **it might be easier to just build Caddy manually!** 37 | 38 | Caddy's [main.go file](https://github.com/caddyserver/caddy/blob/master/cmd/caddy/main.go), the main entry point to the application, has instructions in the comments explaining how to build Caddy essentially the same way xcaddy does it. But when you use the `go` command directly, you have more control over the whole thing and it may save you a lot of trouble. 39 | 40 | The manual build procedure is very easy: just copy the main.go into a new folder, initialize a Go module, plug in your plugins (add an `import` for each one) and then run `go build`. Of course, you may wish to customize the go.mod file to your liking (specific dependency versions, replacements, etc). 41 | 42 | 43 | ## Command usage 44 | 45 | The `xcaddy` command has two primary uses: 46 | 47 | 1. Compile custom `caddy` binaries 48 | 2. A replacement for `go run` while developing Caddy plugins 49 | 50 | The `xcaddy` command will use the latest version of Caddy by default. You can customize this for all invocations by setting the `CADDY_VERSION` environment variable. 51 | 52 | As usual with `go` command, the `xcaddy` command will pass the `GOOS`, `GOARCH`, and `GOARM` environment variables through for cross-compilation. 53 | 54 | Note that `xcaddy` will ignore the `vendor/` folder with `-mod=readonly`. 55 | 56 | 57 | ### Custom builds 58 | 59 | Syntax: 60 | 61 | ``` 62 | $ xcaddy build [] 63 | [--output ] 64 | [--with ...] 65 | [--replace ...] 66 | [--embed <[alias]:path/to/dir>...] 67 | ``` 68 | 69 | - `` is the core Caddy version to build; defaults to `CADDY_VERSION` env variable or latest.
70 | This can be the keyword `latest`, which will use the latest stable tag, or any git ref such as: 71 | - A tag like `v2.0.1` 72 | - A branch like `master` 73 | - A commit like `a58f240d3ecbb59285303746406cab50217f8d24` 74 | 75 | - `--output` changes the output file. 76 | 77 | - `--with` can be used multiple times to add plugins by specifying the Go module name and optionally its version, similar to `go get`. Module name is required, but specific version and/or local replacement are optional. 78 | 79 | - `--replace` is like `--with`, but does not add a blank import to the code; it only writes a replace directive to `go.mod`, which is useful when developing on Caddy's dependencies (ones that are not Caddy modules). Try this if you got an error when using `--with`, like `cannot find module providing package`. 80 | 81 | - `--embed` can be used to embed the contents of a directory into the Caddy executable. `--embed` can be passed multiple times with separate source directories. The source directory can be prefixed with a custom alias and a colon `:` to write the embedded files into an aliased subdirectory, which is useful when combined with the `root` directive and sub-directive. 82 | 83 | #### Examples 84 | 85 | ```bash 86 | $ xcaddy build \ 87 | --with github.com/caddyserver/ntlm-transport 88 | 89 | $ xcaddy build v2.0.1 \ 90 | --with github.com/caddyserver/ntlm-transport@v0.1.1 91 | 92 | $ xcaddy build master \ 93 | --with github.com/caddyserver/ntlm-transport 94 | 95 | $ xcaddy build a58f240d3ecbb59285303746406cab50217f8d24 \ 96 | --with github.com/caddyserver/ntlm-transport 97 | 98 | $ xcaddy build \ 99 | --with github.com/caddyserver/ntlm-transport=../../my-fork 100 | 101 | $ xcaddy build \ 102 | --with github.com/caddyserver/ntlm-transport@v0.1.1=../../my-fork 103 | ``` 104 | 105 | You can even replace Caddy core using the `--with` flag: 106 | 107 | ``` 108 | $ xcaddy build \ 109 | --with github.com/caddyserver/caddy/v2=../../my-caddy-fork 110 | 111 | $ xcaddy build \ 112 | --with github.com/caddyserver/caddy/v2=github.com/my-user/caddy/v2@some-branch 113 | ``` 114 | 115 | This allows you to hack on Caddy core (and optionally plug in extra modules at the same time!) with relative ease. 116 | 117 | --- 118 | 119 | If `--embed` is used without an alias prefix, the contents of the source directory are written directly into the root directory of the embedded filesystem within the Caddy executable. The contents of multiple unaliased source directories will be merged together: 120 | 121 | ``` 122 | $ xcaddy build --embed ./my-files --embed ./my-other-files 123 | $ cat Caddyfile 124 | { 125 | # You must declare a custom filesystem using the `embedded` module. 126 | # The first argument to `filesystem` is an arbitrary identifier 127 | # that will also be passed to `fs` directives. 128 | filesystem my_embeds embedded 129 | } 130 | 131 | localhost { 132 | # This serves the files or directories that were 133 | # contained inside of ./my-files and ./my-other-files 134 | file_server { 135 | fs my_embeds 136 | } 137 | } 138 | ``` 139 | 140 | You may also prefix the source directory with a custom alias and colon separator to write the source directory's contents to a separate subdirectory within the `embedded` filesystem: 141 | 142 | ``` 143 | $ xcaddy build --embed foo:./sites/foo --embed bar:./sites/bar 144 | $ cat Caddyfile 145 | { 146 | filesystem my_embeds embedded 147 | } 148 | 149 | foo.localhost { 150 | # This serves the files or directories that were 151 | # contained inside of ./sites/foo 152 | root * /foo 153 | file_server { 154 | fs my_embeds 155 | } 156 | } 157 | 158 | bar.localhost { 159 | # This serves the files or directories that were 160 | # contained inside of ./sites/bar 161 | root * /bar 162 | file_server { 163 | fs my_embeds 164 | } 165 | } 166 | ``` 167 | 168 | This allows you to serve 2 sites from 2 different embedded directories, which are referenced by aliases, from a single Caddy executable. 169 | 170 | --- 171 | 172 | If you need to work on Caddy's dependencies, you can use the `--replace` flag to replace it with a local copy of that dependency (or your fork on github etc if you need): 173 | 174 | ``` 175 | $ xcaddy build some-branch-on-caddy \ 176 | --replace golang.org/x/net=../net 177 | ``` 178 | 179 | ### For plugin development 180 | 181 | If you run `xcaddy` from within the folder of the Caddy plugin you're working on _without the `build` subcommand_, it will build Caddy with your current module and run it, as if you manually plugged it in and invoked `go run`. 182 | 183 | The binary will be built and run from the current directory, then cleaned up. 184 | 185 | The current working directory must be inside an initialized Go module. 186 | 187 | Syntax: 188 | 189 | ``` 190 | $ xcaddy 191 | ``` 192 | - `` are passed through to the `caddy` command. 193 | 194 | For example: 195 | 196 | ```bash 197 | $ xcaddy list-modules 198 | $ xcaddy run 199 | $ xcaddy run --config caddy.json 200 | ``` 201 | 202 | The race detector can be enabled by setting `XCADDY_RACE_DETECTOR=1`. The DWARF debug info can be enabled by setting `XCADDY_DEBUG=1`. 203 | 204 | 205 | ### Getting `xcaddy`'s version 206 | 207 | ``` 208 | $ xcaddy version 209 | ``` 210 | 211 | 212 | ## Library usage 213 | 214 | ```go 215 | builder := xcaddy.Builder{ 216 | CaddyVersion: "v2.0.0", 217 | Plugins: []xcaddy.Dependency{ 218 | { 219 | ModulePath: "github.com/caddyserver/ntlm-transport", 220 | Version: "v0.1.1", 221 | }, 222 | }, 223 | } 224 | err := builder.Build(context.Background(), "./caddy") 225 | ``` 226 | 227 | Versions can be anything compatible with `go get`. 228 | 229 | 230 | 231 | ## Environment variables 232 | 233 | Because the subcommands and flags are constrained to benefit rapid plugin prototyping, xcaddy does read some environment variables to take cues for its behavior and/or configuration when there is no room for flags. 234 | 235 | - `CADDY_VERSION` sets the version of Caddy to build. 236 | - `XCADDY_RACE_DETECTOR=1` enables the Go race detector in the build. 237 | - `XCADDY_DEBUG=1` enables the DWARF debug information in the build. 238 | - `XCADDY_SETCAP=1` will run `sudo setcap cap_net_bind_service=+ep` on the resulting binary. By default, the `sudo` command will be used if it is found; set `XCADDY_SUDO=0` to avoid using `sudo` if necessary. 239 | - `XCADDY_SKIP_BUILD=1` causes xcaddy to not compile the program, it is used in conjunction with build tools such as [GoReleaser](https://goreleaser.com). Implies `XCADDY_SKIP_CLEANUP=1`. 240 | - `XCADDY_SKIP_CLEANUP=1` causes xcaddy to leave build artifacts on disk after exiting. 241 | - `XCADDY_WHICH_GO` sets the go command to use when for example more then 1 version of go is installed. 242 | - `XCADDY_GO_BUILD_FLAGS` overrides default build arguments. Supports Unix-style shell quoting, for example: XCADDY_GO_BUILD_FLAGS="-ldflags '-w -s'". The provided flags are applied to `go` commands: build, clean, get, install, list, run, and test 243 | - `XCADDY_GO_MOD_FLAGS` overrides default `go mod` arguments. Supports Unix-style shell quoting. 244 | 245 | --- 246 | 247 | © 2020 Matthew Holt 248 | -------------------------------------------------------------------------------- /builder.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Matthew Holt 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package xcaddy 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "fmt" 21 | "log" 22 | "os" 23 | "path" 24 | "path/filepath" 25 | "regexp" 26 | "runtime" 27 | "strconv" 28 | "strings" 29 | "time" 30 | 31 | "github.com/Masterminds/semver/v3" 32 | "github.com/caddyserver/xcaddy/internal/utils" 33 | ) 34 | 35 | // Builder can produce a custom Caddy build with the 36 | // configuration it represents. 37 | type Builder struct { 38 | Compile 39 | CaddyVersion string `json:"caddy_version,omitempty"` 40 | Plugins []Dependency `json:"plugins,omitempty"` 41 | Replacements []Replace `json:"replacements,omitempty"` 42 | TimeoutGet time.Duration `json:"timeout_get,omitempty"` 43 | TimeoutBuild time.Duration `json:"timeout_build,omitempty"` 44 | RaceDetector bool `json:"race_detector,omitempty"` 45 | SkipCleanup bool `json:"skip_cleanup,omitempty"` 46 | SkipBuild bool `json:"skip_build,omitempty"` 47 | Debug bool `json:"debug,omitempty"` 48 | BuildFlags string `json:"build_flags,omitempty"` 49 | ModFlags string `json:"mod_flags,omitempty"` 50 | 51 | // Experimental: subject to change 52 | EmbedDirs []struct { 53 | Dir string `json:"dir,omitempty"` 54 | Name string `json:"name,omitempty"` 55 | } `json:"embed_dir,omitempty"` 56 | } 57 | 58 | // Build builds Caddy at the configured version with the 59 | // configured plugins and plops down a binary at outputFile. 60 | func (b Builder) Build(ctx context.Context, outputFile string) error { 61 | var cancel context.CancelFunc 62 | if b.TimeoutBuild > 0 { 63 | ctx, cancel = context.WithTimeout(ctx, b.TimeoutBuild) 64 | defer cancel() 65 | } 66 | if outputFile == "" { 67 | return fmt.Errorf("output file path is required") 68 | } 69 | // the user's specified output file might be relative, and 70 | // because the `go build` command is executed in a different, 71 | // temporary folder, we convert the user's input to an 72 | // absolute path so it goes the expected place 73 | absOutputFile, err := filepath.Abs(outputFile) 74 | if err != nil { 75 | return err 76 | } 77 | log.Printf("[INFO] absolute output file path: %s", absOutputFile) 78 | 79 | // set some defaults from the environment, if applicable 80 | if b.OS == "" { 81 | b.OS = utils.GetGOOS() 82 | } 83 | if b.Arch == "" { 84 | b.Arch = utils.GetGOARCH() 85 | } 86 | if b.ARM == "" { 87 | b.ARM = os.Getenv("GOARM") 88 | } 89 | 90 | // prepare the build environment 91 | buildEnv, err := b.newEnvironment(ctx) 92 | if err != nil { 93 | return err 94 | } 95 | defer buildEnv.Close() 96 | 97 | // generating windows resources for embedding 98 | if b.OS == "windows" { 99 | // get version string, we need to parse the output to get the exact version instead tag, branch or commit 100 | cmd, err := buildEnv.newGoBuildCommand(ctx, "list", "-m", buildEnv.caddyModulePath) 101 | if err != nil { 102 | return err 103 | } 104 | var buffer bytes.Buffer 105 | cmd.Stdout = &buffer 106 | err = buildEnv.runCommand(ctx, cmd) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | // output looks like: github.com/caddyserver/caddy/v2 v2.7.6 112 | version := strings.TrimPrefix(buffer.String(), buildEnv.caddyModulePath) 113 | // if caddy replacement is a local directory, version will be 114 | // like v2.8.4 => c:\Users\test\caddy 115 | // see https://github.com/caddyserver/xcaddy/issues/215 116 | // strings.Cut return the string unchanged if separator is not found 117 | version, _, _ = strings.Cut(version, "=>") 118 | version = strings.TrimSpace(version) 119 | err = utils.WindowsResource(version, outputFile, buildEnv.tempFolder) 120 | if err != nil { 121 | return err 122 | } 123 | } 124 | 125 | if b.SkipBuild { 126 | log.Printf("[INFO] Skipping build as requested") 127 | 128 | return nil 129 | } 130 | 131 | // prepare the environment for the go command; for 132 | // the most part we want it to inherit our current 133 | // environment, with a few customizations 134 | env := os.Environ() 135 | env = setEnv(env, "GOOS="+b.OS) 136 | env = setEnv(env, "GOARCH="+b.Arch) 137 | env = setEnv(env, "GOARM="+b.ARM) 138 | if b.RaceDetector && !b.Compile.Cgo { 139 | log.Println("[WARNING] Enabling cgo because it is required by the race detector") 140 | b.Compile.Cgo = true 141 | } 142 | env = setEnv(env, fmt.Sprintf("CGO_ENABLED=%s", b.Compile.CgoEnabled())) 143 | 144 | log.Println("[INFO] Building Caddy") 145 | 146 | // tidy the module to ensure go.mod and go.sum are consistent with the module prereq 147 | tidyCmd := buildEnv.newGoModCommand(ctx, "tidy", "-e") 148 | if err := buildEnv.runCommand(ctx, tidyCmd); err != nil { 149 | return err 150 | } 151 | 152 | // compile 153 | cmd, err := buildEnv.newGoBuildCommand(ctx, "build", 154 | "-o", absOutputFile, 155 | ) 156 | if err != nil { 157 | return err 158 | } 159 | if b.Debug { 160 | // support dlv 161 | cmd.Args = append(cmd.Args, "-gcflags", "all=-N -l") 162 | } else { 163 | if buildEnv.buildFlags == "" { 164 | cmd.Args = append(cmd.Args, 165 | "-ldflags", "-w -s", // trim debug symbols 166 | "-trimpath", 167 | "-tags", "nobadger,nomysql,nopgx", 168 | ) 169 | } 170 | } 171 | 172 | if b.RaceDetector { 173 | cmd.Args = append(cmd.Args, "-race") 174 | } 175 | cmd.Env = env 176 | err = buildEnv.runCommand(ctx, cmd) 177 | if err != nil { 178 | return err 179 | } 180 | 181 | log.Printf("[INFO] Build complete: %s", outputFile) 182 | 183 | return nil 184 | } 185 | 186 | // setEnv sets an environment variable-value pair in 187 | // env, overriding an existing variable if it already 188 | // exists. The env slice is one such as is returned 189 | // by os.Environ(), and set must also have the form 190 | // of key=value. 191 | func setEnv(env []string, set string) []string { 192 | parts := strings.SplitN(set, "=", 2) 193 | key := parts[0] 194 | for i := 0; i < len(env); i++ { 195 | if strings.HasPrefix(env[i], key+"=") { 196 | env[i] = set 197 | return env 198 | } 199 | } 200 | return append(env, set) 201 | } 202 | 203 | // Dependency pairs a Go module path with a version. 204 | type Dependency struct { 205 | // The name (import path) of the Go package. If at a version > 1, 206 | // it should contain semantic import version (i.e. "/v2"). 207 | // Used with `go get`. 208 | PackagePath string `json:"module_path,omitempty"` 209 | 210 | // The version of the Go module, as used with `go get`. 211 | Version string `json:"version,omitempty"` 212 | } 213 | 214 | func (d Dependency) String() string { 215 | if d.Version != "" { 216 | return d.PackagePath + "@" + d.Version 217 | } 218 | return d.PackagePath 219 | } 220 | 221 | // ReplacementPath represents an old or new path component 222 | // within a Go module replacement directive. 223 | type ReplacementPath string 224 | 225 | // Param reformats a go.mod replace directive to be 226 | // compatible with the `go mod edit` command. 227 | func (r ReplacementPath) Param() string { 228 | return strings.Replace(string(r), " ", "@", 1) 229 | } 230 | 231 | func (r ReplacementPath) String() string { return string(r) } 232 | 233 | // Replace represents a Go module replacement. 234 | type Replace struct { 235 | // The import path of the module being replaced. 236 | Old ReplacementPath `json:"old,omitempty"` 237 | 238 | // The path to the replacement module. 239 | New ReplacementPath `json:"new,omitempty"` 240 | } 241 | 242 | // NewReplace creates a new instance of Replace provided old and 243 | // new Go module paths 244 | func NewReplace(old, new string) Replace { 245 | return Replace{ 246 | Old: ReplacementPath(old), 247 | New: ReplacementPath(new), 248 | } 249 | } 250 | 251 | // newTempFolder creates a new folder in a temporary location. 252 | // It is the caller's responsibility to remove the folder when finished. 253 | func newTempFolder() (string, error) { 254 | var parentDir string 255 | if runtime.GOOS == "darwin" { 256 | // After upgrading to macOS High Sierra, Caddy builds mysteriously 257 | // started missing the embedded version information that -ldflags 258 | // was supposed to produce. But it only happened on macOS after 259 | // upgrading to High Sierra, and it didn't happen with the usual 260 | // `go run build.go` -- only when using a buildenv. Bug in git? 261 | // Nope. Not a bug in Go 1.10 either. Turns out it's a breaking 262 | // change in macOS High Sierra. When the GOPATH of the buildenv 263 | // was set to some other folder, like in the $HOME dir, it worked 264 | // fine. Only within $TMPDIR it broke. The $TMPDIR folder is inside 265 | // /var, which is symlinked to /private/var, which is mounted 266 | // with noexec. I don't understand why, but evidently that 267 | // makes -ldflags of `go build` not work. Bizarre. 268 | // The solution, I guess, is to just use our own "temp" dir 269 | // outside of /var. Sigh... as long as it still gets cleaned up, 270 | // I guess it doesn't matter too much. 271 | // See: https://github.com/caddyserver/caddy/issues/2036 272 | // and https://twitter.com/mholt6/status/978345803365273600 (thread) 273 | // (using an absolute path prevents problems later when removing this 274 | // folder if the CWD changes) 275 | var err error 276 | parentDir, err = filepath.Abs(".") 277 | if err != nil { 278 | return "", err 279 | } 280 | } 281 | ts := time.Now().Format(yearMonthDayHourMin) 282 | return os.MkdirTemp(parentDir, fmt.Sprintf("buildenv_%s.", ts)) 283 | } 284 | 285 | // versionedModulePath helps enforce Go Module's Semantic Import Versioning (SIV) by 286 | // returning the form of modulePath with the major component of moduleVersion added, 287 | // if > 1. For example, inputs of "foo" and "v1.0.0" will return "foo", but inputs 288 | // of "foo" and "v2.0.0" will return "foo/v2", for use in Go imports and go commands. 289 | // Inputs that conflict, like "foo/v2" and "v3.1.0" are an error. This function 290 | // returns the input if the moduleVersion is not a valid semantic version string. 291 | // If moduleVersion is empty string, the input modulePath is returned without error. 292 | func versionedModulePath(modulePath, moduleVersion string) (string, error) { 293 | if moduleVersion == "" { 294 | return modulePath, nil 295 | } 296 | ver, err := semver.StrictNewVersion(strings.TrimPrefix(moduleVersion, "v")) 297 | if err != nil { 298 | // only return the error if we know they were trying to use a semantic version 299 | // (could have been a commit SHA or something) 300 | if strings.HasPrefix(moduleVersion, "v") { 301 | return "", fmt.Errorf("%s: %v", moduleVersion, err) 302 | } 303 | return modulePath, nil 304 | } 305 | major := ver.Major() 306 | 307 | // see if the module path has a major version at the end (SIV) 308 | matches := moduleVersionRegexp.FindStringSubmatch(modulePath) 309 | if len(matches) == 2 { 310 | modPathVer, err := strconv.Atoi(matches[1]) 311 | if err != nil { 312 | return "", fmt.Errorf("this error should be impossible, but module path %s has bad version: %v", modulePath, err) 313 | } 314 | if modPathVer != int(major) { 315 | return "", fmt.Errorf("versioned module path (%s) and requested module major version (%d) diverge", modulePath, major) 316 | } 317 | } else if major > 1 { 318 | modulePath += fmt.Sprintf("/v%d", major) 319 | } 320 | 321 | return path.Clean(modulePath), nil 322 | } 323 | 324 | var moduleVersionRegexp = regexp.MustCompile(`.+/v(\d+)$`) 325 | 326 | const ( 327 | // yearMonthDayHourMin is the date format 328 | // used for temporary folder paths. 329 | yearMonthDayHourMin = "2006-01-02-1504" 330 | 331 | defaultCaddyModulePath = "github.com/caddyserver/caddy" 332 | ) 333 | -------------------------------------------------------------------------------- /builder_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Matthew Holt 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package xcaddy 16 | 17 | import ( 18 | "fmt" 19 | "reflect" 20 | "testing" 21 | ) 22 | 23 | func TestReplacementPath_Param(t *testing.T) { 24 | tests := []struct { 25 | name string 26 | r ReplacementPath 27 | want string 28 | }{ 29 | { 30 | "Empty", 31 | ReplacementPath(""), 32 | "", 33 | }, 34 | { 35 | "ModulePath", 36 | ReplacementPath("github.com/x/y"), 37 | "github.com/x/y", 38 | }, 39 | { 40 | "ModulePath Version Pinned", 41 | ReplacementPath("github.com/x/y v0.0.0-20200101000000-xxxxxxxxxxxx"), 42 | "github.com/x/y@v0.0.0-20200101000000-xxxxxxxxxxxx", 43 | }, 44 | { 45 | "FilePath", 46 | ReplacementPath("/x/y/z"), 47 | "/x/y/z", 48 | }, 49 | } 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | fmt.Println(tt.r.Param()) 53 | if got := tt.r.Param(); got != tt.want { 54 | t.Errorf("ReplacementPath.Param() = %v, want %v", got, tt.want) 55 | } 56 | }) 57 | } 58 | } 59 | 60 | func TestNewReplace(t *testing.T) { 61 | type args struct { 62 | old string 63 | new string 64 | } 65 | tests := []struct { 66 | name string 67 | args args 68 | want Replace 69 | }{ 70 | { 71 | "Empty", 72 | args{"", ""}, 73 | Replace{"", ""}, 74 | }, 75 | { 76 | "Constructor", 77 | args{"a", "b"}, 78 | Replace{"a", "b"}, 79 | }, 80 | } 81 | for _, tt := range tests { 82 | t.Run(tt.name, func(t *testing.T) { 83 | if got := NewReplace(tt.args.old, tt.args.new); !reflect.DeepEqual(got, tt.want) { 84 | t.Errorf("NewReplace() = %v, want %v", got, tt.want) 85 | } 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /cmd/cobra.go: -------------------------------------------------------------------------------- 1 | package xcaddycmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | 9 | "github.com/caddyserver/xcaddy" 10 | "github.com/caddyserver/xcaddy/internal/utils" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var rootCmd = &cobra.Command{ 15 | Use: "xcaddy ", 16 | Long: "xcaddy is a custom Caddy builder for advanced users and plugin developers.\n" + 17 | "The xcaddy command has two primary uses:\n" + 18 | "- Compile custom caddy binaries\n" + 19 | "- A replacement for `go run` while developing Caddy plugins\n" + 20 | "xcaddy accepts any Caddy command (except help and version) to pass through to the custom-built Caddy, notably `run` and `list-modules`. The command pass-through allows for iterative development process.\n\n" + 21 | "Report bugs on https://github.com/caddyserver/xcaddy\n", 22 | Short: "Caddy module development helper", 23 | SilenceUsage: true, 24 | Version: xcaddyVersion(), 25 | Args: cobra.ArbitraryArgs, 26 | RunE: func(cmd *cobra.Command, args []string) error { 27 | binOutput := getCaddyOutputFile() 28 | 29 | // get current/main module name and the root directory of the main module 30 | // 31 | // make sure the module being developed is replaced 32 | // so that the local copy is used 33 | // 34 | // replace directives only apply to the top-level/main go.mod, 35 | // and since this tool is a carry-through for the user's actual 36 | // go.mod, we need to transfer their replace directives through 37 | // to the one we're making 38 | execCmd := exec.Command(utils.GetGo(), "list", "-mod=readonly", "-m", "-json", "all") 39 | execCmd.Stderr = os.Stderr 40 | out, err := execCmd.Output() 41 | if err != nil { 42 | return fmt.Errorf("exec %v: %v: %s", cmd.Args, err, string(out)) 43 | } 44 | currentModule, moduleDir, replacements, err := parseGoListJson(out) 45 | if err != nil { 46 | return fmt.Errorf("json parse error: %v", err) 47 | } 48 | 49 | // reconcile remaining path segments; for example if a module foo/a 50 | // is rooted at directory path /home/foo/a, but the current directory 51 | // is /home/foo/a/b, then the package to import should be foo/a/b 52 | cwd, err := os.Getwd() 53 | if err != nil { 54 | return fmt.Errorf("unable to determine current directory: %v", err) 55 | } 56 | importPath := normalizeImportPath(currentModule, cwd, moduleDir) 57 | 58 | // build caddy with this module plugged in 59 | builder := xcaddy.Builder{ 60 | Compile: xcaddy.Compile{ 61 | Cgo: os.Getenv("CGO_ENABLED") == "1", 62 | }, 63 | CaddyVersion: caddyVersion, 64 | Plugins: []xcaddy.Dependency{ 65 | {PackagePath: importPath}, 66 | }, 67 | Replacements: replacements, 68 | RaceDetector: raceDetector, 69 | SkipBuild: skipBuild, 70 | SkipCleanup: skipCleanup, 71 | Debug: buildDebugOutput, 72 | } 73 | err = builder.Build(cmd.Context(), binOutput) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | // if requested, run setcap to allow binding to low ports 79 | err = setcapIfRequested(binOutput) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | log.Printf("[INFO] Running %v\n\n", append([]string{binOutput}, args...)) 85 | 86 | execCmd = exec.Command(binOutput, args...) 87 | execCmd.Stdin = os.Stdin 88 | execCmd.Stdout = os.Stdout 89 | execCmd.Stderr = os.Stderr 90 | err = execCmd.Start() 91 | if err != nil { 92 | return err 93 | } 94 | defer func() { 95 | if skipCleanup { 96 | log.Printf("[INFO] Skipping cleanup as requested; leaving artifact: %s", binOutput) 97 | return 98 | } 99 | err = os.Remove(binOutput) 100 | if err != nil && !os.IsNotExist(err) { 101 | log.Printf("[ERROR] Deleting temporary binary %s: %v", binOutput, err) 102 | } 103 | }() 104 | 105 | return execCmd.Wait() 106 | }, 107 | } 108 | 109 | const fullDocsFooter = `Full documentation is available at: 110 | https://github.com/caddyserver/xcaddy` 111 | 112 | func init() { 113 | rootCmd.SetVersionTemplate("{{.Version}}\n") 114 | rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n") 115 | rootCmd.AddCommand(buildCommand) 116 | rootCmd.AddCommand(versionCommand) 117 | } 118 | -------------------------------------------------------------------------------- /cmd/commands.go: -------------------------------------------------------------------------------- 1 | package xcaddycmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "runtime" 10 | "strings" 11 | 12 | "github.com/caddyserver/xcaddy" 13 | "github.com/caddyserver/xcaddy/internal/utils" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | func init() { 18 | buildCommand.Flags().StringArray("with", []string{}, "caddy modules package path to include in the build") 19 | buildCommand.Flags().String("output", "", "change the output file name") 20 | buildCommand.Flags().StringArray("replace", []string{}, "like --with but for Go modules") 21 | buildCommand.Flags().StringArray("embed", []string{}, "embeds directories into the built Caddy executable to use with the `embedded` file-system") 22 | } 23 | 24 | var versionCommand = &cobra.Command{ 25 | Use: "version", 26 | Short: "Prints xcaddy version", 27 | RunE: func(cm *cobra.Command, args []string) error { 28 | fmt.Println(xcaddyVersion()) 29 | return nil 30 | }, 31 | } 32 | 33 | var buildCommand = &cobra.Command{ 34 | Use: `build [] 35 | [--output ] 36 | [--with ...] 37 | [--replace ...] 38 | [--embed <[alias]:path/to/dir>...]`, 39 | Long: ` 40 | is the core Caddy version to build; defaults to CADDY_VERSION env variable or latest. 41 | This can be the keyword latest, which will use the latest stable tag, or any git ref such as: 42 | 43 | A tag like v2.0.1 44 | A branch like master 45 | A commit like a58f240d3ecbb59285303746406cab50217f8d24 46 | 47 | Flags: 48 | --output changes the output file. 49 | 50 | --with can be used multiple times to add plugins by specifying the Go module name and optionally its version, similar to go get. Module name is required, but specific version and/or local replacement are optional. 51 | 52 | --replace is like --with, but does not add a blank import to the code; it only writes a replace directive to go.mod, which is useful when developing on Caddy's dependencies (ones that are not Caddy modules). Try this if you got an error when using --with, like cannot find module providing package. 53 | 54 | --embed can be used to embed the contents of a directory into the Caddy executable. --embed can be passed multiple times with separate source directories. The source directory can be prefixed with a custom alias and a colon : to write the embedded files into an aliased subdirectory, which is useful when combined with the root directive and sub-directive. 55 | `, 56 | Short: "Compile custom caddy binaries", 57 | Args: cobra.MaximumNArgs(1), 58 | RunE: func(cmd *cobra.Command, args []string) error { 59 | var output string 60 | var plugins []xcaddy.Dependency 61 | var replacements []xcaddy.Replace 62 | var embedDir []string 63 | var argCaddyVersion string 64 | if len(args) > 0 { 65 | argCaddyVersion = args[0] 66 | } 67 | withArgs, err := cmd.Flags().GetStringArray("with") 68 | if err != nil { 69 | return fmt.Errorf("unable to parse --with arguments: %s", err.Error()) 70 | } 71 | 72 | replaceArgs, err := cmd.Flags().GetStringArray("replace") 73 | if err != nil { 74 | return fmt.Errorf("unable to parse --replace arguments: %s", err.Error()) 75 | } 76 | for _, withArg := range withArgs { 77 | mod, ver, repl, err := splitWith(withArg) 78 | if err != nil { 79 | return err 80 | } 81 | mod = strings.TrimSuffix(mod, "/") // easy to accidentally leave a trailing slash if pasting from a URL, but is invalid for Go modules 82 | plugins = append(plugins, xcaddy.Dependency{ 83 | PackagePath: mod, 84 | Version: ver, 85 | }) 86 | handleReplace(withArg, mod, ver, repl, &replacements) 87 | } 88 | 89 | for _, withArg := range replaceArgs { 90 | mod, ver, repl, err := splitWith(withArg) 91 | if err != nil { 92 | return err 93 | } 94 | handleReplace(withArg, mod, ver, repl, &replacements) 95 | } 96 | 97 | output, err = cmd.Flags().GetString("output") 98 | if err != nil { 99 | return fmt.Errorf("unable to parse --output arguments: %s", err.Error()) 100 | } 101 | 102 | embedDir, err = cmd.Flags().GetStringArray("embed") 103 | if err != nil { 104 | return fmt.Errorf("unable to parse --embed arguments: %s", err.Error()) 105 | } 106 | // prefer caddy version from command line argument over env var 107 | if argCaddyVersion != "" { 108 | caddyVersion = argCaddyVersion 109 | } 110 | 111 | // ensure an output file is always specified 112 | if output == "" { 113 | output = getCaddyOutputFile() 114 | } 115 | 116 | // perform the build 117 | builder := xcaddy.Builder{ 118 | Compile: xcaddy.Compile{ 119 | Cgo: os.Getenv("CGO_ENABLED") == "1", 120 | }, 121 | CaddyVersion: caddyVersion, 122 | Plugins: plugins, 123 | Replacements: replacements, 124 | RaceDetector: raceDetector, 125 | SkipBuild: skipBuild, 126 | SkipCleanup: skipCleanup, 127 | Debug: buildDebugOutput, 128 | BuildFlags: buildFlags, 129 | ModFlags: modFlags, 130 | } 131 | for _, md := range embedDir { 132 | if before, after, found := strings.Cut(md, ":"); found { 133 | builder.EmbedDirs = append(builder.EmbedDirs, struct { 134 | Dir string `json:"dir,omitempty"` 135 | Name string `json:"name,omitempty"` 136 | }{ 137 | after, before, 138 | }) 139 | } else { 140 | builder.EmbedDirs = append(builder.EmbedDirs, struct { 141 | Dir string `json:"dir,omitempty"` 142 | Name string `json:"name,omitempty"` 143 | }{ 144 | before, "", 145 | }) 146 | } 147 | } 148 | err = builder.Build(cmd.Root().Context(), output) 149 | if err != nil { 150 | log.Fatalf("[FATAL] %v", err) 151 | } 152 | 153 | // done if we're skipping the build 154 | if builder.SkipBuild { 155 | return nil 156 | } 157 | 158 | // if requested, run setcap to allow binding to low ports 159 | err = setcapIfRequested(output) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | // prove the build is working by printing the version 165 | if runtime.GOOS == utils.GetGOOS() && runtime.GOARCH == utils.GetGOARCH() { 166 | if !filepath.IsAbs(output) { 167 | output = "." + string(filepath.Separator) + output 168 | } 169 | fmt.Println() 170 | fmt.Printf("%s version\n", output) 171 | cmd := exec.Command(output, "version") 172 | cmd.Stdout = os.Stdout 173 | cmd.Stderr = os.Stderr 174 | err = cmd.Run() 175 | if err != nil { 176 | log.Fatalf("[FATAL] %v", err) 177 | } 178 | } 179 | 180 | return nil 181 | }, 182 | } 183 | 184 | func handleReplace(orig, mod, ver, repl string, replacements *[]xcaddy.Replace) { 185 | if repl != "" { 186 | // adjust relative replacements in current working directory since our temporary module is in a different directory 187 | if strings.HasPrefix(repl, ".") { 188 | var err error 189 | repl, err = filepath.Abs(repl) 190 | if err != nil { 191 | log.Fatalf("[FATAL] %v", err) 192 | } 193 | log.Printf("[INFO] Resolved relative replacement %s to %s", orig, repl) 194 | } 195 | *replacements = append(*replacements, xcaddy.NewReplace(xcaddy.Dependency{PackagePath: mod, Version: ver}.String(), repl)) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Matthew Holt 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package xcaddycmd 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "io" 23 | "log" 24 | "os" 25 | "os/exec" 26 | "os/signal" 27 | "path" 28 | "path/filepath" 29 | "runtime/debug" 30 | "strings" 31 | 32 | "github.com/caddyserver/xcaddy" 33 | "github.com/caddyserver/xcaddy/internal/utils" 34 | ) 35 | 36 | var ( 37 | caddyVersion = os.Getenv("CADDY_VERSION") 38 | raceDetector = os.Getenv("XCADDY_RACE_DETECTOR") == "1" 39 | skipBuild = os.Getenv("XCADDY_SKIP_BUILD") == "1" 40 | skipCleanup = os.Getenv("XCADDY_SKIP_CLEANUP") == "1" || skipBuild 41 | buildDebugOutput = os.Getenv("XCADDY_DEBUG") == "1" 42 | buildFlags = os.Getenv("XCADDY_GO_BUILD_FLAGS") 43 | modFlags = os.Getenv("XCADDY_GO_MOD_FLAGS") 44 | ) 45 | 46 | func Main() { 47 | ctx, cancel := context.WithCancel(context.Background()) 48 | defer cancel() 49 | go trapSignals(ctx, cancel) 50 | 51 | if err := rootCmd.Execute(); err != nil { 52 | fmt.Println(err) 53 | os.Exit(1) 54 | } 55 | } 56 | 57 | func getCaddyOutputFile() string { 58 | f := "." + string(filepath.Separator) + "caddy" 59 | // compiling for Windows or compiling on windows without setting GOOS, use .exe extension 60 | if utils.GetGOOS() == "windows" { 61 | f += ".exe" 62 | } 63 | return f 64 | } 65 | 66 | func setcapIfRequested(output string) error { 67 | if os.Getenv("XCADDY_SETCAP") != "1" { 68 | return nil 69 | } 70 | 71 | args := []string{"setcap", "cap_net_bind_service=+ep", output} 72 | 73 | // check if sudo isn't available, or we were instructed not to use it 74 | _, sudoNotFound := exec.LookPath("sudo") 75 | skipSudo := sudoNotFound != nil || os.Getenv("XCADDY_SUDO") == "0" 76 | 77 | var cmd *exec.Cmd 78 | if skipSudo { 79 | cmd = exec.Command(args[0], args[1:]...) 80 | } else { 81 | cmd = exec.Command("sudo", args...) 82 | } 83 | cmd.Stdout = os.Stdout 84 | cmd.Stderr = os.Stderr 85 | 86 | log.Printf("[INFO] Setting capabilities (requires admin privileges): %v", cmd.Args) 87 | if err := cmd.Run(); err != nil { 88 | return fmt.Errorf("failed to setcap on the binary: %v", err) 89 | } 90 | 91 | return nil 92 | } 93 | 94 | type module struct { 95 | Path string // module path 96 | Version string // module version 97 | Replace *module // replaced by this module 98 | Main bool // is this the main module? 99 | Dir string // directory holding files for this module, if any 100 | } 101 | 102 | func parseGoListJson(out []byte) (currentModule, moduleDir string, replacements []xcaddy.Replace, err error) { 103 | var unjoinedReplaces []int 104 | 105 | decoder := json.NewDecoder(bytes.NewReader(out)) 106 | for { 107 | var mod module 108 | if err = decoder.Decode(&mod); err == io.EOF { 109 | err = nil 110 | break 111 | } else if err != nil { 112 | return 113 | } 114 | 115 | if mod.Main { 116 | // Current module is main module, retrieve the main module name and 117 | // root directory path of the main module 118 | currentModule = mod.Path 119 | moduleDir = mod.Dir 120 | replacements = append(replacements, xcaddy.NewReplace(currentModule, moduleDir)) 121 | continue 122 | } 123 | 124 | // Skip if current module is not replacement 125 | if mod.Replace == nil { 126 | continue 127 | } 128 | 129 | // 1. Target is module, version is required in this case 130 | // 2A. Target is absolute path 131 | // 2B. Target is relative path, proper handling is required in this case 132 | dstPath := mod.Replace.Path 133 | dstVersion := mod.Replace.Version 134 | var dst string 135 | if dstVersion != "" { 136 | dst = dstPath + "@" + dstVersion 137 | } else if filepath.IsAbs(dstPath) { 138 | dst = dstPath 139 | } else { 140 | if moduleDir != "" { 141 | dst = filepath.Join(moduleDir, dstPath) 142 | log.Printf("[INFO] Resolved relative replacement %s to %s", dstPath, dst) 143 | } else { 144 | // moduleDir is not parsed yet, defer to later 145 | dst = dstPath 146 | unjoinedReplaces = append(unjoinedReplaces, len(replacements)) 147 | } 148 | } 149 | 150 | replacements = append(replacements, xcaddy.NewReplace(mod.Path, dst)) 151 | } 152 | for _, idx := range unjoinedReplaces { 153 | unresolved := string(replacements[idx].New) 154 | resolved := filepath.Join(moduleDir, unresolved) 155 | log.Printf("[INFO] Resolved previously-unjoined relative replacement %s to %s", unresolved, resolved) 156 | replacements[idx].New = xcaddy.ReplacementPath(resolved) 157 | } 158 | return 159 | } 160 | 161 | func normalizeImportPath(currentModule, cwd, moduleDir string) string { 162 | return path.Join(currentModule, filepath.ToSlash(strings.TrimPrefix(cwd, moduleDir))) 163 | } 164 | 165 | func trapSignals(ctx context.Context, cancel context.CancelFunc) { 166 | sig := make(chan os.Signal, 1) 167 | signal.Notify(sig, os.Interrupt) 168 | 169 | select { 170 | case <-sig: 171 | log.Printf("[INFO] SIGINT: Shutting down") 172 | cancel() 173 | case <-ctx.Done(): 174 | return 175 | } 176 | } 177 | 178 | func splitWith(arg string) (module, version, replace string, err error) { 179 | const versionSplit, replaceSplit = "@", "=" 180 | 181 | parts := strings.SplitN(arg, replaceSplit, 2) 182 | if len(parts) > 1 { 183 | replace = parts[1] 184 | } 185 | module = parts[0] 186 | 187 | // accommodate module paths that have @ in them, but we can only tolerate that if there's also 188 | // a version, otherwise we don't know if it's a version separator or part of the file path (see #109) 189 | lastVersionSplit := strings.LastIndex(module, versionSplit) 190 | if lastVersionSplit < 0 { 191 | if replaceIdx := strings.Index(module, replaceSplit); replaceIdx >= 0 { 192 | module, replace = module[:replaceIdx], module[replaceIdx+1:] 193 | } 194 | } else { 195 | module, version = module[:lastVersionSplit], module[lastVersionSplit+1:] 196 | if replaceIdx := strings.Index(version, replaceSplit); replaceIdx >= 0 { 197 | version, replace = module[:replaceIdx], module[replaceIdx+1:] 198 | } 199 | } 200 | 201 | if module == "" { 202 | err = fmt.Errorf("module name is required") 203 | } 204 | 205 | return 206 | } 207 | 208 | // xcaddyVersion returns a detailed version string, if available. 209 | func xcaddyVersion() string { 210 | mod := goModule() 211 | ver := mod.Version 212 | if mod.Sum != "" { 213 | ver += " " + mod.Sum 214 | } 215 | if mod.Replace != nil { 216 | ver += " => " + mod.Replace.Path 217 | if mod.Replace.Version != "" { 218 | ver += "@" + mod.Replace.Version 219 | } 220 | if mod.Replace.Sum != "" { 221 | ver += " " + mod.Replace.Sum 222 | } 223 | } 224 | return ver 225 | } 226 | 227 | func goModule() *debug.Module { 228 | mod := &debug.Module{} 229 | mod.Version = "unknown" 230 | bi, ok := debug.ReadBuildInfo() 231 | if ok { 232 | mod.Path = bi.Main.Path 233 | // The recommended way to build xcaddy involves 234 | // creating a separate main module, which 235 | // TODO: track related Go issue: https://github.com/golang/go/issues/29228 236 | // once that issue is fixed, we should just be able to use bi.Main... hopefully. 237 | for _, dep := range bi.Deps { 238 | if dep.Path == "github.com/caddyserver/xcaddy" { 239 | return dep 240 | } 241 | } 242 | return &bi.Main 243 | } 244 | return mod 245 | } 246 | -------------------------------------------------------------------------------- /cmd/main_test.go: -------------------------------------------------------------------------------- 1 | package xcaddycmd 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | ) 7 | 8 | func TestSplitWith(t *testing.T) { 9 | for i, tc := range []struct { 10 | input string 11 | expectModule string 12 | expectVersion string 13 | expectReplace string 14 | expectErr bool 15 | }{ 16 | { 17 | input: "module", 18 | expectModule: "module", 19 | }, 20 | { 21 | input: "module@version", 22 | expectModule: "module", 23 | expectVersion: "version", 24 | }, 25 | { 26 | input: "module@version=replace", 27 | expectModule: "module", 28 | expectVersion: "version", 29 | expectReplace: "replace", 30 | }, 31 | { 32 | input: "module=replace", 33 | expectModule: "module", 34 | expectReplace: "replace", 35 | }, 36 | { 37 | input: "module=replace@version", 38 | expectModule: "module", 39 | expectReplace: "replace@version", 40 | }, 41 | { 42 | input: "module@version=replace@version", 43 | expectModule: "module", 44 | expectVersion: "version", 45 | expectReplace: "replace@version", 46 | }, 47 | { 48 | input: "=replace", 49 | expectErr: true, 50 | }, 51 | { 52 | input: "@version", 53 | expectErr: true, 54 | }, 55 | { 56 | input: "@version=replace", 57 | expectErr: true, 58 | }, 59 | { 60 | input: "", 61 | expectErr: true, 62 | }, 63 | { 64 | // issue #109 65 | input: "/home/devin/projects/@relay/caddy-bin@version", 66 | expectModule: "/home/devin/projects/@relay/caddy-bin", 67 | expectVersion: "version", 68 | }, 69 | } { 70 | actualModule, actualVersion, actualReplace, actualErr := splitWith(tc.input) 71 | if actualModule != tc.expectModule { 72 | t.Errorf("Test %d: Expected module '%s' but got '%s' (input=%s)", 73 | i, tc.expectModule, actualModule, tc.input) 74 | } 75 | if tc.expectErr { 76 | if actualErr == nil { 77 | t.Errorf("Test %d: Expected error but did not get one (input='%s')", i, tc.input) 78 | } 79 | continue 80 | } 81 | if !tc.expectErr && actualErr != nil { 82 | t.Errorf("Test %d: Expected no error but got: %s (input='%s')", i, actualErr, tc.input) 83 | } 84 | if actualVersion != tc.expectVersion { 85 | t.Errorf("Test %d: Expected version '%s' but got '%s' (input='%s')", 86 | i, tc.expectVersion, actualVersion, tc.input) 87 | } 88 | if actualReplace != tc.expectReplace { 89 | t.Errorf("Test %d: Expected module '%s' but got '%s' (input='%s')", 90 | i, tc.expectReplace, actualReplace, tc.input) 91 | } 92 | } 93 | } 94 | 95 | func TestNormalizeImportPath(t *testing.T) { 96 | type ( 97 | args struct { 98 | currentModule string 99 | cwd string 100 | moduleDir string 101 | } 102 | testCaseType []struct { 103 | name string 104 | args args 105 | want string 106 | } 107 | ) 108 | 109 | tests := testCaseType{ 110 | {"linux-path", args{ 111 | currentModule: "github.com/caddyserver/xcaddy", 112 | cwd: "/xcaddy", 113 | moduleDir: "/xcaddy", 114 | }, "github.com/caddyserver/xcaddy"}, 115 | {"linux-subpath", args{ 116 | currentModule: "github.com/caddyserver/xcaddy", 117 | cwd: "/xcaddy/subdir", 118 | moduleDir: "/xcaddy", 119 | }, "github.com/caddyserver/xcaddy/subdir"}, 120 | } 121 | windowsTests := testCaseType{ 122 | {"windows-path", args{ 123 | currentModule: "github.com/caddyserver/xcaddy", 124 | cwd: "c:\\xcaddy", 125 | moduleDir: "c:\\xcaddy", 126 | }, "github.com/caddyserver/xcaddy"}, 127 | {"windows-subpath", args{ 128 | currentModule: "github.com/caddyserver/xcaddy", 129 | cwd: "c:\\xcaddy\\subdir", 130 | moduleDir: "c:\\xcaddy", 131 | }, "github.com/caddyserver/xcaddy/subdir"}, 132 | } 133 | if runtime.GOOS == "windows" { 134 | tests = append(tests, windowsTests...) 135 | } 136 | for _, tt := range tests { 137 | t.Run(tt.name, func(t *testing.T) { 138 | if got := normalizeImportPath(tt.args.currentModule, tt.args.cwd, tt.args.moduleDir); got != tt.want { 139 | t.Errorf("normalizeImportPath() = %v, want %v", got, tt.want) 140 | } 141 | }) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /cmd/main_unix_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package xcaddycmd 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/caddyserver/xcaddy" 11 | ) 12 | 13 | func TestParseGoListJson(t *testing.T) { 14 | currentModule, moduleDir, replacements, err := parseGoListJson([]byte(` 15 | { 16 | "Path": "replacetest1", 17 | "Version": "v1.2.3", 18 | "Replace": { 19 | "Path": "golang.org/x/example", 20 | "Version": "v0.0.0-20210811190340-787a929d5a0d", 21 | "Time": "2021-08-11T19:03:40Z", 22 | "GoMod": "/home/simnalamburt/.go/pkg/mod/cache/download/golang.org/x/example/@v/v0.0.0-20210811190340-787a929d5a0d.mod", 23 | "GoVersion": "1.15" 24 | }, 25 | "GoMod": "/home/simnalamburt/.go/pkg/mod/cache/download/golang.org/x/example/@v/v0.0.0-20210811190340-787a929d5a0d.mod", 26 | "GoVersion": "1.15" 27 | } 28 | { 29 | "Path": "replacetest2", 30 | "Version": "v0.0.1", 31 | "Replace": { 32 | "Path": "golang.org/x/example", 33 | "Version": "v0.0.0-20210407023211-09c3a5e06b5d", 34 | "Time": "2021-04-07T02:32:11Z", 35 | "GoMod": "/home/simnalamburt/.go/pkg/mod/cache/download/golang.org/x/example/@v/v0.0.0-20210407023211-09c3a5e06b5d.mod", 36 | "GoVersion": "1.15" 37 | }, 38 | "GoMod": "/home/simnalamburt/.go/pkg/mod/cache/download/golang.org/x/example/@v/v0.0.0-20210407023211-09c3a5e06b5d.mod", 39 | "GoVersion": "1.15" 40 | } 41 | { 42 | "Path": "replacetest3", 43 | "Version": "v1.2.3", 44 | "Replace": { 45 | "Path": "./fork1", 46 | "Dir": "/home/work/module/fork1", 47 | "GoMod": "/home/work/module/fork1/go.mod", 48 | "GoVersion": "1.17" 49 | }, 50 | "Dir": "/home/work/module/fork1", 51 | "GoMod": "/home/work/module/fork1/go.mod", 52 | "GoVersion": "1.17" 53 | } 54 | { 55 | "Path": "github.com/simnalamburt/module", 56 | "Main": true, 57 | "Dir": "/home/work/module", 58 | "GoMod": "/home/work/module/go.mod", 59 | "GoVersion": "1.17" 60 | } 61 | { 62 | "Path": "replacetest4", 63 | "Version": "v0.0.1", 64 | "Replace": { 65 | "Path": "/srv/fork2", 66 | "Dir": "/home/work/module/fork2", 67 | "GoMod": "/home/work/module/fork2/go.mod", 68 | "GoVersion": "1.17" 69 | }, 70 | "Dir": "/home/work/module/fork2", 71 | "GoMod": "/home/work/module/fork2/go.mod", 72 | "GoVersion": "1.17" 73 | } 74 | { 75 | "Path": "replacetest5", 76 | "Version": "v1.2.3", 77 | "Replace": { 78 | "Path": "./fork3", 79 | "Dir": "/home/work/module/fork3", 80 | "GoMod": "/home/work/module/fork3/go.mod", 81 | "GoVersion": "1.17" 82 | }, 83 | "Dir": "/home/work/module/fork3", 84 | "GoMod": "/home/work/module/fork3/go.mod", 85 | "GoVersion": "1.17" 86 | } 87 | `)) 88 | if err != nil { 89 | t.Errorf("Error occured during JSON parsing") 90 | } 91 | if currentModule != "github.com/simnalamburt/module" { 92 | t.Errorf("Unexpected module name") 93 | } 94 | if moduleDir != "/home/work/module" { 95 | t.Errorf("Unexpected module path") 96 | } 97 | expected := []xcaddy.Replace{ 98 | xcaddy.NewReplace("replacetest1", "golang.org/x/example@v0.0.0-20210811190340-787a929d5a0d"), 99 | xcaddy.NewReplace("replacetest2", "golang.org/x/example@v0.0.0-20210407023211-09c3a5e06b5d"), 100 | xcaddy.NewReplace("replacetest3", "/home/work/module/fork1"), 101 | xcaddy.NewReplace("github.com/simnalamburt/module", "/home/work/module"), 102 | xcaddy.NewReplace("replacetest4", "/srv/fork2"), 103 | xcaddy.NewReplace("replacetest5", "/home/work/module/fork3"), 104 | } 105 | if !reflect.DeepEqual(replacements, expected) { 106 | t.Errorf("Expected replacements '%v' but got '%v'", expected, replacements) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /cmd/main_windows_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package xcaddycmd 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/caddyserver/xcaddy" 11 | ) 12 | 13 | func TestParseGoListJson(t *testing.T) { 14 | currentModule, moduleDir, replacements, err := parseGoListJson([]byte(` 15 | { 16 | "Path": "replacetest1", 17 | "Version": "v1.2.3", 18 | "Replace": { 19 | "Path": "golang.org/x/example", 20 | "Version": "v0.0.0-20210811190340-787a929d5a0d", 21 | "Time": "2021-08-11T19:03:40Z", 22 | "Dir": "C:\\Users\\simna\\go\\pkg\\mod\\golang.org\\x\\example@v0.0.0-20210811190340-787a929d5a0d", 23 | "GoMod": "C:\\Users\\simna\\go\\pkg\\mod\\cache\\download\\golang.org\\x\\example\\@v\\v0.0.0-20210811190340-787a929d5a0d.mod", 24 | "GoVersion": "1.15" 25 | }, 26 | "Dir": "C:\\Users\\simna\\go\\pkg\\mod\\golang.org\\x\\example@v0.0.0-20210811190340-787a929d5a0d", 27 | "GoMod": "C:\\Users\\simna\\go\\pkg\\mod\\cache\\download\\golang.org\\x\\example\\@v\\v0.0.0-20210811190340-787a929d5a0d.mod", 28 | "GoVersion": "1.15" 29 | } 30 | { 31 | "Path": "replacetest2", 32 | "Version": "v0.0.1", 33 | "Replace": { 34 | "Path": "golang.org/x/example", 35 | "Version": "v0.0.0-20210407023211-09c3a5e06b5d", 36 | "Time": "2021-04-07T02:32:11Z", 37 | "Dir": "C:\\Users\\simna\\go\\pkg\\mod\\golang.org\\x\\example@v0.0.0-20210407023211-09c3a5e06b5d", 38 | "GoMod": "C:\\Users\\simna\\go\\pkg\\mod\\cache\\download\\golang.org\\x\\example\\@v\\v0.0.0-20210407023211-09c3a5e06b5d.mod", 39 | "GoVersion": "1.15" 40 | }, 41 | "Dir": "C:\\Users\\simna\\go\\pkg\\mod\\golang.org\\x\\example@v0.0.0-20210407023211-09c3a5e06b5d", 42 | "GoMod": "C:\\Users\\simna\\go\\pkg\\mod\\cache\\download\\golang.org\\x\\example\\@v\\v0.0.0-20210407023211-09c3a5e06b5d.mod", 43 | "GoVersion": "1.15" 44 | } 45 | { 46 | "Path": "replacetest3", 47 | "Version": "v1.2.3", 48 | "Replace": { 49 | "Path": "./fork1", 50 | "Dir": "C:\\Users\\work\\module\\fork1", 51 | "GoMod": "C:\\Users\\work\\module\\fork1\\go.mod", 52 | "GoVersion": "1.17" 53 | }, 54 | "Dir": "C:\\Users\\work\\module\\fork1", 55 | "GoMod": "C:\\Users\\work\\module\\fork1\\go.mod", 56 | "GoVersion": "1.17" 57 | } 58 | { 59 | "Path": "github.com/simnalamburt/module", 60 | "Main": true, 61 | "Dir": "C:\\Users\\work\\module", 62 | "GoMod": "C:\\Users\\work\\module\\go.mod", 63 | "GoVersion": "1.17" 64 | } 65 | { 66 | "Path": "replacetest4", 67 | "Version": "v0.0.1", 68 | "Replace": { 69 | "Path": "C:\\go\\fork2", 70 | "Dir": "C:\\Users\\work\\module\\fork2", 71 | "GoMod": "C:\\Users\\work\\module\\fork2\\go.mod", 72 | "GoVersion": "1.17" 73 | }, 74 | "Dir": "C:\\Users\\work\\module\\fork2", 75 | "GoMod": "C:\\Users\\work\\module\\fork2\\go.mod", 76 | "GoVersion": "1.17" 77 | } 78 | { 79 | "Path": "replacetest5", 80 | "Version": "v1.2.3", 81 | "Replace": { 82 | "Path": "./fork3", 83 | "Dir": "C:\\Users\\work\\module\\fork3", 84 | "GoMod": "C:\\Users\\work\\module\\fork1\\go.mod", 85 | "GoVersion": "1.17" 86 | }, 87 | "Dir": "C:\\Users\\work\\module\\fork3", 88 | "GoMod": "C:\\Users\\work\\module\\fork3\\go.mod", 89 | "GoVersion": "1.17" 90 | } 91 | `)) 92 | if err != nil { 93 | t.Errorf("Error occured during JSON parsing") 94 | } 95 | if currentModule != "github.com/simnalamburt/module" { 96 | t.Errorf("Unexpected module name") 97 | } 98 | if moduleDir != "C:\\Users\\work\\module" { 99 | t.Errorf("Unexpected module path") 100 | } 101 | expected := []xcaddy.Replace{ 102 | xcaddy.NewReplace("replacetest1", "golang.org/x/example@v0.0.0-20210811190340-787a929d5a0d"), 103 | xcaddy.NewReplace("replacetest2", "golang.org/x/example@v0.0.0-20210407023211-09c3a5e06b5d"), 104 | xcaddy.NewReplace("replacetest3", "C:\\Users\\work\\module\\fork1"), 105 | xcaddy.NewReplace("github.com/simnalamburt/module", "C:\\Users\\work\\module"), 106 | xcaddy.NewReplace("replacetest4", "C:\\go\\fork2"), 107 | xcaddy.NewReplace("replacetest5", "C:\\Users\\work\\module\\fork3"), 108 | } 109 | if !reflect.DeepEqual(replacements, expected) { 110 | t.Errorf("Expected replacements '%v' but got '%v'", expected, replacements) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /cmd/xcaddy/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Matthew Holt 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | xcaddycmd "github.com/caddyserver/xcaddy/cmd" 19 | ) 20 | 21 | func main() { 22 | xcaddycmd.Main() 23 | } 24 | -------------------------------------------------------------------------------- /environment.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Matthew Holt 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package xcaddy 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "fmt" 21 | "log" 22 | "os" 23 | "os/exec" 24 | "path/filepath" 25 | "strings" 26 | "text/template" 27 | "time" 28 | 29 | "github.com/caddyserver/xcaddy/internal/utils" 30 | "github.com/google/shlex" 31 | ) 32 | 33 | func (b Builder) newEnvironment(ctx context.Context) (*environment, error) { 34 | // assume Caddy v2 if no semantic version is provided 35 | caddyModulePath := defaultCaddyModulePath 36 | if !strings.HasPrefix(b.CaddyVersion, "v") || !strings.Contains(b.CaddyVersion, ".") { 37 | caddyModulePath += "/v2" 38 | } 39 | caddyModulePath, err := versionedModulePath(caddyModulePath, b.CaddyVersion) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | // clean up any SIV-incompatible module paths real quick 45 | for i, p := range b.Plugins { 46 | b.Plugins[i].PackagePath, err = versionedModulePath(p.PackagePath, p.Version) 47 | if err != nil { 48 | return nil, err 49 | } 50 | } 51 | 52 | // create the context for the main module template 53 | tplCtx := goModTemplateContext{ 54 | CaddyModule: caddyModulePath, 55 | } 56 | for _, p := range b.Plugins { 57 | tplCtx.Plugins = append(tplCtx.Plugins, p.PackagePath) 58 | } 59 | 60 | // evaluate the template for the main module 61 | var buf bytes.Buffer 62 | tpl, err := template.New("main").Parse(mainModuleTemplate) 63 | if err != nil { 64 | return nil, err 65 | } 66 | err = tpl.Execute(&buf, tplCtx) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | // create the folder in which the build environment will operate 72 | tempFolder, err := newTempFolder() 73 | if err != nil { 74 | return nil, err 75 | } 76 | defer func() { 77 | if err != nil { 78 | err2 := os.RemoveAll(tempFolder) 79 | if err2 != nil { 80 | err = fmt.Errorf("%w; additionally, cleaning up folder: %v", err, err2) 81 | } 82 | } 83 | }() 84 | log.Printf("[INFO] Temporary folder: %s", tempFolder) 85 | 86 | // write the main module file to temporary folder 87 | mainPath := filepath.Join(tempFolder, "main.go") 88 | log.Printf("[INFO] Writing main module: %s\n%s", mainPath, buf.Bytes()) 89 | err = os.WriteFile(mainPath, buf.Bytes(), 0o644) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | if len(b.EmbedDirs) > 0 { 95 | for _, d := range b.EmbedDirs { 96 | err = copy(d.Dir, filepath.Join(tempFolder, "files", d.Name)) 97 | if err != nil { 98 | return nil, err 99 | } 100 | _, err = os.Stat(d.Dir) 101 | if err != nil { 102 | return nil, fmt.Errorf("embed directory does not exist: %s", d.Dir) 103 | } 104 | log.Printf("[INFO] Embedding directory: %s", d.Dir) 105 | buf.Reset() 106 | tpl, err = template.New("embed").Parse(embeddedModuleTemplate) 107 | if err != nil { 108 | return nil, err 109 | } 110 | err = tpl.Execute(&buf, tplCtx) 111 | if err != nil { 112 | return nil, err 113 | } 114 | log.Printf("[INFO] Writing 'embedded' module: %s\n%s", mainPath, buf.Bytes()) 115 | emedPath := filepath.Join(tempFolder, "embed.go") 116 | err = os.WriteFile(emedPath, buf.Bytes(), 0o644) 117 | if err != nil { 118 | return nil, err 119 | } 120 | } 121 | } 122 | 123 | env := &environment{ 124 | caddyVersion: b.CaddyVersion, 125 | plugins: b.Plugins, 126 | caddyModulePath: caddyModulePath, 127 | tempFolder: tempFolder, 128 | timeoutGoGet: b.TimeoutGet, 129 | skipCleanup: b.SkipCleanup, 130 | buildFlags: b.BuildFlags, 131 | modFlags: b.ModFlags, 132 | } 133 | 134 | // initialize the go module 135 | log.Println("[INFO] Initializing Go module") 136 | cmd := env.newGoModCommand(ctx, "init") 137 | cmd.Args = append(cmd.Args, "caddy") 138 | err = env.runCommand(ctx, cmd) 139 | if err != nil { 140 | return nil, err 141 | } 142 | 143 | // specify module replacements before pinning versions 144 | replaced := make(map[string]string) 145 | for _, r := range b.Replacements { 146 | log.Printf("[INFO] Replace %s => %s", r.Old.String(), r.New.String()) 147 | replaced[r.Old.String()] = r.New.String() 148 | } 149 | if len(replaced) > 0 { 150 | cmd := env.newGoModCommand(ctx, "edit") 151 | for o, n := range replaced { 152 | cmd.Args = append(cmd.Args, "-replace", fmt.Sprintf("%s=%s", o, n)) 153 | } 154 | err := env.runCommand(ctx, cmd) 155 | if err != nil { 156 | return nil, err 157 | } 158 | } 159 | 160 | // check for early abort 161 | select { 162 | case <-ctx.Done(): 163 | return nil, ctx.Err() 164 | default: 165 | } 166 | 167 | // The timeout for the `go get` command may be different than `go build`, 168 | // so create a new context with the timeout for `go get` 169 | if env.timeoutGoGet > 0 { 170 | var cancel context.CancelFunc 171 | ctx, cancel = context.WithTimeout(context.Background(), env.timeoutGoGet) 172 | defer cancel() 173 | } 174 | 175 | // pin versions by populating go.mod, first for Caddy itself and then plugins 176 | log.Println("[INFO] Pinning versions") 177 | err = env.execGoGet(ctx, caddyModulePath, env.caddyVersion, "", "") 178 | if err != nil { 179 | return nil, err 180 | } 181 | nextPlugin: 182 | for _, p := range b.Plugins { 183 | // still fetch new modules and check is the latest or tagged version is viable 184 | // regardless if this is in reference to a local module, lexical submodule or minor semantic revision. 185 | // 186 | // see issue caddyserver/xcaddy/issues/221 for more info about line 187 | // with or without `if strings.HasPrefix(p.PackagePath, repl+"/") ... ` 188 | // 189 | // In the case of lexical submodules: 190 | // say you are requiring both submodules libdns *and* libdns-provider 191 | // your new local dns provder module prefixed with `libdns-` will be skipped when running 192 | // xcaddy `build --with libdns-provider ...` 193 | // 194 | // In the case of semantic and/or local submodules: 195 | // say you are requiring both submodules