macOS build #39
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.0, 8.1, 8.2" | |
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: "" | |
jobs: | |
gen-jobs: | |
name: Generate jobs | |
runs-on: macos-latest | |
outputs: | |
jobs: ${{ steps.gen-jobs.outputs.jobs }} | |
steps: | |
- name: Generate jobs | |
id: gen-jobs | |
shell: php {0} | |
run: | | |
<?php | |
function arg2arr(string $arg): array | |
{ | |
return array_filter(array_map("trim", explode(',', $arg))); | |
} | |
$flavors = arg2arr(<<<'ARG' | |
${{ github.event.inputs.flavors }} | |
ARG); | |
$archs = arg2arr(<<<'ARG' | |
${{ github.event.inputs.archs }} | |
ARG); | |
$sapis = arg2arr(<<<'ARG' | |
${{ github.event.inputs.sapis }} | |
ARG); | |
$phpVers = arg2arr(<<<'ARG' | |
${{ github.event.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.0', '8.1', '8.2']; | |
} | |
$customLibraries = <<<'ARG' | |
${{ github.event.inputs.customLibraries }} | |
ARG; | |
$customExtensions = <<<'ARG' | |
${{ github.event.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-latest | |
needs: | |
- gen-jobs | |
strategy: | |
max-parallel: 3 | |
fail-fast: false | |
matrix: | |
include: ${{ fromJson(needs.gen-jobs.outputs.jobs) }} | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v3 | |
- name: Restore sources | |
uses: actions/cache/restore@v3 | |
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@v3 | |
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', | |
]; | |
$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/artifact @actions/core @actions/github | |
echo "NODE_PATH=$(npm root --quiet -g)" >> "$GITHUB_ENV" | |
- name: Expose GitHub Runtime | |
if: always() | |
uses: crazy-max/ghaction-github-runtime@v2 | |
- name: Upload artifacts | |
if: always() | |
shell: node {0} | |
run: | | |
const fs = require('fs/promises'); | |
const crypto = require('crypto'); | |
const core = require('@actions/core'); | |
// const github = require('@actions/github'); | |
const artifact = require('@actions/artifact'); | |
const matrix = JSON.parse(` | |
${{ toJson(matrix) }} | |
`) | |
const sapis = matrix.sapis | |
const flavors = matrix.flavors | |
const binFile = { | |
'micro': 'micro.sfx', | |
'micro-cli': 'micro_cli.sfx', | |
'cli': 'php', | |
} | |
async function main() { | |
let client = artifact.create() | |
let artifacts = {}; | |
let results = {}; | |
let versionFile = await fs.readFile('build/versionFile'); | |
let srcHash = crypto.createHash('sha256').update(versionFile).digest('hex'); | |
for (const flavor of flavors) { | |
let dir = `build/out/${flavor}`; | |
for (const sapi of sapis) { | |
let binPath = `${dir}/${binFile[sapi]}`; | |
try { | |
await fs.access(binPath) | |
console.log(`\x1b[37m;File ${binPath} found\x1b[0m;`) | |
let artifactName = `${sapi}_${flavor}_${matrix.phpVer}_${matrix.arch}_${srcHash}`; | |
artifacts[artifactName] = { | |
'file': binPath, | |
'dir': dir, | |
}; | |
let shaSum = crypto.createHash('sha256').update(await fs.readFile(binPath)).digest('hex') | |
console.log(`\x1b[37m;File ${binPath} sha256: ${shaSum}\x1b[0m;`) | |
await fs.writeFile(`${dir}/sha256sums.txt`, `${shaSum} ${binFile[sapi]}\n`, { flag: 'a' }) | |
try { | |
shaSum = crypto.createHash('sha256').update(await fs.readFile(`${binPath}.dwarf`)).digest('hex') | |
console.log(`\x1b[37m;File ${binPath}.dwarf sha256: ${shaSum}\x1b[0m;`) | |
await fs.writeFile(`${dir}/sha256sums.txt`, `${shaSum} ${binFile[sapi]}.dwarf\n`, { flag: 'a' }) | |
} catch (error) { | |
// pass | |
} | |
} catch (error) { | |
console.log(`\x1b[30m;File ${binPath} not found\x1b[0m;`) | |
} | |
} | |
} | |
for (const [name, info] of Object.entries(artifacts)) { | |
let fileList = [ | |
`${info.dir}/sha256sums.txt`, | |
info.file, | |
]; | |
try { | |
await fs.access(`${info.file}.dwarf`); | |
fileList.push(`${info.file}.dwarf`); | |
} catch (error) { | |
// pass | |
} | |
try { | |
for (const file of await fs.readdir(`${info.dir}/licenses`)) { | |
fileList.push(`${info.dir}/licenses/${file}`); | |
} | |
} catch (error) { | |
console.log(`Directory ${info.dir}/licenses not found`); | |
} | |
fileList.push(`${info.dir}/versionFile`); | |
try { | |
console.log(`Uploading artifact ${name}`); | |
let uploadResponse = client.uploadArtifact(name, fileList, info.dir); | |
results[name] = uploadResponse; | |
} catch (error) { | |
core.setFailed(error.message); | |
} | |
} | |
for (const [name, result] of Object.entries(results)) { | |
let res = await result; | |
console.log(`Artifact ${name} uploaded: ${res.artifactName}`); | |
} | |
} | |
main().catch(err => core.setFailed(err.message)); | |