Yoga Layout WASM #113
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Yoga Layout WASM | |
| on: | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| inputs: | |
| dry_run: | |
| description: 'Dry run (build only, no release)' | |
| type: boolean | |
| default: true | |
| force: | |
| description: 'Force rebuild (ignore cache)' | |
| type: boolean | |
| default: false | |
| build_mode: | |
| description: 'Build mode' | |
| type: choice | |
| options: | |
| - prod | |
| - dev | |
| default: prod | |
| workflow_call: | |
| inputs: | |
| dry_run: | |
| type: boolean | |
| default: true | |
| force: | |
| type: boolean | |
| default: false | |
| build_mode: | |
| type: string | |
| default: prod | |
| permissions: | |
| contents: read | |
| jobs: | |
| build: | |
| permissions: | |
| contents: read | |
| runs-on: macos-14 | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| persist-credentials: false | |
| - name: Load tool versions from external-tools.json | |
| id: tool-versions | |
| run: | | |
| NODE_VERSION=$(jq -r '.tools.node.versions.recommendedVersion' packages/build-infra/external-tools.json) | |
| YOGA_VERSION=$(jq -r '.sources.yoga.version' packages/yoga-layout-builder/package.json) | |
| echo "node-version=$NODE_VERSION" >> $GITHUB_OUTPUT | |
| echo "yoga-version=$YOGA_VERSION" >> $GITHUB_OUTPUT | |
| echo "Loaded Node.js: $NODE_VERSION, Yoga Layout: $YOGA_VERSION" | |
| - name: Setup Node.js | |
| uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 | |
| with: | |
| node-version: ${{ steps.tool-versions.outputs.node-version }} | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 | |
| # Note: version is specified in package.json packageManager field, not here | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Set build mode | |
| id: build-mode | |
| env: | |
| INPUT_BUILD_MODE: ${{ inputs.build_mode }} | |
| run: | | |
| # Sanitize input - only allow 'prod' or 'dev' | |
| if [ "$INPUT_BUILD_MODE" = "dev" ]; then | |
| BUILD_MODE="dev" | |
| else | |
| BUILD_MODE="prod" | |
| fi | |
| echo "mode=$BUILD_MODE" >> $GITHUB_OUTPUT | |
| echo "Build mode: $BUILD_MODE" | |
| - name: Setup Emscripten cache | |
| uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 | |
| with: | |
| path: ~/emsdk | |
| key: emsdk-${{ runner.os }}-4.0.20 | |
| - name: Setup Emscripten | |
| run: | | |
| # Load version from external-tools.json (single source of truth) | |
| EMSCRIPTEN_VERSION=$(node packages/build-infra/scripts/get-tool-version.mjs emscripten emsdk --package-root packages/yoga-layout-builder) | |
| if [ ! -d ~/emsdk ]; then | |
| git clone https://github.com/emscripten-core/emsdk.git ~/emsdk | |
| cd ~/emsdk | |
| ./emsdk install ${EMSCRIPTEN_VERSION} | |
| fi | |
| cd ~/emsdk | |
| ./emsdk activate ${EMSCRIPTEN_VERSION} | |
| source ./emsdk_env.sh | |
| echo "Emscripten version: ${EMSCRIPTEN_VERSION}" | |
| emcc --version | |
| - name: Generate Yoga cache key | |
| id: cache-key | |
| run: | | |
| # Per-checkpoint cumulative hashing (like node-smol) | |
| # Each checkpoint includes its dependencies (cumulative) | |
| # Helper to hash directory contents | |
| hash_dir() { | |
| local dir=$1 | |
| if [ -d "$dir" ]; then | |
| find "$dir" -type f -name "*.mjs" 2>/dev/null | sort | xargs shasum -a 256 2>/dev/null | shasum -a 256 | cut -d' ' -f1 || echo "" | |
| else | |
| echo "" | |
| fi | |
| } | |
| # Cache version - bump to force-invalidate all caches | |
| CACHE_VERSION="v2" | |
| # Common scripts (used by all checkpoints) | |
| COMMON=$(hash_dir packages/yoga-layout-builder/scripts/common) | |
| PACKAGE_JSON=$(shasum -a 256 packages/yoga-layout-builder/package.json | cut -d' ' -f1) | |
| BUILD_MJS=$(shasum -a 256 packages/yoga-layout-builder/scripts/build.mjs | cut -d' ' -f1) | |
| # source-cloned: cache-version + common + source-cloned + build.mjs + package.json | |
| SOURCE_CLONED_DIR=$(hash_dir packages/yoga-layout-builder/scripts/source-cloned) | |
| SOURCE_CLONED_HASH=$(echo "${CACHE_VERSION}${COMMON}${SOURCE_CLONED_DIR}${BUILD_MJS}${PACKAGE_JSON}" | shasum -a 256 | cut -d' ' -f1) | |
| # source-configured: source-cloned + source-configured | |
| SOURCE_CONFIGURED_DIR=$(hash_dir packages/yoga-layout-builder/scripts/source-configured) | |
| SOURCE_CONFIGURED_HASH=$(echo "${SOURCE_CLONED_HASH}${SOURCE_CONFIGURED_DIR}" | shasum -a 256 | cut -d' ' -f1) | |
| # wasm-compiled: source-configured + wasm-compiled | |
| WASM_COMPILED_DIR=$(hash_dir packages/yoga-layout-builder/scripts/wasm-compiled) | |
| WASM_COMPILED_HASH=$(echo "${SOURCE_CONFIGURED_HASH}${WASM_COMPILED_DIR}" | shasum -a 256 | cut -d' ' -f1) | |
| # wasm-released: wasm-compiled + wasm-released | |
| WASM_RELEASE_DIR=$(hash_dir packages/yoga-layout-builder/scripts/wasm-released) | |
| WASM_RELEASE_HASH=$(echo "${WASM_COMPILED_HASH}${WASM_RELEASE_DIR}" | shasum -a 256 | cut -d' ' -f1) | |
| # wasm-optimized: wasm-released + wasm-optimized | |
| WASM_OPTIMIZED_DIR=$(hash_dir packages/yoga-layout-builder/scripts/wasm-optimized) | |
| WASM_OPTIMIZED_HASH=$(echo "${WASM_RELEASE_HASH}${WASM_OPTIMIZED_DIR}" | shasum -a 256 | cut -d' ' -f1) | |
| # wasm-synced: wasm-optimized + wasm-synced + wasm-sync-wrapper.mjs (shared infra) | |
| WASM_SYNC_DIR=$(hash_dir packages/yoga-layout-builder/scripts/wasm-synced) | |
| WASM_SYNC_WRAPPER=$(shasum -a 256 packages/build-infra/wasm-synced/wasm-sync-wrapper.mjs | cut -d' ' -f1) | |
| WASM_SYNC_HASH=$(echo "${WASM_OPTIMIZED_HASH}${WASM_SYNC_DIR}${WASM_SYNC_WRAPPER}" | shasum -a 256 | cut -d' ' -f1) | |
| # finalized: wasm-synced + finalized (most complete hash) | |
| FINAL_DIR=$(hash_dir packages/yoga-layout-builder/scripts/finalized) | |
| FINAL_HASH=$(echo "${WASM_SYNC_HASH}${FINAL_DIR}" | shasum -a 256 | cut -d' ' -f1) | |
| echo "cache_version=${CACHE_VERSION}" >> $GITHUB_OUTPUT | |
| echo "source_cloned_hash=${SOURCE_CLONED_HASH}" >> $GITHUB_OUTPUT | |
| echo "wasm_final_hash=${FINAL_HASH}" >> $GITHUB_OUTPUT | |
| - name: Restore Yoga checkpoint cache | |
| uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 | |
| id: yoga-checkpoint-cache | |
| if: ${{ !inputs.force }} | |
| with: | |
| path: | | |
| packages/yoga-layout-builder/build/shared/checkpoints | |
| packages/yoga-layout-builder/build/${{ steps.build-mode.outputs.mode }}/checkpoints | |
| key: yoga-checkpoints-${{ steps.cache-key.outputs.cache_version }}-v${{ steps.tool-versions.outputs.yoga-version }}-${{ runner.os }}-${{ steps.build-mode.outputs.mode }}-${{ steps.cache-key.outputs.wasm_final_hash }} | |
| - name: Validate checkpoint cache integrity | |
| id: validate-cache | |
| if: steps.yoga-checkpoint-cache.outputs.cache-hit == 'true' | |
| env: | |
| BUILD_MODE: ${{ steps.build-mode.outputs.mode }} | |
| run: | | |
| echo "Validating cached Yoga Layout checkpoints..." | |
| SHARED_CHECKPOINT_DIR="packages/yoga-layout-builder/build/shared/checkpoints" | |
| MODE_CHECKPOINT_DIR="packages/yoga-layout-builder/build/${BUILD_MODE}/checkpoints" | |
| # Check if required checkpoint files exist | |
| # source-cloned is in shared directory, others are in mode-specific directory | |
| # Note: wasm-optimized.json only exists in prod mode (optimization is skipped in dev mode) | |
| # Check shared checkpoints | |
| if [ ! -f "${SHARED_CHECKPOINT_DIR}/source-cloned.json" ] || \ | |
| [ ! -f "${SHARED_CHECKPOINT_DIR}/source-cloned.tar.gz" ]; then | |
| echo "❌ Shared checkpoint files incomplete" | |
| rm -rf "${SHARED_CHECKPOINT_DIR}" "${MODE_CHECKPOINT_DIR}" | |
| echo "cache_valid=false" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| # Check mode-specific checkpoints | |
| MODE_CHECKPOINTS="source-configured wasm-compiled wasm-released wasm-synced finalized" | |
| if [ "${BUILD_MODE}" = "prod" ]; then | |
| MODE_CHECKPOINTS="source-configured wasm-compiled wasm-released wasm-optimized wasm-synced finalized" | |
| fi | |
| for checkpoint in ${MODE_CHECKPOINTS}; do | |
| if [ ! -f "${MODE_CHECKPOINT_DIR}/${checkpoint}.json" ] || \ | |
| [ ! -f "${MODE_CHECKPOINT_DIR}/${checkpoint}.tar.gz" ]; then | |
| echo "❌ Mode checkpoint files incomplete (missing: ${checkpoint})" | |
| rm -rf "${SHARED_CHECKPOINT_DIR}" "${MODE_CHECKPOINT_DIR}" | |
| echo "cache_valid=false" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| done | |
| # Check tarball integrity for both directories | |
| for checkpoint_dir in "${SHARED_CHECKPOINT_DIR}" "${MODE_CHECKPOINT_DIR}"; do | |
| for tarball in "${checkpoint_dir}"/*.tar.gz; do | |
| if [ -f "$tarball" ]; then | |
| if ! gzip -t "$tarball" 2>/dev/null; then | |
| echo "❌ Corrupted tarball: $(basename "$tarball")" | |
| rm -rf "${SHARED_CHECKPOINT_DIR}" "${MODE_CHECKPOINT_DIR}" | |
| echo "cache_valid=false" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| fi | |
| done | |
| done | |
| echo "✅ Checkpoint cache validation passed" | |
| echo "cache_valid=true" >> $GITHUB_OUTPUT | |
| - name: Set checkpoint chain for build mode | |
| id: checkpoint-chain | |
| env: | |
| BUILD_MODE: ${{ steps.build-mode.outputs.mode }} | |
| run: | | |
| # Dev mode: skip wasm-optimized (optimization is disabled) | |
| # Prod mode: include wasm-optimized | |
| if [ "$BUILD_MODE" = "prod" ]; then | |
| CHAIN="finalized,wasm-synced,wasm-optimized,wasm-released,wasm-compiled,source-configured,source-cloned" | |
| else | |
| CHAIN="finalized,wasm-synced,wasm-released,wasm-compiled,source-configured,source-cloned" | |
| fi | |
| echo "checkpoint_chain=$CHAIN" >> $GITHUB_OUTPUT | |
| echo "Checkpoint chain for $BUILD_MODE mode: $CHAIN" | |
| - name: Restore build output from checkpoint chain | |
| id: restore-checkpoint | |
| uses: ./.github/actions/restore-checkpoint | |
| with: | |
| package-name: 'yoga-layout-builder' | |
| build-mode: ${{ steps.build-mode.outputs.mode }} | |
| checkpoint-chain: ${{ steps.checkpoint-chain.outputs.checkpoint_chain }} | |
| cache-hit: ${{ steps.yoga-checkpoint-cache.outputs.cache-hit }} | |
| cache-valid: ${{ steps.validate-cache.outputs.cache_valid }} | |
| - name: Build Yoga Layout WASM | |
| if: | | |
| (steps.yoga-checkpoint-cache.outputs.cache-hit != 'true' || steps.validate-cache.outputs.cache_valid == 'false') || | |
| steps.restore-checkpoint.outputs.needs-build == 'true' | |
| env: | |
| BUILD_MODE: ${{ steps.build-mode.outputs.mode }} | |
| run: | | |
| source ~/emsdk/emsdk_env.sh | |
| pnpm --filter yoga-layout-builder build --$BUILD_MODE | |
| - name: Validate build output | |
| env: | |
| BUILD_MODE: ${{ steps.build-mode.outputs.mode }} | |
| run: | | |
| echo "Validating Yoga Layout build output..." | |
| if [ ! -f "packages/yoga-layout-builder/build/${BUILD_MODE}/out/Final/yoga.wasm" ]; then | |
| echo "❌ Build failed: WASM file missing" | |
| exit 1 | |
| fi | |
| if [ ! -f "packages/yoga-layout-builder/build/${BUILD_MODE}/out/Final/yoga.mjs" ]; then | |
| echo "❌ Build failed: JS module file missing" | |
| exit 1 | |
| fi | |
| if [ ! -f "packages/yoga-layout-builder/build/${BUILD_MODE}/out/Final/yoga-sync.js" ]; then | |
| echo "❌ Build failed: Sync JS file missing" | |
| exit 1 | |
| fi | |
| WASM_SIZE=$(stat -f%z "packages/yoga-layout-builder/build/${BUILD_MODE}/out/Final/yoga.wasm" 2>/dev/null || stat -c%s "packages/yoga-layout-builder/build/${BUILD_MODE}/out/Final/yoga.wasm") | |
| if [ "$WASM_SIZE" -lt 100000 ]; then | |
| echo "❌ Build failed: WASM file too small ($WASM_SIZE bytes)" | |
| exit 1 | |
| fi | |
| echo "✅ Build validation passed" | |
| ls -lh "packages/yoga-layout-builder/build/${BUILD_MODE}/out/Final/" | |
| - name: Upload Yoga Layout artifacts | |
| uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 | |
| with: | |
| name: yoga-layout-wasm | |
| path: packages/yoga-layout-builder/build/${{ steps.build-mode.outputs.mode }}/out/Final/ | |
| retention-days: 30 | |
| if-no-files-found: error | |
| release: | |
| needs: build | |
| if: | | |
| (github.event_name == 'workflow_dispatch' && !inputs.dry_run) || | |
| (github.event_name == 'release') | |
| runs-on: ubuntu-22.04 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| persist-credentials: false | |
| - name: Load Yoga version from package.json | |
| id: tool-versions | |
| run: | | |
| YOGA_VERSION=$(jq -r '.sources.yoga.version' packages/yoga-layout-builder/package.json) | |
| echo "yoga-version=$YOGA_VERSION" >> $GITHUB_OUTPUT | |
| echo "Loaded Yoga Layout: $YOGA_VERSION" | |
| - name: Download Yoga Layout artifacts | |
| uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 | |
| with: | |
| name: yoga-layout-wasm | |
| path: packages/yoga-layout-builder/build/prod/out/Final/ | |
| - name: Generate version | |
| id: version | |
| run: | | |
| source .github/scripts/generate-version.sh | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Version: $VERSION" | |
| - name: Rename assets with version | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| cd packages/yoga-layout-builder/build/prod/out/Final | |
| mv yoga.wasm yoga-v${VERSION}.wasm | |
| mv yoga.mjs yoga-v${VERSION}.mjs | |
| mv yoga-sync.js yoga-sync-v${VERSION}.js | |
| - name: Generate checksums | |
| run: | | |
| cd packages/yoga-layout-builder/build/prod/out/Final | |
| shasum -a 256 yoga-v*.wasm yoga-v*.mjs yoga-sync-v*.js > checksums.txt | |
| cat checksums.txt | |
| - name: Import GPG key | |
| if: ${{ env.GPG_PRIVATE_KEY != '' }} | |
| env: | |
| GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} | |
| GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} | |
| run: | | |
| if [ -n "$GPG_PRIVATE_KEY" ]; then | |
| echo "$GPG_PRIVATE_KEY" | gpg --batch --import | |
| echo "GPG key imported successfully" | |
| else | |
| echo "⚠️ GPG_PRIVATE_KEY secret not set, skipping signature" | |
| fi | |
| - name: Sign checksums | |
| if: ${{ env.GPG_PRIVATE_KEY != '' }} | |
| env: | |
| GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} | |
| GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} | |
| run: | | |
| if [ -n "$GPG_PRIVATE_KEY" ]; then | |
| cd packages/yoga-layout-builder/build/prod/out/Final | |
| if [ -n "$GPG_PASSPHRASE" ]; then | |
| echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 --detach-sign --armor checksums.txt | |
| else | |
| gpg --batch --yes --detach-sign --armor checksums.txt | |
| fi | |
| echo "✓ Created checksums.txt.asc" | |
| ls -lh checksums.txt.asc | |
| fi | |
| - name: Create GitHub Release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| STEPS_VERSION_OUTPUTS_VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| VERSION="${STEPS_VERSION_OUTPUTS_VERSION}" | |
| TAG="yoga-layout-v${VERSION}" | |
| # Check if release already exists | |
| if gh release view "$TAG" &>/dev/null; then | |
| echo "Release $TAG already exists, uploading assets..." | |
| UPLOAD_ARGS="packages/yoga-layout-builder/build/prod/out/Final/yoga-v${VERSION}.wasm \ | |
| packages/yoga-layout-builder/build/prod/out/Final/yoga-v${VERSION}.mjs \ | |
| packages/yoga-layout-builder/build/prod/out/Final/yoga-sync-v${VERSION}.js \ | |
| packages/yoga-layout-builder/build/prod/out/Final/checksums.txt" | |
| # Add signature if it exists | |
| if [ -f packages/yoga-layout-builder/build/prod/out/Final/checksums.txt.asc ]; then | |
| UPLOAD_ARGS="$UPLOAD_ARGS packages/yoga-layout-builder/build/prod/out/Final/checksums.txt.asc" | |
| fi | |
| gh release upload "$TAG" $UPLOAD_ARGS --clobber | |
| else | |
| echo "Creating new release $TAG..." | |
| gh release create "$TAG" \ | |
| --title "Yoga Layout WASM v${VERSION}" \ | |
| --notes "Yoga Layout v${{ steps.tool-versions.outputs.yoga-version }} compiled to WASM for flexbox layout calculations. | |
| ## Included Files | |
| - \`yoga-v${VERSION}.wasm\` - Yoga Layout WASM binary | |
| - \`yoga-v${VERSION}.mjs\` - ES Module JavaScript bindings | |
| - \`yoga-sync-v${VERSION}.js\` - Synchronous JavaScript wrapper | |
| - \`checksums.txt\` - SHA256 checksums for verification | |
| - \`checksums.txt.asc\` - GPG signature (if available) | |
| ## Download URLs | |
| \`\`\` | |
| https://github.com/SocketDev/socket-btm/releases/download/${TAG}/yoga-v${VERSION}.wasm | |
| https://github.com/SocketDev/socket-btm/releases/download/${TAG}/yoga-v${VERSION}.mjs | |
| https://github.com/SocketDev/socket-btm/releases/download/${TAG}/yoga-sync-v${VERSION}.js | |
| \`\`\` | |
| ## Verification | |
| \`\`\`bash | |
| # Verify checksums | |
| shasum -a 256 -c checksums.txt | |
| # Verify GPG signature (if GPG key is available) | |
| gpg --verify checksums.txt.asc checksums.txt | |
| \`\`\` | |
| ## Usage | |
| \`\`\`javascript | |
| // ES Module (async) | |
| import Yoga from './yoga-v${VERSION}.mjs'; | |
| // Synchronous wrapper | |
| import Yoga from './yoga-sync-v${VERSION}.js'; | |
| \`\`\` | |
| Built from Yoga Layout v${{ steps.tool-versions.outputs.yoga-version }}" \ | |
| packages/yoga-layout-builder/build/prod/out/Final/yoga-v${VERSION}.wasm \ | |
| packages/yoga-layout-builder/build/prod/out/Final/yoga-v${VERSION}.mjs \ | |
| packages/yoga-layout-builder/build/prod/out/Final/yoga-sync-v${VERSION}.js \ | |
| packages/yoga-layout-builder/build/prod/out/Final/checksums.txt \ | |
| $([ -f packages/yoga-layout-builder/build/prod/out/Final/checksums.txt.asc ] && echo "packages/yoga-layout-builder/build/prod/out/Final/checksums.txt.asc" || echo "") | |
| fi |