diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c536645 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Alif Rachmawadi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b49781f --- /dev/null +++ b/README.md @@ -0,0 +1,315 @@ +# flutter-action + +Flutter environment for use in GitHub Actions. It works on Linux, Windows, and +macOS. + +The following sections show how to configure this action. + +## Specifying Flutter version + +### Use specific version and channel + +```yaml +steps: + - name: Clone repository + uses: actions/checkout@v4 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: 3.19.0 + - run: flutter --version +``` + +### Use version from pubspec.yaml + +This is inspired by [`actions/setup-go`](https://github.com/actions/setup-go). + +```yaml +steps: + - name: Clone repository + uses: actions/checkout@v4 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version-file: pubspec.yaml # path to pubspec.yaml + - run: flutter --version +``` + +> [!IMPORTANT] +> +> For `flutter-version-file` to work, you need to have the exact Flutter version +> defined in your pubspec.yaml: +> +> **Good** +> +> ```yaml +> environment: +> sdk: ">=3.3.0 <4.0.0" +> flutter: 3.19.0 +> ``` +> +> **Bad** +> +> ```yaml +> environment: +> sdk: ">=3.3.0 <4.0.0" +> flutter: ">= 3.19.0 <4.0.0" +> ``` + +> [!WARNING] +> +> Using `flutter-version-file` requires [`yq`](https://github.com/mikefarah/yq), +> which is not pre-installed in `windows` images. Install it yourself. + +### Use latest release for particular channel + +```yaml +steps: + - name: Clone repository + uses: actions/checkout@v4 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable # or: beta, master (or main) + - run: flutter --version +``` + +### Use latest release for particular version and/or channel + +```yaml +steps: + - name: Clone repository + uses: actions/checkout@v4 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: dev + flutter-version: 1.22.x + - run: flutter --version +``` + +### Use particular version on any channel + +```yaml +steps: + - name: Clone repository + uses: actions/checkout@v4 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: any + flutter-version: 3.x + - run: flutter --version +``` + +### Use particular git reference on master channel + +```yaml +steps: + - name: Clone repository + uses: actions/checkout@v4 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: master + flutter-version: 5b12b74 # tag, commit or branch + - run: flutter --version +``` + +## Build Target + +Build **Android** APK and app bundle: + +```yaml +steps: + - name: Clone repository + uses: actions/checkout@v4 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: 3.19.0 + - run: flutter pub get + - run: flutter test + - run: flutter build apk + - run: flutter build appbundle +``` + +### Build for iOS + +> [!NOTE] +> +> Building for iOS requires a macOS runner. + +```yaml +jobs: + main: + runs-on: macos-latest + steps: + - name: Clone repository + uses: actions/checkout@v4 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + - run: flutter pub get + - run: flutter test + - run: flutter build ios --release --no-codesign +``` + +### Build for the web + +```yaml +steps: + - name: Clone repository + uses: actions/checkout@v4 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + - run: flutter pub get + - run: flutter test + - run: flutter build web +``` + +### Build for Windows + +```yaml +jobs: + main: + runs-on: windows-latest + steps: + - name: Clone repository + uses: actions/checkout@v4 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + - run: flutter build windows +``` + +### Build for Linux desktop + +```yaml +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v4 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + - run: | + sudo apt-get update -y + sudo apt-get install -y ninja-build libgtk-3-dev + - run: flutter build linux +``` + +### Build for macOS desktop + +> [!NOTE] +> +> Building for macOS requires a macOS runner. + +```yaml +jobs: + main: + runs-on: macos-latest + steps: + - name: Clone repository + uses: actions/checkout@v4 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + - run: flutter build macos +``` + +## Caching + +Integration with [`actions/cache`](https://github.com/actions/cache): + +```yaml +steps: + - name: Clone repository + uses: actions/checkout@v4 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + # optional parameters follow + cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:" # optional, change this to force refresh cache + cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:" # optional, change this to specify the cache path + pub-cache-key: "flutter-pub:os:-:channel:-:version:-:arch:-:hash:" # optional, change this to force refresh cache of dart pub get dependencies + pub-cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:" # optional, change this to specify the cache path + - run: flutter --version +``` + +Note: `cache-key`, `pub-cache-key`, and `cache-path` have support for several +dynamic values: + +- `:os:` +- `:channel:` +- `:version:` +- `:arch:` +- `:hash:` +- `:sha256:` + +Use outputs from `flutter-action`: + +```yaml +steps: + - name: Clone repository + - uses: actions/checkout@v4 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + id: flutter-action + with: + channel: stable + - name: Print outputs + shell: bash + run: | + echo CACHE-PATH=${{ steps.flutter-action.outputs.CACHE-PATH }} + echo CACHE-KEY=${{ steps.flutter-action.outputs.CACHE-KEY }} + echo CHANNEL=${{ steps.flutter-action.outputs.CHANNEL }} + echo VERSION=${{ steps.flutter-action.outputs.VERSION }} + echo ARCHITECTURE=${{ steps.flutter-action.outputs.ARCHITECTURE }} + echo PUB-CACHE-PATH=${{ steps.flutter-action.outputs.PUB-CACHE-PATH }} + echo PUB-CACHE-KEY=${{ steps.flutter-action.outputs.PUB-CACHE-KEY }} +``` + +If you don't need to install Flutter and just want the outputs, you can use the +`dry-run` option: + +```yaml +steps: + - name: Clone repository + - uses: actions/checkout@v4 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + id: flutter-action + with: + channel: stable + dry-run: true + - run: | + echo CACHE-PATH=${{ steps.flutter-action.outputs.CACHE-PATH }} + echo CACHE-KEY=${{ steps.flutter-action.outputs.CACHE-KEY }} + echo CHANNEL=${{ steps.flutter-action.outputs.CHANNEL }} + echo VERSION=${{ steps.flutter-action.outputs.VERSION }} + echo ARCHITECTURE=${{ steps.flutter-action.outputs.ARCHITECTURE }} + echo PUB-CACHE-PATH=${{ steps.flutter-action.outputs.PUB-CACHE-PATH }} + echo PUB-CACHE-KEY=${{ steps.flutter-action.outputs.PUB-CACHE-KEY }} + shell: bash +``` + +## Maintainers + +- [Alif Rachmawadi] (original creator) +- [Bartek Pacia] + +[Alif Rachmawadi]: https://github.com/subosito +[Bartek Pacia]: https://github.com/bartekpacia diff --git a/action.yaml b/action.yaml new file mode 100644 index 0000000..73774f4 --- /dev/null +++ b/action.yaml @@ -0,0 +1,121 @@ +name: Set up Flutter +description: Setup your runner with Flutter environment +author: Alif Rachmawadi +branding: + icon: maximize + color: blue + +inputs: + channel: + description: The Flutter build release channel + required: false + default: stable + flutter-version: + description: The Flutter version to make available on the path + required: false + default: "" + flutter-version-file: + description: The pubspec.yaml file with exact Flutter version defined + required: false + default: "" + architecture: + description: The architecture of Flutter SDK executable (x64 or arm64) + required: false + default: "${{ runner.arch }}" + cache: + description: Cache the Flutter SDK + required: false + default: "false" + cache-key: + description: Identifier for the Flutter SDK cache + required: false + default: "" + cache-path: + description: Flutter SDK cache path + required: false + default: "" + pub-cache-key: + description: Identifier for the Dart .pub-cache cache + required: false + default: "" + pub-cache-path: + description: Flutter pub cache path + required: false + default: default + dry-run: + description: If true, get outputs but do not install Flutter + required: false + default: "false" + +outputs: + CHANNEL: + value: "${{ steps.flutter-action.outputs.CHANNEL }}" + description: The selected Flutter release channel + VERSION: + value: "${{ steps.flutter-action.outputs.VERSION }}" + description: The selected Flutter version + ARCHITECTURE: + value: "${{ steps.flutter-action.outputs.ARCHITECTURE }}" + description: The selected Flutter CPU architecture + CACHE-KEY: + value: "${{ steps.flutter-action.outputs.CACHE-KEY }}" + description: Key used to cache the Flutter SDK + CACHE-PATH: + value: "${{ steps.flutter-action.outputs.CACHE-PATH }}" + description: Path to Flutter SDK + PUB-CACHE-KEY: + value: "${{ steps.flutter-action.outputs.PUB-CACHE-KEY }}" + description: Key used to cache the pub dependencies + PUB-CACHE-PATH: + value: "${{ steps.flutter-action.outputs.PUB-CACHE-PATH }}" + description: Path to pub cache + +runs: + using: composite + steps: + - name: Make setup script executable + run: chmod +x "$GITHUB_ACTION_PATH/setup.sh" + shell: bash + + - name: Set action inputs + id: flutter-action + shell: bash + run: | + $GITHUB_ACTION_PATH/setup.sh -p \ + -n '${{ inputs.flutter-version }}' \ + -f '${{ inputs.flutter-version-file }}' \ + -a '${{ inputs.architecture }}' \ + -k '${{ inputs.cache-key }}' \ + -c '${{ inputs.cache-path }}' \ + -l '${{ inputs.pub-cache-key }}' \ + -d '${{ inputs.pub-cache-path }}' \ + ${{ inputs.channel }} + + - name: Cache Flutter + uses: actions/cache@v4 + if: ${{ inputs.cache == 'true' }} + with: + path: ${{ steps.flutter-action.outputs.CACHE-PATH }} + key: ${{ steps.flutter-action.outputs.CACHE-KEY }} + restore-keys: | + ${{ steps.flutter-action.outputs.CACHE-KEY }} + + - name: Cache pub dependencies + uses: actions/cache@v4 + if: ${{ inputs.cache == 'true' }} + with: + path: ${{ steps.flutter-action.outputs.PUB-CACHE-PATH }} + key: ${{ steps.flutter-action.outputs.PUB-CACHE-KEY }}-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ steps.flutter-action.outputs.PUB-CACHE-KEY }}-${{ hashFiles('**/pubspec.lock') }} + ${{ steps.flutter-action.outputs.PUB-CACHE-KEY }} + + - name: Run setup script + shell: bash + if: ${{ inputs.dry-run != 'true' }} + run: | + $GITHUB_ACTION_PATH/setup.sh \ + -n '${{ steps.flutter-action.outputs.VERSION }}' \ + -a '${{ steps.flutter-action.outputs.ARCHITECTURE }}' \ + -c '${{ steps.flutter-action.outputs.CACHE-PATH }}' \ + ${{ steps.flutter-action.outputs.CHANNEL }} diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..9cce2cc --- /dev/null +++ b/setup.sh @@ -0,0 +1,236 @@ +#!/bin/bash +set -eu + +check_command() { + command -v "$1" >/dev/null 2>&1 +} + +if ! check_command jq; then + echo "jq not found. Install it from https://stedolan.github.io/jq" + exit 1 +fi + +OS_NAME=$(echo "$RUNNER_OS" | awk '{print tolower($0)}') +ARCH_NAME=$(echo "$RUNNER_ARCH" | awk '{print tolower($0)}') +MANIFEST_BASE_URL="https://storage.googleapis.com/flutter_infra_release/releases" +MANIFEST_JSON_PATH="releases_$OS_NAME.json" +MANIFEST_URL="$MANIFEST_BASE_URL/$MANIFEST_JSON_PATH" + +filter_by_channel() { + jq --arg channel "$1" '[.releases[] | select($channel == "any" or .channel == $channel)]' +} + +filter_by_arch() { + jq --arg arch "$1" '[.[] | select(.dart_sdk_arch == $arch or ($arch == "x64" and (has("dart_sdk_arch") | not)))]' +} + +filter_by_version() { + jq --arg version "$1" '.[].version |= gsub("^v"; "") | (if $version == "any" then .[0] else (map(select(.version == $version or (.version | startswith(($version | sub("\\.x$"; "")) + ".")) and .version != $version)) | .[0]) end)' +} + +not_found_error() { + echo "Unable to determine Flutter version for channel: $1 version: $2 architecture: $3" +} + +transform_path() { + if [ "$OS_NAME" = windows ]; then + echo "$1" | sed -e 's/^\///' -e 's/\//\\/g' + else + echo "$1" + fi +} + +download_archive() { + archive_url="$MANIFEST_BASE_URL/$1" + archive_name=$(basename "$1") + archive_local="$RUNNER_TEMP/$archive_name" + + curl --connect-timeout 15 --retry 5 "$archive_url" >"$archive_local" + + mkdir -p "$2" + + case "$archive_name" in + *.zip) + EXTRACT_PATH="$RUNNER_TEMP/_unzip_temp" + unzip -q -o "$archive_local" -d "$EXTRACT_PATH" + # Remove the folder again so that the move command can do a simple rename + # instead of moving the content into the target folder. + # This is a little bit of a hack since the "mv --no-target-directory" + # linux option is not available here + rm -r "$2" + mv "$EXTRACT_PATH"/flutter "$2" + rm -r "$EXTRACT_PATH" + ;; + *) + tar xf "$archive_local" -C "$2" --strip-components=1 + ;; + esac + + rm "$archive_local" +} + +CACHE_PATH="" +CACHE_KEY="" +PUB_CACHE_PATH="" +PUB_CACHE_KEY="" +PRINT_ONLY="" +TEST_MODE=false +ARCH="" +VERSION="" +VERSION_FILE="" + +while getopts 'tc:k:d:l:pa:n:f:' flag; do + case "$flag" in + c) CACHE_PATH="$OPTARG" ;; + k) CACHE_KEY="$OPTARG" ;; + d) PUB_CACHE_PATH="$OPTARG" ;; + l) PUB_CACHE_KEY="$OPTARG" ;; + p) PRINT_ONLY=true ;; + t) TEST_MODE=true ;; + a) ARCH="$(echo "$OPTARG" | awk '{print tolower($0)}')" ;; + n) VERSION="$OPTARG" ;; + f) + VERSION_FILE="$OPTARG" + if [ -n "$VERSION_FILE" ] && ! check_command yq; then + echo "yq not found. Install it from https://mikefarah.gitbook.io/yq" + exit 1 + fi + ;; + ?) exit 2 ;; + esac +done + +[ -z "$ARCH" ] && ARCH="$ARCH_NAME" + +if [ -n "$VERSION_FILE" ]; then + if [ -n "$VERSION" ]; then + echo "Cannot specify both a version and a version file" + exit 1 + fi + + VERSION="$(yq '.environment.flutter' "$VERSION_FILE")" +fi + +ARR_CHANNEL=("${@:$OPTIND:1}") +CHANNEL="${ARR_CHANNEL[0]:-}" + +[ -z "$CHANNEL" ] && CHANNEL=stable +[ -z "$VERSION" ] && VERSION=any +[ -z "$ARCH" ] && ARCH=x64 +[ -z "$CACHE_PATH" ] && CACHE_PATH="$RUNNER_TOOL_CACHE/flutter/:channel:-:version:-:arch:" +[ -z "$CACHE_KEY" ] && CACHE_KEY="flutter-:os:-:channel:-:version:-:arch:-:hash:" +[ -z "$PUB_CACHE_KEY" ] && PUB_CACHE_KEY="flutter-pub-:os:-:channel:-:version:-:arch:-:hash:" +[ -z "$PUB_CACHE_PATH" ] && PUB_CACHE_PATH="default" + +# `PUB_CACHE` is what Dart and Flutter looks for in the environment, while +# `PUB_CACHE_PATH` is passed in from the action. +# +# If `PUB_CACHE` is set already, then it should continue to be used. Otherwise, satisfy it +# if the action requests a custom path, or set to the Dart default values depending +# on the operating system. +if [ -z "${PUB_CACHE:-}" ]; then + if [ "$PUB_CACHE_PATH" != "default" ]; then + PUB_CACHE="$PUB_CACHE_PATH" + elif [ "$OS_NAME" = "windows" ]; then + PUB_CACHE="$LOCALAPPDATA\\Pub\\Cache" + else + PUB_CACHE="$HOME/.pub-cache" + fi +fi + +if [ "$TEST_MODE" = true ]; then + RELEASE_MANIFEST=$(cat "$(dirname -- "${BASH_SOURCE[0]}")/test/$MANIFEST_JSON_PATH") +else + RELEASE_MANIFEST=$(curl --silent --connect-timeout 15 --retry 5 "$MANIFEST_URL") +fi + +if [ "$CHANNEL" = "master" ] || [ "$CHANNEL" = "main" ]; then + VERSION_MANIFEST="{\"channel\":\"$CHANNEL\",\"version\":\"$VERSION\",\"dart_sdk_arch\":\"$ARCH\",\"hash\":\"$CHANNEL\",\"sha256\":\"$CHANNEL\"}" +else + VERSION_MANIFEST=$(echo "$RELEASE_MANIFEST" | filter_by_channel "$CHANNEL" | filter_by_arch "$ARCH" | filter_by_version "$VERSION") +fi + +case "$VERSION_MANIFEST" in +*null*) + not_found_error "$CHANNEL" "$VERSION" "$ARCH" + exit 1 + ;; +esac + +expand_key() { + version_channel=$(echo "$VERSION_MANIFEST" | jq -r '.channel') + version_version=$(echo "$VERSION_MANIFEST" | jq -r '.version') + version_arch=$(echo "$VERSION_MANIFEST" | jq -r '.dart_sdk_arch // "x64"') + version_hash=$(echo "$VERSION_MANIFEST" | jq -r '.hash') + version_sha_256=$(echo "$VERSION_MANIFEST" | jq -r '.sha256') + + expanded_key="${1/:channel:/$version_channel}" + expanded_key="${expanded_key/:version:/$version_version}" + expanded_key="${expanded_key/:arch:/$version_arch}" + expanded_key="${expanded_key/:hash:/$version_hash}" + expanded_key="${expanded_key/:sha256:/$version_sha_256}" + expanded_key="${expanded_key/:os:/$OS_NAME}" + + echo "$expanded_key" +} + +CACHE_KEY=$(expand_key "$CACHE_KEY") +PUB_CACHE_KEY=$(expand_key "$PUB_CACHE_KEY") +CACHE_PATH=$(expand_key "$(transform_path "$CACHE_PATH")") + +if [ "$PRINT_ONLY" = true ]; then + version_info=$(echo "$VERSION_MANIFEST" | jq -j '.channel,":",.version,":",.dart_sdk_arch // "x64"') + + info_channel=$(echo "$version_info" | awk -F ':' '{print $1}') + info_version=$(echo "$version_info" | awk -F ':' '{print $2}') + info_architecture=$(echo "$version_info" | awk -F ':' '{print $3}') + + if [ "$TEST_MODE" = true ]; then + echo "CHANNEL=$info_channel" + echo "VERSION=$info_version" + # VERSION_FILE is not printed, because it is essentially same as VERSION + echo "ARCHITECTURE=$info_architecture" + echo "CACHE-KEY=$CACHE_KEY" + echo "CACHE-PATH=$CACHE_PATH" + echo "PUB-CACHE-KEY=$PUB_CACHE_KEY" + echo "PUB-CACHE-PATH=$PUB_CACHE" + exit 0 + fi + + { + echo "CHANNEL=$info_channel" + echo "VERSION=$info_version" + # VERSION_FILE is not printed, because it is essentially same as VERSION + echo "ARCHITECTURE=$info_architecture" + echo "CACHE-KEY=$CACHE_KEY" + echo "CACHE-PATH=$CACHE_PATH" + echo "PUB-CACHE-KEY=$PUB_CACHE_KEY" + echo "PUB-CACHE-PATH=$PUB_CACHE" + } >>"${GITHUB_OUTPUT:-/dev/null}" + + exit 0 +fi + +if [ ! -x "$CACHE_PATH/bin/flutter" ]; then + if [ "$CHANNEL" = "master" ] || [ "$CHANNEL" = "main" ]; then + git clone -b "$CHANNEL" https://github.com/flutter/flutter.git "$CACHE_PATH" + if [ "$VERSION" != "any" ]; then + git config --global --add safe.directory "$CACHE_PATH" + (cd "$CACHE_PATH" && git checkout "$VERSION") + fi + else + archive_url=$(echo "$VERSION_MANIFEST" | jq -r '.archive') + download_archive "$archive_url" "$CACHE_PATH" + fi +fi + +{ + echo "FLUTTER_ROOT=$CACHE_PATH" + echo "PUB_CACHE=$PUB_CACHE" +} >>"${GITHUB_ENV:-/dev/null}" + +{ + echo "$CACHE_PATH/bin" + echo "$CACHE_PATH/bin/cache/dart-sdk/bin" + echo "$PUB_CACHE/bin" +} >>"${GITHUB_PATH:-/dev/null}"