diff --git a/.github/workflows/release-3.yml b/.github/workflows/release-3.yml new file mode 100644 index 0000000000..859be79ad4 --- /dev/null +++ b/.github/workflows/release-3.yml @@ -0,0 +1,201 @@ +name: Release 3 + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + version: + description: 'Version to build ("1.2.3")' + required: true + + +jobs: + build: + strategy: + matrix: + include: + - { platform: linux, os: ubuntu-24.04, arch: amd64 } + - { platform: linux, os: ubuntu-24.04, arch: arm64 } + - { platform: windows, os: windows-latest, arch: amd64 } + - { platform: darwin, os: ubuntu-24.04, arch: amd64 } + - { platform: darwin, os: ubuntu-24.04, arch: arm64 } + + runs-on: ${{ matrix.os }} + steps: + - name: Check out repo + uses: actions/checkout@v4 + + - name: Setup Mise + uses: jdx/mise-action@v2 + + - name: Install tools + run: mise install + + - name: Setup macOS SDKs for cross-compilation + if: matrix.platform == 'darwin' + run: | + echo "Setting up macOS SDKs for cross-compilation on Ubuntu..." + + # Create resources directory + mkdir -p resources/sdks + + # Install dependencies for SDK extraction + sudo apt-get update + sudo apt-get install -y bzip2 xz-utils + + # Download and extract appropriate SDK based on architecture + if [[ "${{ matrix.arch }}" == "amd64" ]]; then + echo "Downloading macOS 10.15 SDK for AMD64 builds..." + curl -L "https://github.com/alexey-lysiuk/macos-sdk/releases/download/10.15/MacOSX10.15.tar.bz2" | tar -C resources/sdks -xjf - + SDK_FULL_PATH="${GITHUB_WORKSPACE}/resources/sdks/MacOSX10.15.sdk" + echo "Using macOS 10.15 SDK for AMD64 cross-compilation" + else + echo "Downloading macOS 11.3 SDK for ARM64 builds..." + curl -L "https://github.com/alexey-lysiuk/macos-sdk/releases/download/11.3/MacOSX11.3.tar.bz2" | tar -C resources/sdks -xjf - + SDK_FULL_PATH="${GITHUB_WORKSPACE}/resources/sdks/MacOSX11.3.sdk" + echo "Using macOS 11.3 SDK for ARM64 cross-compilation" + fi + + # Set MACOS_SDK_PATH to full absolute path + echo "MACOS_SDK_PATH=${SDK_FULL_PATH}" >> $GITHUB_ENV + echo "SDKROOT=${SDK_FULL_PATH}" >> $GITHUB_ENV + echo "SDK path: ${SDK_FULL_PATH}" + + # Verify SDK installation + ls -la ${SDK_FULL_PATH}/ + + - name: Extract version from tag + shell: bash + id: version + run: | + if [[ "${{ github.ref_type }}" == "tag" ]]; then + VERSION="${{ github.ref_name }}" + VERSION="${VERSION#v}" # Remove 'v' prefix + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Using version from tag: $VERSION" + else + VERSION="${{ github.event.inputs.version }}" + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Using version from input: $VERSION" + fi + + - name: Download Encore-Go + shell: bash + run: | + echo "Downloading latest Encore-Go fork for ${{ matrix.platform }}_${{ matrix.arch }}" + + # Create encore-go directory and download the fork + mkdir -p encore-go + + # Determine the asset name based on platform and architecture + if [ "${{ matrix.platform }}" = "linux" ]; then + if [ "${{ matrix.arch }}" = "amd64" ]; then + ASSET="linux_x86-64.tar.gz" + else + ASSET="linux_arm64.tar.gz" + fi + elif [ "${{ matrix.platform }}" = "windows" ]; then + ASSET="windows_x86-64.tar.gz" + elif [ "${{ matrix.platform }}" = "darwin" ]; then + if [ "${{ matrix.arch }}" = "amd64" ]; then + ASSET="macos_x86-64.tar.gz" + else + ASSET="macos_arm64.tar.gz" + fi + fi + + # Download and extract the latest Go fork + curl -L "https://github.com/TheGB0077/encore-go/releases/latest/download/${ASSET}" | tar -C encore-go -xzf - + + echo "Go fork downloaded to encore-go/" + ls -la encore-go/ + + - name: Build + run: task ${{ matrix.platform }}:build:${{ matrix.arch }} + shell: bash + env: + VERSION: "${{ steps.version.outputs.version }}" + + - name: Create archives + shell: bash + run: | + cd dist/encore-${{ matrix.platform }}-${{ matrix.arch }} + + # Create tar.gz with version in name for all platforms + tar -czf ../../encore-${{ steps.version.outputs.version }}-${{ matrix.platform }}-${{ matrix.arch }}.tar.gz . + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: encore-${{ matrix.platform }}-${{ matrix.arch }} + path: | + encore-${{ steps.version.outputs.version }}-${{ matrix.platform }}-${{ matrix.arch }}.tar.gz + retention-days: 30 + + release: + needs: build + runs-on: ubuntu-latest + if: github.ref_type == 'tag' # Only create releases for tags + steps: + - name: Extract version from tag + id: version + run: | + VERSION="${{ github.ref_name }}" + VERSION="${VERSION#v}" # Remove 'v' prefix + echo "version=$VERSION" >> $GITHUB_OUTPUT + shell: bash + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts/ + pattern: encore-* + merge-multiple: true + + - name: Organize release assets + run: | + mkdir -p release-assets + + # Move archives to release-assets (already correctly named) + mv artifacts/encore-${{ steps.version.outputs.version }}-*.tar.gz release-assets/ 2>/dev/null || true + mv artifacts/encore-${{ steps.version.outputs.version }}-*.zip release-assets/ 2>/dev/null || true + + # Create checksums + cd release-assets + sha256sum * > checksums.txt + cd .. + + echo "Release assets ready:" + ls -la release-assets/ + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: Encore ${{ steps.version.outputs.version }} + body: | + ## Encore ${{ steps.version.outputs.version }} + + ### Installation + ```bash + # Install Encore CLI + curl -L https://github.com/encoredev/encore/releases/download/v${{ steps.version.outputs.version }}/install.sh | bash + ``` + + ### Downloads + - **Linux AMD64**: `encore-${{ steps.version.outputs.version }}-linux-amd64.tar.gz` + - **Linux ARM64**: `encore-${{ steps.version.outputs.version }}-linux-arm64.tar.gz` + - **macOS AMD64**: `encore-${{ steps.version.outputs.version }}-darwin-amd64.tar.gz` + - **macOS ARM64**: `encore-${{ steps.version.outputs.version }}-darwin-arm64.tar.gz` + - **Windows**: `encore-${{ steps.version.outputs.version }}-windows-amd64.tar.gz` + + ### Changelog + TODO: Add changelog entries + files: | + release-assets/* + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000000..826aefdf07 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,98 @@ +version: "3" + +includes: + common: ./build/Taskfile.yml + darwin: ./build/darwin/Taskfile.yml + linux: ./build/linux/Taskfile.yml + windows: ./build/windows/Taskfile.yml + +vars: + APP_NAME: "encore" + BIN_DIR: "dist" + VERSION: '{{.VERSION | default "dev"}}' + ENCORE_GO_VERSION: '{{.ENCORE_GO_VERSION | default "encore-go1.23.0"}}' + ZIG_PATH: + sh: mise which zig + MACOS_SDK_PATH: '{{.MACOS_SDK_PATH | default ""}}' + +tasks: + default: + summary: Shows available tasks + cmds: + - task --list + + setup: + summary: Sets up development environment + cmds: + - task: common:setup + + build: + summary: Builds Encore CLI for current platform + cmds: + - task: "{{OS}}:build" + + build:cli: + summary: Builds only the main CLI for current platform + cmds: + - task: "{{OS}}:build:cli" + + test: + summary: Runs all tests + cmds: + - task: common:test:unit + + test:unit: + summary: Runs unit tests + cmds: + - task: common:test:unit + + clean: + summary: Cleans build artifacts + cmds: + - rm -rf {{.BIN_DIR}} + - rm -rf encore-go + - rm -f *.tar.gz *.zip + + clean:all: + summary: Cleans all build artifacts and caches + deps: + - task: clean + cmds: + - go clean -cache + + version: + summary: Shows current version + cmds: + - 'echo "Encore version: {{.VERSION}}"' + - 'echo "Encore-Go version: {{.ENCORE_GO_VERSION}}"' + + deps: + summary: Downloads all dependencies + cmds: + - task: common:setup + + # Development helpers + dev: + summary: Runs Encore in development mode + deps: + - task: build:cli + cmds: + - ./{{.BIN_DIR}}/encore dev + + run: + summary: Runs built Encore binary + deps: + - task: build:cli + cmds: + - ./{{.BIN_DIR}}/encore {{.CLI_ARGS}} + + # Linting and formatting + lint: + summary: Runs linters + cmds: + - task: common:lint:go + + fmt: + summary: Formats code + cmds: + - task: common:fmt:go diff --git a/build/Taskfile.yml b/build/Taskfile.yml new file mode 100644 index 0000000000..7374ac7641 --- /dev/null +++ b/build/Taskfile.yml @@ -0,0 +1,64 @@ +version: "3" + +tasks: + setup: + summary: Sets up build environment + cmds: + - mise install + - go mod download + - mkdir -p {{.BIN_DIR}} + - mkdir -p runtimes + + go:mod:tidy: + summary: Runs go mod tidy + internal: true + cmds: + - go mod tidy + + generate:bindings: + summary: Generates code bindings + deps: + - task: go:mod:tidy + sources: + - "**/*.go" + - go.mod + - go.sum + cmds: + - go generate ./... + + test:unit: + summary: Runs unit tests + deps: + - task: setup + cmds: + - go test -tags=dev_build ./... + + # Linting tasks + lint:go: + summary: Runs Go linters + cmds: + - golangci-lint run + + # Formatting tasks + fmt:go: + summary: Formats Go code + cmds: + - go fmt ./... + - goimports -w . + + # Build helpers + build:go:binary: + summary: Builds a Go binary with specified parameters + internal: true + vars: + BUILD_CMD: go build + LDFLAGS: '{{.LDFLAGS | default "-s -w"}}' + BUILD_FLAGS: '{{.BUILD_FLAGS | default "-trimpath -tags=netgo"}}' + OUTPUT: "{{.OUTPUT}}" + PACKAGE: "{{.PACKAGE}}" + cmds: + - '{{.BUILD_CMD}} -ldflags="{{.LDFLAGS}}" {{.BUILD_FLAGS}} -o {{.OUTPUT}} {{.PACKAGE}}' + + + + diff --git a/build/darwin/Taskfile.yml b/build/darwin/Taskfile.yml new file mode 100644 index 0000000000..b0af3d8a27 --- /dev/null +++ b/build/darwin/Taskfile.yml @@ -0,0 +1,110 @@ +version: "3" + +includes: + common: ../Taskfile.yml + +tasks: + build: + summary: Builds all macOS binaries + deps: + - task: setup + cmds: + - task: build:amd64 + - task: build:arm64 + + build:amd64: + summary: Builds Encore for macOS AMD64 + deps: + - task: build:components + vars: + ARCH: amd64 + cmds: + - mkdir -p {{.BIN_DIR}}/encore-darwin-amd64/bin + - | + echo "Building macOS AMD64 with SDK: {{.MACOS_SDK_PATH}}" + + # Cross-compilation with macOS 10.15 SDK using Frameworks path + + CGO_ENABLED=1 \ + CC="{{.ZIG_PATH}} cc -target x86_64-macos.10.12 -arch x86_64 -isysroot {{.MACOS_SDK_PATH}} -Wno-error=expansion-to-defined -Wno-error=nullability-completeness -Wno-error=undef-prefix -Wno-error=undefined-var-template" \ + CXX="{{.ZIG_PATH}} c++ -target x86_64-macos.10.12 -arch x86_64 -isysroot {{.MACOS_SDK_PATH}} -Wno-error=expansion-to-defined -Wno-error=nullability-completeness -Wno-error=undef-prefix -Wno-error=undefined-var-template" \ + CGO_CFLAGS="-D_DARWIN_C_SOURCE -D__POSIX_C_SOURCE=200809L -I{{.MACOS_SDK_PATH}}/usr/include" \ + CGO_LDFLAGS="-L{{.MACOS_SDK_PATH}}/usr/lib -F{{.MACOS_SDK_PATH}}/System/Library/Frameworks -lSystem -framework Security -framework CoreFoundation -lpthread -lresolv" \ + go build -ldflags="-s -w -X encr.dev/internal/version.Version={{.VERSION}}" -trimpath -tags=netgo -buildmode=pie -o {{.BIN_DIR}}/encore-darwin-amd64/bin/encore ./cli/cmd/encore + + - mv {{.BIN_DIR}}/git-remote-encore-darwin-amd64 {{.BIN_DIR}}/encore-darwin-amd64/bin/git-remote-encore 2>/dev/null || true + - mv {{.BIN_DIR}}/tsbundler-encore-darwin-amd64 {{.BIN_DIR}}/encore-darwin-amd64/bin/tsbundler-encore 2>/dev/null || true + - cp -r encore-go/* {{.BIN_DIR}}/encore-darwin-amd64/encore-go/ 2>/dev/null || true + - cp LICENSE* {{.BIN_DIR}}/encore-darwin-amd64/ 2>/dev/null || true + - cp README* {{.BIN_DIR}}/encore-darwin-amd64/ 2>/dev/null || true + - mkdir -p {{.BIN_DIR}}/encore-darwin-amd64/runtimes/ + - cp -r runtimes/* {{.BIN_DIR}}/encore-darwin-amd64/runtimes/ 2>/dev/null || true + env: + GOOS: darwin + GOARCH: amd64 + + build:arm64: + summary: Builds Encore for macOS ARM64 + deps: + - task: build:components + vars: + ARCH: arm64 + cmds: + - mkdir -p {{.BIN_DIR}}/encore-darwin-arm64/bin + - | + echo "Building macOS ARM64 with SDK: {{.MACOS_SDK_PATH}}" + + # Cross-compilation with macOS 11.3 SDK for ARM64 targeting 11.3 minimum + + CGO_ENABLED=1 \ + CC="{{.ZIG_PATH}} cc -target aarch64-macos.11.3 -arch arm64 -mmacosx-version-min=11.0 \ + -isysroot {{.MACOS_SDK_PATH}} \ + -D_DARWIN_C_SOURCE -D_DARWIN_UNLIMITED_SELECT -DTARGET_OS_IPHONE=0 -Wno-error=undef-prefix \ + -Wno-error=expansion-to-defined -Wno-error=nullability-completeness -Wno-error=availability" \ + CXX="{{.ZIG_PATH}} c++ -target aarch64-macos.11.3 -arch arm64 -mmacosx-version-min=11.0 \ + -isysroot {{.MACOS_SDK_PATH}} \ + -D_DARWIN_C_SOURCE -D_DARWIN_UNLIMITED_SELECT \ + -Wno-error=expansion-to-defined -Wno-error=nullability-completeness -Wno-error=availability" \ + CGO_CFLAGS="-D_DARWIN_C_SOURCE -D_DARWIN_UNLIMITED_SELECT -mmacosx-version-min=11.0 \ + -I{{.MACOS_SDK_PATH}}/usr/include \ + -Wno-error=availability" \ + CGO_LDFLAGS="--sysroot {{.MACOS_SDK_PATH}} \ + -L{{.MACOS_SDK_PATH}}/usr/lib \ + -F{{.MACOS_SDK_PATH}}/System/Library/Frameworks \ + -lSystem -framework Security -framework CoreFoundation -lpthread -lresolv" \ + go build \ + -ldflags="-s -w -X encr.dev/internal/version.Version={{.VERSION}}" \ + -trimpath -tags=netgo -buildmode=pie \ + -o {{.BIN_DIR}}/encore-darwin-arm64/bin/encore ./cli/cmd/encore + + - mv {{.BIN_DIR}}/git-remote-encore-darwin-arm64 {{.BIN_DIR}}/encore-darwin-arm64/bin/git-remote-encore 2>/dev/null || true + - mv {{.BIN_DIR}}/tsbundler-encore-darwin-arm64 {{.BIN_DIR}}/encore-darwin-arm64/bin/tsbundler-encore 2>/dev/null || true + - cp -r encore-go/* {{.BIN_DIR}}/encore-darwin-arm64/encore-go/ 2>/dev/null || true + - cp LICENSE* {{.BIN_DIR}}/encore-darwin-arm64/ 2>/dev/null || true + - cp README* {{.BIN_DIR}}/encore-darwin-arm64/ 2>/dev/null || true + - mkdir -p {{.BIN_DIR}}/encore-darwin-arm64/runtimes/ + - cp -r runtimes/* {{.BIN_DIR}}/encore-darwin-arm64/runtimes/ 2>/dev/null || true + env: + GOOS: darwin + GOARCH: arm64 + + setup: + internal: true + deps: + - task: common:setup + + build:cli: + summary: Builds only the main CLI for macOS + deps: + - task: build + + + + # Build additional components for macOS + build:components: + summary: Builds git-remote and tsbundler for macOS + cmds: + - | + echo "Building macOS components for {{.ARCH}}..." + CGO_ENABLED=0 GOOS=darwin GOARCH={{.ARCH}} go build -trimpath -o {{.BIN_DIR}}/git-remote-encore-darwin-{{.ARCH}} ./cli/cmd/git-remote-encore + CGO_ENABLED=0 GOOS=darwin GOARCH={{.ARCH}} go build -trimpath -o {{.BIN_DIR}}/tsbundler-encore-darwin-{{.ARCH}} ./cli/cmd/tsbundler-encore diff --git a/build/linux/Taskfile.yml b/build/linux/Taskfile.yml new file mode 100644 index 0000000000..003586c957 --- /dev/null +++ b/build/linux/Taskfile.yml @@ -0,0 +1,67 @@ +version: "3" + +includes: + common: ../Taskfile.yml + +tasks: + build: + summary: Builds all Linux binaries + deps: + - task: common:setup + cmds: + - task: build:amd64 + - task: build:arm64 + + build:cli: + summary: Builds only the main CLI for Linux + deps: + - task: build + + build:amd64: + summary: Builds Encore for Linux AMD64 + deps: + - task: build:components + vars: + ARCH: amd64 + cmds: + - mkdir -p {{.BIN_DIR}}/encore-linux-amd64/bin + - CGO_ENABLED=1 CC="{{.ZIG_PATH}} cc -target x86_64-linux-gnu.2.31" CXX="{{.ZIG_PATH}} c++ -target x86_64-linux-gnu.2.31" go build -ldflags="-s -w -X encr.dev/internal/version.Version={{.VERSION}}" -trimpath -tags=netgo -o {{.BIN_DIR}}/encore-linux-amd64/bin/encore ./cli/cmd/encore + - mv {{.BIN_DIR}}/git-remote-encore-linux-amd64 {{.BIN_DIR}}/encore-linux-amd64/bin/git-remote-encore 2>/dev/null || true + - mv {{.BIN_DIR}}/tsbundler-encore-linux-amd64 {{.BIN_DIR}}/encore-linux-amd64/bin/tsbundler-encore 2>/dev/null || true + - cp -r encore-go/* {{.BIN_DIR}}/encore-linux-amd64/encore-go/ 2>/dev/null || true + - mkdir -p {{.BIN_DIR}}/encore-linux-amd64/runtimes/ + - cp -r runtimes/* {{.BIN_DIR}}/encore-linux-amd64/runtimes/ 2>/dev/null || true + - cp LICENSE* {{.BIN_DIR}}/encore-linux-amd64/ 2>/dev/null || true + - cp README* {{.BIN_DIR}}/encore-linux-amd64/ 2>/dev/null || true + env: + GOOS: linux + GOARCH: amd64 + + build:arm64: + summary: Builds Encore for Linux ARM64 + deps: + - task: build:components + vars: + ARCH: arm64 + cmds: + - mkdir -p {{.BIN_DIR}}/encore-linux-arm64/bin + - CGO_ENABLED=1 CC="{{.ZIG_PATH}} cc -target aarch64-linux-gnu.2.31" CXX="{{.ZIG_PATH}} c++ -target aarch64-linux-gnu.2.31" PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig go build -ldflags="-s -w -X encr.dev/internal/version.Version={{.VERSION}}" -trimpath -tags=netgo -o {{.BIN_DIR}}/encore-linux-arm64/bin/encore ./cli/cmd/encore + - mv {{.BIN_DIR}}/git-remote-encore-linux-arm64 {{.BIN_DIR}}/encore-linux-arm64/bin/git-remote-encore 2>/dev/null || true + - mv {{.BIN_DIR}}/tsbundler-encore-linux-arm64 {{.BIN_DIR}}/encore-linux-arm64/bin/tsbundler-encore 2>/dev/null || true + - cp -r encore-go/* {{.BIN_DIR}}/encore-linux-arm64/encore-go/ 2>/dev/null || true + - mkdir -p {{.BIN_DIR}}/encore-linux-arm64/runtimes/ + - cp -r runtimes/* {{.BIN_DIR}}/encore-linux-arm64/runtimes/ 2>/dev/null || true + - cp LICENSE* {{.BIN_DIR}}/encore-linux-arm64/ 2>/dev/null || true + - cp README* {{.BIN_DIR}}/encore-linux-arm64/ 2>/dev/null || true + env: + GOOS: linux + GOARCH: arm64 + + # Build additional components for Linux + build:components: + summary: Builds git-remote and tsbundler for Linux + cmds: + - | + echo "Building Linux components for {{.ARCH}}..." + CGO_ENABLED=0 GOOS=linux GOARCH={{.ARCH}} go build -trimpath -o {{.BIN_DIR}}/git-remote-encore-linux-{{.ARCH}} ./cli/cmd/git-remote-encore + CGO_ENABLED=0 GOOS=linux GOARCH={{.ARCH}} go build -trimpath -o {{.BIN_DIR}}/tsbundler-encore-linux-{{.ARCH}} ./cli/cmd/tsbundler-encore diff --git a/build/windows/Taskfile.yml b/build/windows/Taskfile.yml new file mode 100644 index 0000000000..3981291d71 --- /dev/null +++ b/build/windows/Taskfile.yml @@ -0,0 +1,101 @@ +version: "3" + +includes: + common: ../Taskfile.yml + +tasks: + build: + summary: Builds all Windows binaries + deps: + - task: setup + cmds: + - task: build:amd64 + + # Windows CLI build + build:amd64: + summary: Builds Encore for Windows AMD64 + deps: + - task: build:components + vars: + ARCH: amd64 + cmds: + - mkdir -p {{.BIN_DIR}}/encore-windows-amd64/bin + - CGO_ENABLED=1 CC="{{.ZIG_PATH}} cc -target x86_64-windows-gnu -fno-stack-protector" CXX="{{.ZIG_PATH}} c++ -target x86_64-windows-gnu -fno-stack-protector" go build -ldflags="-s -w -X encr.dev/internal/version.Version={{.VERSION}} -H=windowsgui" -trimpath -tags=netgo -o {{.BIN_DIR}}/encore-windows-amd64/bin/encore.exe ./cli/cmd/encore + - mv {{.BIN_DIR}}/git-remote-encore-windows-amd64.exe {{.BIN_DIR}}/encore-windows-amd64/bin/git-remote-encore.exe 2>/dev/null || true + - mv {{.BIN_DIR}}/tsbundler-encore-windows-amd64.exe {{.BIN_DIR}}/encore-windows-amd64/bin/tsbundler-encore.exe 2>/dev/null || true + - cp -r encore-go/* {{.BIN_DIR}}/encore-windows-amd64/encore-go/ 2>/dev/null || true + - mkdir -p {{.BIN_DIR}}/encore-windows-amd64/runtimes/ + - cp -r runtimes/* {{.BIN_DIR}}/encore-windows-amd64/runtimes/ 2>/dev/null || true + - cp LICENSE* {{.BIN_DIR}}/encore-windows-amd64/ 2>/dev/null || true + - cp README* {{.BIN_DIR}}/encore-windows-amd64/ 2>/dev/null || true + env: + GOOS: windows + GOARCH: amd64 + + # Windows components + build:components: + summary: Builds Windows components + cmds: + - CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -o {{.BIN_DIR}}/git-remote-encore-windows-amd64.exe ./cli/cmd/git-remote-encore + - CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -o {{.BIN_DIR}}/tsbundler-encore-windows-amd64.exe ./cli/cmd/tsbundler-encore + + # TS parser for Windows + build:tsparser: + summary: Builds TS parser for Windows + cmds: + - task: common:build:rust:tsparser + vars: + TARGET: x86_64-pc-windows-gnu + + # JS runtime for Windows + build:js:runtimes: + summary: Builds JS runtime for Windows + cmds: + - task: common:build:js:runtime + vars: + OS: windows + ARCH: amd64 + TARGET: x86_64-pc-windows-gnu + OUTPUT_DIR: "{{.BIN_DIR}}" + + download:runtimes: + summary: Downloads Windows runtimes + internal: true + cmds: + - | + if [ ! -d "runtimes/windows_amd64/encore-go" ]; then + echo "Downloading encore-go for windows_amd64" + mkdir -p runtimes/windows_amd64 + curl --fail -L "https://github.com/encoredev/go/releases/download/{{.ENCORE_GO_VERSION}}/windows_x86-64.tar.gz" -o temp-windows.tar.gz + gzip -d temp-windows.tar.gz + tar -xf temp-windows.tar -C runtimes/windows_amd64 + rm temp-windows.tar + else + echo "encore-go already exists for windows_amd64" + fi + + # Windows packaging + package: + summary: Packages complete Windows distribution + deps: + - task: build + cmds: + - task: package:amd64 + + package:amd64: + summary: Packages complete Windows AMD64 distribution + deps: + - task: build:amd64 + - task: build:components + - task: build:tsparser + - task: build:js:runtimes + cmds: + - task: common:assemble:distribution + vars: + PLATFORM: windows-amd64 + RUNTIME_PLATFORM: windows_amd64 + VERSION_SUFFIX: "" + - task: common:package:zip + vars: + ARCHIVE_DIR: "{{.BIN_DIR}}/encore-windows-amd64" + ARCHIVE_NAME: "{{.BIN_DIR}}/encore-{{.VERSION}}-windows_amd64.zip" \ No newline at end of file diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000000..b59b68cec6 --- /dev/null +++ b/mise.toml @@ -0,0 +1,55 @@ +[env] +ENCORE_RUNTIMES_PATH = "{{config_root}}/runtimes" +# ENCORE_GOROOT = "{{mise.installs.aqua-encoredev-encore}}/encore-go" + +[tools] +# Core languages +go = "1.23" +node = "20.19" +rust = "stable" + +# Build tools +goreleaser = "latest" +protoc = "latest" +task = "latest" + +# Development tools +golangci-lint = "latest" +# semgrep = "latest" + +# Cross Compilation compiler +[tools.zig] +version = "0.9.1" + +[tasks] +[tasks.build] +description = "Build Encore CLI using Task" +run = "task build" + +[tasks.setup] +description = "Setup development environment" +run = "task setup" + +[tasks.test] +description = "Run all tests" +run = "task test" + +[tasks.release] +description = "Full release using Task" +run = "task release" + +[tasks.release-test] +description = "Test release process" +run = "task release:test" + +[tasks.clean] +description = "Clean build artifacts" +run = "task clean" + +[tasks.lint] +description = "Run linters" +run = "task lint" + +[tasks.cli-build] +description = "Build Encore CLI" +run = "go build ./cli/cmd/encore" diff --git a/pkg/clientgen/golang.go b/pkg/clientgen/golang.go index c7850949c3..a0527fe9d8 100644 --- a/pkg/clientgen/golang.go +++ b/pkg/clientgen/golang.go @@ -740,10 +740,19 @@ func (g *golang) rpcCallSite(rpc *meta.RPC) (code []Code, err error) { enc := g.enc.NewPossibleInstance("respDecoder") for _, field := range respEnc.HeaderParameters { + var getValue Code + if strings.ToLower(field.WireFormat) == "set-cookie" && field.Type.GetList() != nil { + // For Set-Cookie arrays, use Values() to get all cookies + getValue = Id(headersId).Dot("Values").Call(Lit(field.WireFormat)) + } else { + // For other headers, use the standard approach + getValue = Id(headersId).Dot("Get").Call(Lit(field.WireFormat)) + } + str, err := enc.FromString( field.Type, field.SrcName, - Id(headersId).Dot("Get").Call(Lit(field.WireFormat)), + getValue, Id(headersId).Dot("Values").Call(Lit(field.WireFormat)), true, ) diff --git a/pkg/clientgen/javascript.go b/pkg/clientgen/javascript.go index 44d8c7e464..50698366a8 100644 --- a/pkg/clientgen/javascript.go +++ b/pkg/clientgen/javascript.go @@ -523,6 +523,8 @@ func (js *javascript) rpcCallSite(w *indentWriter, rpc *meta.RPC, rpcPath string for _, headerField := range respEnc.HeaderParameters { isSetCookie := strings.ToLower(headerField.WireFormat) == "set-cookie" + isSetCookieArray := isSetCookie && headerField.Type.GetList() != nil + if isSetCookie { w.WriteString("// Skip set-cookie header in browser context as browsers doesn't have access to read it\n") w.WriteString("if (!BROWSER) {\n") @@ -530,9 +532,16 @@ func (js *javascript) rpcCallSite(w *indentWriter, rpc *meta.RPC, rpcPath string } js.seenHeaderResponse = true - fieldValue := fmt.Sprintf("mustBeSet(\"Header `%s`\", resp.headers.get(\"%s\"))", headerField.WireFormat, headerField.WireFormat) - w.WriteStringf("%s = %s\n", js.Dot("rtn", headerField.SrcName), js.convertStringToBuiltin(headerField.Type.GetBuiltin(), fieldValue)) + if isSetCookieArray { + // Handle multiple Set-Cookie headers + fieldValue := fmt.Sprintf("resp.headers.getAll(\"%s\")", headerField.WireFormat) + w.WriteStringf("%s = %s\n", js.Dot("rtn", headerField.SrcName), fieldValue) + } else { + // Handle single value headers (including single Set-Cookie) + fieldValue := fmt.Sprintf("mustBeSet(\"Header `%s`\", resp.headers.get(\"%s\"))", headerField.WireFormat, headerField.WireFormat) + w.WriteStringf("%s = %s\n", js.Dot("rtn", headerField.SrcName), js.convertStringToBuiltin(headerField.Type.GetBuiltin(), fieldValue)) + } if isSetCookie { w = w.Dedent() diff --git a/pkg/clientgen/typescript.go b/pkg/clientgen/typescript.go index e547e96c50..51c5fd8186 100644 --- a/pkg/clientgen/typescript.go +++ b/pkg/clientgen/typescript.go @@ -761,6 +761,8 @@ func (ts *typescript) rpcCallSite(ns string, w *indentWriter, rpc *meta.RPC, rpc for _, headerField := range respEnc.HeaderParameters { isSetCookie := strings.ToLower(headerField.WireFormat) == "set-cookie" + isSetCookieArray := isSetCookie && headerField.Type.GetList() != nil + if isSetCookie { w.WriteString("// Skip set-cookie header in browser context as browsers doesn't have access to read it\n") w.WriteString("if (!BROWSER) {\n") @@ -768,9 +770,16 @@ func (ts *typescript) rpcCallSite(ns string, w *indentWriter, rpc *meta.RPC, rpc } ts.seenHeaderResponse = true - fieldValue := fmt.Sprintf("mustBeSet(\"Header `%s`\", resp.headers.get(\"%s\"))", headerField.WireFormat, headerField.WireFormat) - w.WriteStringf("%s = %s\n", ts.Dot("rtn", headerField.SrcName), ts.convertStringToBuiltin(headerField.Type.GetBuiltin(), fieldValue)) + if isSetCookieArray { + // Handle multiple Set-Cookie headers + fieldValue := fmt.Sprintf("resp.headers.getAll(\"%s\")", headerField.WireFormat) + w.WriteStringf("%s = %s\n", ts.Dot("rtn", headerField.SrcName), fieldValue) + } else { + // Handle single value headers (including single Set-Cookie) + fieldValue := fmt.Sprintf("mustBeSet(\"Header `%s`\", resp.headers.get(\"%s\"))", headerField.WireFormat, headerField.WireFormat) + w.WriteStringf("%s = %s\n", ts.Dot("rtn", headerField.SrcName), ts.convertStringToBuiltin(headerField.Type.GetBuiltin(), fieldValue)) + } if isSetCookie { w = w.Dedent() diff --git a/v2/codegen/apigen/endpointgen/response.go b/v2/codegen/apigen/endpointgen/response.go index 6336a8e442..3d486661b6 100644 --- a/v2/codegen/apigen/endpointgen/response.go +++ b/v2/codegen/apigen/endpointgen/response.go @@ -92,15 +92,13 @@ func (d *responseDesc) EncodeResponse() *Statement { g.Line().Comment("Encode headers") g.Id("headers").Op("=").Map(String()).Index().String().Values(DictFunc(func(dict Dict) { for _, f := range resp.HeaderParameters { - if builtin, ok := f.Type.(schema.BuiltinType); ok { - encExpr := genutil.MarshalBuiltin(builtin.Kind, Id("resp").Dot(f.SrcName)) - dict[Lit(f.WireName)] = Index().String().Values(encExpr) - } else { - d.gu.Errs.Addf(f.Type.ASTExpr().Pos(), "unsupported type in header: %s", d.gu.TypeToString(f.Type)) + if unprocessedType := processHeaderField(f, dict); unprocessedType != nil { + d.gu.Errs.Addf(f.Type.ASTExpr().Pos(), "unsupported type in header: %s", d.gu.TypeToString(unprocessedType)) } } })) } + }) // If response is a ptr we need to check it's not nil @@ -149,6 +147,30 @@ func (d *responseDesc) EncodeResponse() *Statement { }) } +// processHeaderField processes a single header field. +// Returns the unsupported schema. Type if processing fails, nil if successful. +func processHeaderField(f *apienc.ParameterEncoding, headersMap Dict) schema.Type { + kind, isList, ok := schemautil.IsBuiltinOrList(f.Type) + if !ok { + return f.Type + } + + // Handling for Set-Cookie arrays + if f.WireName == "set-cookie" && isList && kind == schema.String { + headersMap[Lit(f.WireName)] = genutil.MarshalBuiltinList(kind, Id("resp").Dot(f.SrcName)) + return nil + } + + var encExpr Code + if isList { + encExpr = genutil.MarshalBuiltinList(kind, Id("resp").Dot(f.SrcName)) + } else { + encExpr = genutil.MarshalBuiltin(kind, Id("resp").Dot(f.SrcName)) + } + headersMap[Lit(f.WireName)] = Index().String().Values(encExpr) + return nil +} + func (d *responseDesc) DecodeExternalResp() *Statement { if d.ep.Raw { // TODO(andre) support diff --git a/v2/codegen/apigen/endpointgen/testdata/set_cookie_array.txt b/v2/codegen/apigen/endpointgen/testdata/set_cookie_array.txt new file mode 100644 index 0000000000..1045c8b240 --- /dev/null +++ b/v2/codegen/apigen/endpointgen/testdata/set_cookie_array.txt @@ -0,0 +1,145 @@ +-- code.go -- +package code + +import "context" + +type Response struct { + Cookies []string `header:"Set-Cookie"` + OtherHeader string `header:"X-Other"` +} + +//encore:api public +func TestCookies(ctx context.Context) (*Response, error) { return nil, nil } + +-- want:encore.gen.go -- +// Code generated by encore. DO NOT EDIT. + +package code + +import "context" + +// These functions are automatically generated and maintained by Encore +// to simplify calling them from other services, as they were implemented as methods. +// They are automatically updated by Encore whenever your API endpoints change. + +// Interface defines the service's API surface area, primarily for mocking purposes. +// +// Raw endpoints are currently excluded from this interface, as Encore does not yet +// support service-to-service API calls to raw endpoints. +type Interface interface { + TestCookies(ctx context.Context) (*Response, error) +} +-- want:encore_internal__api.go -- +package code + +import ( + "context" + __api "encore.dev/appruntime/apisdk/api" + __etype "encore.dev/appruntime/shared/etype" + jsoniter "github.com/json-iterator/go" + "net/http" + "net/url" +) + +func init() { + __api.RegisterEndpoint(EncoreInternal_api_APIDesc_TestCookies, TestCookies) +} + +type EncoreInternal_TestCookiesReq struct{} + +type EncoreInternal_TestCookiesResp = *Response + +var EncoreInternal_api_APIDesc_TestCookies = &__api.Desc[*EncoreInternal_TestCookiesReq, EncoreInternal_TestCookiesResp]{ + Access: __api.Public, + AppHandler: func(ctx context.Context, reqData *EncoreInternal_TestCookiesReq) (EncoreInternal_TestCookiesResp, error) { + resp, err := TestCookies(ctx) + if err != nil { + return (*Response)(nil), err + } + return resp, nil + }, + CloneReq: func(r *EncoreInternal_TestCookiesReq) (*EncoreInternal_TestCookiesReq, error) { + var clone *EncoreInternal_TestCookiesReq + bytes, err := jsoniter.ConfigDefault.Marshal(r) + if err == nil { + err = jsoniter.ConfigDefault.Unmarshal(bytes, &clone) + } + return clone, err + }, + CloneResp: func(r EncoreInternal_TestCookiesResp) (EncoreInternal_TestCookiesResp, error) { + var clone EncoreInternal_TestCookiesResp + bytes, err := jsoniter.ConfigDefault.Marshal(r) + if err == nil { + err = jsoniter.ConfigDefault.Unmarshal(bytes, &clone) + } + return clone, err + }, + DecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_TestCookiesResp, err error) { + resp = new(Response) + dec := new(__etype.Unmarshaller) + // Decode headers + h := httpResp.Header + resp.Cookies = __etype.UnmarshalList(dec, __etype.UnmarshalString, "set-cookie", h.Values("set-cookie"), false) + resp.OtherHeader = __etype.UnmarshalOne(dec, __etype.UnmarshalString, "x-other", h.Get("x-other"), false) + + if err := dec.Error; err != nil { + return (*Response)(nil), err + } + return resp, nil + }, + DecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_TestCookiesReq, pathParams __api.UnnamedParams, err error) { + reqData = new(EncoreInternal_TestCookiesReq) + return reqData, nil, nil + }, + DefLoc: uint32(0x0), + EncodeExternalReq: func(reqData *EncoreInternal_TestCookiesReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) { + return nil, nil, nil + }, + EncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_TestCookiesResp, status int) (err error) { + respData := []byte{'\n'} + var headers map[string][]string + if resp != nil { + + // Encode headers + headers = map[string][]string{ + "set-cookie": __etype.MarshalList(__etype.MarshalString, resp.Cookies), + "x-other": []string{__etype.MarshalOne(__etype.MarshalString, resp.OtherHeader)}, + } + } + + // Set response headers + for k, vs := range headers { + for _, v := range vs { + w.Header().Add(k, v) + } + } + + // Set HTTP status code + if status != 0 { + w.WriteHeader(status) + } + + // Write response body + w.Write(respData) + return nil + }, + Endpoint: "TestCookies", + Fallback: false, + GlobalMiddlewareIDs: []string{}, + Methods: []string{"GET", "POST"}, + Path: "/code.TestCookies", + PathParamNames: nil, + Raw: false, + RawHandler: nil, + RawPath: "/code.TestCookies", + ReqPath: func(reqData *EncoreInternal_TestCookiesReq) (string, __api.UnnamedParams, error) { + return "/code.TestCookies", nil, nil + }, + ReqUserPayload: func(reqData *EncoreInternal_TestCookiesReq) any { + return nil + }, + Service: "code", + ServiceMiddleware: []*__api.Middleware{}, + SvcNum: 1, + Tags: nil, +} diff --git a/v2/parser/apis/api/apienc/encoding.go b/v2/parser/apis/api/apienc/encoding.go index fdd9fa4484..b8439f6974 100644 --- a/v2/parser/apis/api/apienc/encoding.go +++ b/v2/parser/apis/api/apienc/encoding.go @@ -535,6 +535,17 @@ func describeParam(errs *perr.List, encodingHints *encodingHints, field schema.S } param.Location = location + + // Validate Set-Cookie array types + if location == Header && strings.ToLower(param.WireName) == "set-cookie" { + if kind, isList, ok := schemautil.IsBuiltinOrList(param.Type); ok { + if isList && kind != schema.String { + errs.Addf(field.Type.ASTExpr().Pos(), "Set-Cookie header arrays must be []string, got []%s", kind) + return nil, false + } + } + } + return ¶m, true }