macOS build #392
This file contains 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: macOS build | |
on: | |
workflow_dispatch: | |
inputs: | |
flavors: | |
description: "flavors, comma splited, empty for 'min,lite,max-swow', available: min, lite, max[-eventengine, like swow/swoole/libev], custom" | |
required: false | |
default: "" | |
archs: | |
description: "archs, comma splited, empty for all, available: x86_64, arm64" | |
required: false | |
default: "" | |
sapis: | |
description: "SAPIs, comma splited, empty for all, available: micro, micro-cli, cli" | |
required: false | |
default: "" | |
phpVers: | |
description: "PHP versions, empty for all, available: 8.1, 8.2, 8.3" | |
required: false | |
default: "" | |
customExtensions: | |
description: "custom extensions, used for custom flavor build" | |
required: false | |
default: "" | |
customLibraries: | |
description: "custom libraries, used for custom flavor build" | |
required: false | |
default: "" | |
uploadRelease: | |
description: "upload binaries to releases, otherwise only upload via artifact" | |
required: false | |
type: boolean | |
default: false | |
workflow_call: | |
inputs: | |
flavors: | |
description: "flavors, comma splited, empty for 'min,lite,max-swow', available: min, lite, max[-eventengine, like swow/swoole/libev], custom" | |
required: false | |
type: string | |
default: "" | |
archs: | |
description: "archs, comma splited, empty for all, available: x86_64, arm64" | |
required: false | |
type: string | |
default: "" | |
sapis: | |
description: "SAPIs, comma splited, empty for all, available: micro, micro-cli, cli" | |
required: false | |
type: string | |
default: "" | |
phpVers: | |
description: "PHP versions, empty for all, available: 8.1, 8.2, 8.3" | |
required: false | |
type: string | |
default: "" | |
customExtensions: | |
description: "custom extensions, used for custom flavor build" | |
required: false | |
type: string | |
default: "" | |
customLibraries: | |
description: "custom libraries, used for custom flavor build" | |
required: false | |
type: string | |
default: "" | |
uploadRelease: | |
description: "upload binaries to releases, otherwise only upload via artifact" | |
required: false | |
type: boolean | |
default: false | |
workflowRepo: | |
description: "repository of workflow to call" | |
required: true | |
type: string | |
workflowRev: | |
description: "revision of workflow to call" | |
required: true | |
type: string | |
jobs: | |
gen-jobs: | |
name: Generate jobs | |
runs-on: macos-13 | |
outputs: | |
jobs: ${{ steps.gen-jobs.outputs.jobs }} | |
permissions: | |
contents: write | |
steps: | |
- name: Generate jobs | |
id: gen-jobs | |
shell: php {0} | |
run: | | |
<?php | |
set_error_handler(function ($severity, $message, $file, $line) { | |
throw new ErrorException($message, 0, $severity, $file, $line); | |
}); | |
if (${{ inputs.uploadRelease }}) { | |
# create github release | |
echo "create release\n"; | |
$tagName = date('Ymd') . '-macos-${{ github.run_id }}'; | |
$context = stream_context_create([ | |
'http' => [ | |
'method' => 'POST', | |
'header' => implode("\r\n", [ | |
'Content-Type: application/json', | |
'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}', | |
'User-Agent: file_get_contents/0', | |
]), | |
'content' => json_encode([ | |
'tag_name' => $tagName, | |
'name' => $tagName, | |
'body' => "automatic release\n\nworkflow run: [${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})", | |
#'draft' => true, | |
#'prerelease' => true, | |
]), | |
], | |
]); | |
file_get_contents('https://api.github.com/repos/${{ github.repository }}/releases', context: $context); | |
} | |
function arg2arr(string $arg): array | |
{ | |
return array_filter(array_map("trim", explode(',', $arg))); | |
} | |
$flavors = arg2arr(<<<'ARG' | |
${{ inputs.flavors }} | |
ARG); | |
$archs = arg2arr(<<<'ARG' | |
${{ inputs.archs }} | |
ARG); | |
$sapis = arg2arr(<<<'ARG' | |
${{ inputs.sapis }} | |
ARG); | |
$phpVers = arg2arr(<<<'ARG' | |
${{ inputs.phpVers }} | |
ARG); | |
if (!$flavors) { | |
$flavors = ['min', 'lite', 'max-swow']; | |
} | |
if (!$archs) { | |
$archs = ['x86_64', 'arm64']; | |
} | |
if (!$sapis) { | |
$sapis = ['micro', 'micro-cli', 'cli']; | |
} | |
if (!$phpVers) { | |
$phpVers = ['8.1', '8.2', '8.3']; | |
} | |
$customLibraries = <<<'ARG' | |
${{ inputs.customLibraries }} | |
ARG; | |
$customExtensions = <<<'ARG' | |
${{ inputs.customExtensions }} | |
ARG; | |
$customLibraries = trim($customLibraries); | |
$customExtensions = trim($customExtensions); | |
foreach ($archs as $arch) { | |
foreach ($phpVers as $phpVer) { | |
$job = [ | |
'flavors' => $flavors, | |
'customLibraries' => $customLibraries, | |
'customExtensions' => $customExtensions, | |
'arch' => $arch, | |
'sapis' => $sapis, | |
'phpVer' => $phpVer, | |
]; | |
$jobs[] = $job; | |
} | |
} | |
if (!$jobs) { | |
echo "no jobs generated\n"; | |
exit(1); | |
} | |
$json = json_encode($jobs); | |
file_put_contents(getenv('GITHUB_OUTPUT'), "jobs=$json"); | |
# $jsonDebug = <<<'JSON' | |
# [{ | |
# "flavors": [ | |
# "custom" | |
# ], | |
# "customLibraries": "", | |
# "customExtensions": "swow", | |
# "arch": "arm64", | |
# "sapis": [ | |
# "micro" | |
# ], | |
# "phpVer": "8.2" | |
# }] | |
# JSON; | |
# $json = json_encode(json_decode($jsonDebug, true)); | |
# file_put_contents(getenv('GITHUB_OUTPUT'), "jobs=$json"); | |
build: | |
name: ${{ matrix.phpVer }} ${{ matrix.arch }} ${{ toJson(matrix.flavors) }} | |
runs-on: macos-13 | |
needs: | |
- gen-jobs | |
permissions: | |
contents: write | |
strategy: | |
max-parallel: 3 | |
fail-fast: false | |
matrix: | |
include: ${{ fromJson(needs.gen-jobs.outputs.jobs) }} | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v4 | |
with: | |
repository: ${{ inputs.workflowRepo || github.repository }} | |
ref: ${{ inputs.workflowRev || github.sha }} | |
- name: Restore sources | |
uses: actions/cache/restore@v4 | |
id: cache-restore | |
with: | |
path: | | |
build/downloads | |
#build/src | |
build/versionFile | |
key: ${{ runner.os }}-build-v1-${{ matrix.phpVer }}-${{ hashFiles('/versionFile') }} | |
restore-keys: | | |
${{ runner.os }}-build-v1-${{ matrix.phpVer }}- | |
- name: Prepare tools and sources | |
id: prepare | |
run: | | |
set -x | |
export GITHUB_TOKEN="${{ secrets.GITHUB_TOKEN }}" | |
mkdir -p build | |
cd build | |
brew install bison re2c automake autoconf | |
brew link automake | |
echo "PATH=/usr/local/opt/bison/bin:/usr/local/opt/re2c/bin:$PATH" >> "$GITHUB_ENV" | |
php ../fetch_source.php \ | |
"" \ | |
"" \ | |
"--phpVer=${{ matrix.phpVer }}" \ | |
"--versionFile=./versionFile" | |
matched_key="${{ steps.cache-restore.outputs.cache-matched-key }}" | |
src_hash="$(shasum -a 256 ./versionFile | head -c 64)" | |
if [ "$matched_key" != "${matched_key##*${src_hash}}" ] | |
then | |
echo "srcHash=" >> "$GITHUB_OUTPUT" | |
else | |
echo "srcHash=${src_hash}" >> "$GITHUB_OUTPUT" | |
fi | |
- name: Save sources | |
uses: actions/cache/save@v4 | |
if: steps.prepare.outputs.srcHash != '' | |
with: | |
path: | | |
build/downloads | |
#build/src | |
build/versionFile | |
key: ${{ runner.os }}-build-v1-${{ matrix.phpVer }}-${{ steps.prepare.outputs.srcHash }} | |
- name: Make build commands | |
shell: php {0} | |
run: | | |
<?php | |
$matrix = <<<'EOF' | |
${{ toJson(matrix) }} | |
EOF; | |
$matrix = json_decode($matrix, true); | |
$customLibraries = $matrix['customLibraries']; | |
$customExtensions = $matrix['customExtensions']; | |
$commands = []; | |
$output = fopen('/tmp/build.sh', 'w'); | |
$writesh = function (...$args) use ($output) { | |
fwrite($output, sprintf(...$args)); | |
fwrite(STDOUT, sprintf(...$args)); | |
}; | |
$writesh(<<<BASH | |
#!/bin/sh | |
#set -x | |
# cached in github actions | |
# echo "::group::Prepare source" | |
# # prepare source for specified php version | |
# # you may select only libraries and extensions you will use | |
# php ../fetch_source.php \ | |
# "" \ | |
# "" \ | |
# --phpVer={$matrix['phpVer']} \ | |
# --versionFile=./versionFile | |
# ret="\$?" | |
# echo "::endgroup::" | |
# if [ ! "\$ret" = "0" ] | |
# then | |
# echo "::error::fetch source failed" | |
# exit 1 | |
# fi | |
echo "::group::Show versions" | |
cat ./versionFile | |
sha256sum ./versionFile | |
echo "::endgroup::" | |
BASH); | |
$maxLibraries = [ | |
// compression | |
'zlib','zstd','bzip2','xz','brotli','libzip', | |
// curl and its deps | |
'nghttp2','libssh2','curl', | |
'libffi', | |
'openssl', | |
'onig', | |
'libyaml', | |
'libxml2', | |
// gd deps | |
'libpng', 'libjpegturbo', 'freetype', 'libwebp', | |
'webview', | |
]; | |
$maxLibraries = implode(',', $maxLibraries); | |
$maxExtensions = [ | |
'iconv', | |
// xml things | |
'dom','xml','simplexml','xmlwriter','xmlreader', | |
'opcache', | |
'bcmath', | |
'phar','zip','zlib','bz2','zstd', | |
'mysqlnd','mysqli','pdo','pdo_mysql', | |
'mbstring','mbregex', | |
'session', | |
'ctype', | |
'fileinfo', | |
'filter', | |
'tokenizer', | |
'curl', | |
'ffi', | |
'redis', | |
'sockets', | |
'openssl', | |
'yaml', | |
'posix','pcntl', | |
'sysvshm','sysvsem','sysvmsg', | |
'gd', | |
]; | |
$maxExtensions = implode(',', $maxExtensions); | |
foreach ($matrix['flavors'] as $flavor) { | |
$libraries = match ($flavor) { | |
'min' => 'libffi', | |
'lite' => 'zstd,zlib,libffi,libzip,bzip2,xz,onig', | |
'max', 'max-swow' => $maxLibraries, | |
'max-libev' => "{$maxLibraries},libev", | |
'max-swoole' => "{$maxLibraries},libstdc++", | |
'custom' => $customLibraries, | |
}; | |
$extensions = match ($flavor) { | |
'min' => 'posix,pcntl,ffi,filter,tokenizer,ctype', | |
'lite' => 'opcache,posix,pcntl,ffi,filter,tokenizer,ctype,iconv,mbstring,mbregex,sockets,zip,zstd,zlib,bz2,phar,fileinfo', | |
'max' => $maxExtensions, | |
'max-swow' => "{$maxExtensions},swow", | |
'max-swoole' => "{$maxExtensions},swoole", | |
'max-libev' => "{$maxExtensions},libev", | |
'custom' => $customExtensions, | |
}; | |
$writesh("\n\n"); | |
$writesh(<<<BASH | |
# ----- "$flavor" flavor ----- | |
echo "::group::Build $flavor libs" | |
# rebuild libs for this flavor to avoid cache | |
php ../build_libs.php \ | |
"$libraries" \ | |
"--arch={$matrix['arch']}" \ | |
"--fresh" | |
ret="\$?" | |
echo "::endgroup::" | |
if [ ! "\$ret" = "0" ] | |
then | |
echo "::error::failed build lib for $flavor" | |
else | |
BASH); | |
foreach ($matrix['sapis'] as $sapi) { | |
$command = match ($sapi) { | |
'micro' => 'build_micro.php', | |
'micro-cli' => 'build_micro.php --fakeCli', | |
'cli' => 'build_cli.php', | |
}; | |
$targetBin = match ($sapi) { | |
'micro' => './src/php-src/sapi/micro/micro.sfx', | |
'micro-cli' => './src/php-src/sapi/micro/micro.sfx', | |
'cli' => './src/php-src/sapi/cli/php', | |
}; | |
$binName = match ($sapi) { | |
'micro' => "micro.sfx", | |
'micro-cli' => "micro_cli.sfx", | |
'cli' => "php", | |
}; | |
// $writesh("\n"); | |
// $writesh("# cleam php build dir\n"); | |
$writesh("\n\n"); | |
$writesh(<<<BASH | |
echo "::group::Build $flavor $sapi" | |
# rm php_micro.lo to avoid cache | |
rm -f src/php-src/sapi/micro/php_micro.lo | |
# $sapi | |
php ../$command \ | |
"$libraries" \ | |
"$extensions" \ | |
"--arch={$matrix['arch']}" | |
ret="\$?" | |
echo "::endgroup::" | |
if [ ! "\$ret" = "0" ] | |
then | |
echo "::error::failed build $flavor $sapi" | |
else | |
# copy the built bin out | |
mkdir -p ./out/{$flavor} | |
cp $targetBin ./out/{$flavor}/$binName | |
cp $targetBin.dwarf ./out/{$flavor}/$binName.dwarf | |
fi | |
BASH); | |
} | |
$writesh("\n\n"); | |
$writesh(<<<BASH | |
echo "::group::Dump $flavor licenses" | |
# dump licenses | |
php ../dump_licenses.php \ | |
"./out/{$flavor}/licenses" \ | |
"$libraries" \ | |
"$extensions" | |
# copy versionFile | |
cp ./versionFile "./out/{$flavor}/versionFile" | |
echo "::endgroup::" | |
fi | |
BASH); | |
} | |
$writesh("\n"); | |
- name: Build | |
shell: sh {0} | |
working-directory: build | |
run: | | |
set +e | |
. /tmp/build.sh | |
# mkdir -p ./out/min | |
# touch ./out/min/micro.sfx | |
# php ../dump_licenses.php \ | |
# "./out/min/licenses" \ | |
# "libffi" \ | |
# "ffi" | |
# cp ./versionFile "./out/min/versionFile" | |
# - name: Setup node | |
# if: always() | |
# uses: actions/setup-node@v3 | |
# with: | |
# node-version: '16' | |
- name: Prepare node for artifact upload | |
if: always() | |
shell: sh {0} | |
run: | | |
npm i -g \ | |
@actions/core \ | |
@actions/github \ | |
@actions/artifact \ | |
@actions/exec | |
echo "NODE_PATH=$(npm root --quiet -g)" >> "$GITHUB_ENV" | |
- name: Expose GitHub Runtime | |
if: always() | |
uses: crazy-max/ghaction-github-runtime@v3 | |
- name: Upload artifacts | |
if: always() | |
shell: node {0} | |
run: | | |
const context = { | |
matrix: ${{ toJson(matrix) }}, | |
github: ${{ toJson(github) }}, | |
inputs: ${{ toJson(inputs) }}, | |
} | |
const core = require('@actions/core'); | |
const { main } = require('${{ github.workspace }}/.github/workflows/upload.js') | |
main('${{ secrets.GITHUB_TOKEN }}', 'macos', context).catch(err => core.setFailed(err.message)) | |