Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# The emulator e2e needs a self-hosted Apple-silicon runner carrying the
# HarmonyOS toolchain and image; actionlint cannot know custom labels.
self-hosted-runner:
labels:
- harmonyos
67 changes: 67 additions & 0 deletions .github/actions/rust-core/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Set up the shared Rust core
description: >
Clones the shadowsocks-rust checkout this crate path-depends on, installs the
Rust toolchain and restores the cargo cache. Every Rust workflow starts here,
so the core's pinned ref lives in exactly one place.

inputs:
ref:
description: >
shadowsocks-rust tag to build against. Keep in step with the
core/src/main/rust/shadowsocks-rust submodule pin in shadowsocks-android,
so both platforms ship the same core.
required: false
default: v1.23.4
targets:
description: Extra rustup targets, comma-separated.
required: false
default: ''
components:
description: Extra rustup components, comma-separated.
required: false
default: ''

outputs:
ref:
description: The shadowsocks-rust ref that was checked out.
value: ${{ inputs.ref }}

runs:
using: composite
steps:
- name: Check out the shared Rust core
shell: bash
run: |
set -euo pipefail
# Cargo.toml path-depends on ../../../core/... relative to
# native/sslocal-ffi, which is a *sibling* of this checkout — the
# layout this subproject has inside shadowsocks-android.
core_dir="$(dirname "$GITHUB_WORKSPACE")/core/src/main/rust"
mkdir -p "$core_dir"
if [[ ! -d "$core_dir/shadowsocks-rust/.git" ]]; then
git clone --depth 1 --branch "${{ inputs.ref }}" \
https://github.com/shadowsocks/shadowsocks-rust.git \
"$core_dir/shadowsocks-rust"
else
# Self-hosted runners keep the workspace parent between runs.
git -C "$core_dir/shadowsocks-rust" fetch --depth 1 origin \
"refs/tags/${{ inputs.ref }}:refs/tags/${{ inputs.ref }}" -f
git -C "$core_dir/shadowsocks-rust" checkout -f "${{ inputs.ref }}"
fi

- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ inputs.targets }}
components: ${{ inputs.components }}

- name: Cargo cache
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
native/sslocal-ffi/target
# Shared across the Rust workflows: they build the same crate, and a
# cache written by one is a valid restore for the others.
key: cargo-${{ runner.os }}-${{ inputs.ref }}-${{ hashFiles('native/sslocal-ffi/Cargo.lock') }}
restore-keys: cargo-${{ runner.os }}-${{ inputs.ref }}-
89 changes: 0 additions & 89 deletions .github/workflows/ci.yml

This file was deleted.

145 changes: 145 additions & 0 deletions .github/workflows/harmonyos-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
name: HarmonyOS build

# The HAP build and debug signing — what needs the HarmonyOS SDK but neither a
# device nor a GUI. The ArkTS unit tests are *not* here: their runner drives the
# previewer, a GUI component that does not work on Linux (the specs silently do
# not execute), so they live in harmonyos-unit-tests.yml on macOS.
#
# Huawei's DevEco command-line tools are behind an account + region gate
# (docs/hos-emulator-vpn.md §4) and cannot be redistributed, so they are
# streamed from a private S3/R2 bucket populated by
# ci/package-hos-toolchain.sh, authenticated with the repository secrets
# R2_API_TOKEN (a Cloudflare API token) and R2_ENDPOINT. Secrets are not
# exposed to pull requests from forks, which is why this runs on pushes to
# main and on demand, while the host-side workflows gate every PR.
on:
push:
branches: [main]
paths-ignore: ['**.md', 'docs/**']
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
# Where ci/package-hos-toolchain.sh put the bundle. The account lives in the
# R2_ENDPOINT secret, so a bucket name on its own gives nothing away.
R2_BUCKET: shadowsocks
HOS_BUNDLE_PREFIX: harmonyos-6.1.1
CARGO_TERM_COLOR: always

jobs:
build:
name: HAP build, signing and ArkTS unit tests
runs-on: ubuntu-latest
timeout-minutes: 60
if: github.repository == 'shadowsocks/shadowsocks-ohos'
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/rust-core
with:
targets: aarch64-unknown-linux-ohos

# 352 bytes, so this runs even on a cache hit: the archive's sha256 from
# the manifest is the cache key, which means re-uploading a bundle
# invalidates the cache by itself, with nothing to bump by hand.
- name: Resolve the bundle version
env:
R2_API_TOKEN: ${{ secrets.R2_API_TOKEN }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
run: |
set -euo pipefail
source ci/r2-env.sh
sha="$(aws s3 cp --endpoint-url "$R2_ENDPOINT" \
"s3://$R2_BUCKET/$HOS_BUNDLE_PREFIX/manifest.txt" - \
| awk '/hos-tools-linux-x64\.zip/ { print $1 }')"
[[ -n "$sha" ]] || { echo "no hos-tools checksum in the manifest"; exit 1; }
echo "HOS_TOOLS_SHA=$sha" >> "$GITHUB_ENV"

- name: Cache the HarmonyOS toolchain
id: tools-cache
uses: actions/cache@v4
with:
path: ~/hos-tools
key: hos-tools-${{ runner.os }}-${{ env.HOS_TOOLS_SHA }}

# Huawei's zip verbatim, because it holds 19 pairs of paths that differ
# only in case (linux/netfilter headers) — repacking it on a
# case-insensitive filesystem silently drops files. It cannot be streamed
# either: unzip needs to seek, so it lands on disk and is deleted after.
# Only the tools are fetched; the emulator image belongs to the
# self-hosted e2e runner, which has it locally.
- name: Fetch the HarmonyOS toolchain
if: steps.tools-cache.outputs.cache-hit != 'true'
env:
R2_API_TOKEN: ${{ secrets.R2_API_TOKEN }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
run: |
set -euo pipefail
source ci/r2-env.sh
mkdir -p "$HOME/hos-tools"
aws s3 cp --endpoint-url "$R2_ENDPOINT" \
"s3://$R2_BUCKET/$HOS_BUNDLE_PREFIX/hos-tools-linux-x64.zip" \
/tmp/hos-tools.zip
unzip -q /tmp/hos-tools.zip -d "$HOME/hos-tools"
rm -f /tmp/hos-tools.zip

- name: Locate the toolchain
run: |
set -euo pipefail
# The archive keeps the directory name it was packed from.
tools="$(find "$HOME/hos-tools" -maxdepth 1 -mindepth 1 -type d | head -1)"
[[ -x "$tools/bin/hvigorw" ]] || { echo "no hvigorw under $tools"; exit 1; }
{
echo "HOS_TOOLS=$tools"
echo "DEVECO_SDK_HOME=$tools/sdk"
echo "OHOS_SDK_HOME=$tools/sdk/default/openharmony"
echo "OHOS_NDK_HOME=$tools/sdk/default/openharmony/native"
} >> "$GITHUB_ENV"
df -h /

# Small (tens of MB) but fetched from Huawei's registries on every build:
# ohpm's package store and the hvigor plugins hvigorw pulls on first run.
- name: Cache ohpm and hvigor dependencies
uses: actions/cache@v4
with:
path: |
~/.ohpm
~/.hvigor
oh_modules
.hvigor
key: hos-deps-${{ runner.os }}-${{ env.HOS_TOOLS_SHA }}-${{ hashFiles('oh-package-lock.json5', 'oh-package.json5', 'entry/oh-package.json5') }}
restore-keys: hos-deps-${{ runner.os }}-${{ env.HOS_TOOLS_SHA }}-

# restool's libimage_transcoder_shared.so links against libGL, which the
# runner image does not ship — @CompileResource dies without it.
- name: Install the resource compiler's dependency
run: sudo apt-get update -qq && sudo apt-get install -y -qq libgl1

- name: Resolve ohpm dependencies
run: |
"$HOS_TOOLS/bin/ohpm" install

- name: Build the native core
run: native/build-ohos.sh

- name: Build and sign both HAPs
run: |
set -euo pipefail
"$HOS_TOOLS/bin/hvigorw" --no-daemon assembleHap --mode module \
-p product=default -p buildMode=debug
"$HOS_TOOLS/bin/hvigorw" --no-daemon assembleHap --mode module \
-p module=entry@ohosTest -p product=default -p buildMode=debug
native/sign-hap-debug.sh entry/build/default/outputs/default/entry-default-unsigned.hap
native/sign-hap-debug.sh entry/build/default/outputs/ohosTest/entry-ohosTest-unsigned.hap

- name: Upload the signed HAPs
uses: actions/upload-artifact@v4
with:
name: haps
path: entry/build/default/outputs/*/*-signed.hap
if-no-files-found: error
68 changes: 68 additions & 0 deletions .github/workflows/harmonyos-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: HarmonyOS on-device e2e

# The ohosTest suites against a booted HarmonyOS emulator, driven by
# ci/hos-emulator-e2e.sh (build → sign → boot → unlock → install → run).
#
# **Cannot run on a GitHub-hosted runner**: the Emulator binary is arm64-only
# and the image is phone_all_arm, so it needs an Apple-silicon host exposing
# HVF — GitHub's Apple-silicon runners do not support nested virtualization,
# and their Intel runners (which do have HVF) cannot execute an arm64 emulator
# at all. It therefore targets a self-hosted Apple-silicon runner.
#
# To enable it, register a self-hosted runner on an Apple-silicon Mac that has
# the tools and the emulator image installed, label it `harmonyos`, and set the
# repository variables:
#
# HOS_SELF_HOSTED = true
# HOS_TOOLS_PATH = /path/to/command-line-tools
# HOS_IMAGES_PATH = /path/to/image root (the parent of system-image/)
#
# Without them the job is skipped, so pushes are never left queued against an
# offline runner. ci/hos-emulator-e2e.sh is the same entry point either way, so
# the e2e can always be run by hand on any Mac that has both.
on:
push:
branches: [main]
paths-ignore: ['**.md', 'docs/**']
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
CARGO_TERM_COLOR: always

jobs:
emulator-e2e:
name: On-device e2e on the HarmonyOS emulator
runs-on: [self-hosted, macOS, ARM64, harmonyos]
timeout-minutes: 90
if: vars.HOS_SELF_HOSTED == 'true'
steps:
- uses: actions/checkout@v4
# Keeps its checkout and cargo cache between runs, so ssserver below is
# usually a no-op rebuild.
- uses: ./.github/actions/rust-core

- name: Build ssserver for the host side of the tunnel
run: |
core="$(dirname "$GITHUB_WORKSPACE")/core/src/main/rust/shadowsocks-rust"
cargo build --release --manifest-path "$core/Cargo.toml" --bin ssserver
echo "SSSERVER=$core/target/release/ssserver" >> "$GITHUB_ENV"

- name: On-device e2e
env:
HOS_TOOLS: ${{ vars.HOS_TOOLS_PATH }}
HOS_IMAGES: ${{ vars.HOS_IMAGES_PATH }}
# Its own instance, so a developer's emulator on the same machine is
# left alone.
HOS_INSTANCE: ss_ci
run: ci/hos-emulator-e2e.sh

- name: Emulator log
if: failure()
run: tail -200 "$HOME/.Huawei/Emulator/deployed/ss_ci/Log/"*.log 2>/dev/null || true
Loading
Loading