Merge pull request #2212 from Kazzz-S/0.30.5-mac1

Overhaul the Mac build system for Apple Silicon + Tahoe
This commit is contained in:
Matthias Köfferlein 2025-11-18 19:24:26 +01:00 committed by GitHub
commit f61e8113b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1974 additions and 251 deletions

283
macbuild/KLayoutNightlyBuild.Bash Executable file
View File

@ -0,0 +1,283 @@
#!/bin/bash
#----------------------------------------------------------------------
# File: KLayoutNightlyBuild.Bash
#
# What: The Bash contents for "KLayoutNightlyBuild.app" script bundle
# that invokes 'nightlyBuild.py' for automation.
#
# Last modified: 2025-11-05
#----------------------------------------------------------------------
dryrun="no"
#-------------------------------------------------------------------------
# Tool logging
#-------------------------------------------------------------------------
set -Eeuo pipefail
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
LOG="/tmp/KLayoutNightlyBuild.$(date +%Y%m%d-%H%M%S).log"
{
echo "=== START $(date) ==="
echo "whoami: $(whoami)"
echo "pwd: $(pwd)"
echo "shell: $SHELL"
echo "osvers: $(sw_vers -productVersion)"
echo "ENV (partial):"
env | grep -E '^(PATH|HOME|LANG|LC_|MyEmail)' | sort
} >>"$LOG" 2>&1
BASH_XTRACEFD=9
exec 9>>"$LOG"
set -x
exec >>"$LOG" 2>&1
#-------------------------------------------------------------------------
# Detect and set the OS name.
#-------------------------------------------------------------------------
osvers=$(sw_vers -productVersion)
osname=$(sw_vers -productName)
case "$osvers" in
12.*) osname="Monterey" ;;
13.*) osname="Ventura" ;;
14.*) osname="Sonoma" ;;
15.*) osname="Sequoia" ;;
26.*) osname="Tahoe" ;;
*) osname="Unknown" ;;
esac
#-------------------------------------------------------------------------
# Set PATH
# to use /opt/local/bin/addr2line -> gaddr2line in MacPorts.
#-------------------------------------------------------------------------
export PATH=/opt/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
#-------------------------------------------------------------------------
# Decide PYBIN (priority: env → MacPorts → Homebrew → Anaconda → /usr/bin)
# KLAYOUT_PYBIN or PYBIN
#-------------------------------------------------------------------------
PYBIN_CANDIDATES=()
# launchctl setenv KLAYOUT_PYBIN ... will work
[[ -n "${KLAYOUT_PYBIN:-}" ]] && PYBIN_CANDIDATES+=("$KLAYOUT_PYBIN")
[[ -n "${PYBIN:-}" ]] && PYBIN_CANDIDATES+=("$PYBIN")
# candidates
PYBIN_CANDIDATES+=(
/opt/local/bin/python3 # MacPorts
/usr/local/bin/python3 # Homebrew (Intel)
/opt/homebrew/bin/python3 # Homebrew (Apple Silicon)
/Applications/anaconda3/bin/python3 # Anaconda
/usr/bin/python3 # System
)
PYBIN=""
for p in "${PYBIN_CANDIDATES[@]}"; do
if [[ -n "$p" && -x "$p" ]]; then
PYBIN="$p"
break
fi
done
if [[ -z "$PYBIN" ]]; then
echo "[FATAL] python3 not found. Tried: ${PYBIN_CANDIDATES[*]}"
exit 127
fi
echo "[INFO] Using PYBIN=$PYBIN"
"$PYBIN" -V || true
#-------------------------------------------------------------------------
# Set build parameters
#-------------------------------------------------------------------------
workdir=$HOME/GitWork/klayout
passbox=$HOME/Dropbox/PrivatePassBox
drylog=nightlyBuild-dryrun.log
jobsCSV=nightlyBuild.csv
#-------------------------------------------------------------------------
# Set mailer (postfix) parameters
#
# launchctl setenv MyEmail1 "my_real_email_address_1"
# launchctl setenv MyEmail2 "my_real_email_address_2"
#
# launchctl getenv MyEmail1
# launchctl getenv MyEmail2
#-------------------------------------------------------------------------
if [[ -z "${MyEmail1:-}" || -z "${MyEmail2:-}" ]]; then
if [[ -f "$HOME/.klayout_env" ]]; then
# shellcheck disable=SC1090
source "$HOME/.klayout_env"
fi
fi
# Re-check
MyEmail1_val="${MyEmail1:-}"
MyEmail2_val="${MyEmail2:-}"
if [[ -z "$MyEmail1_val" ]]; then
echo "[FATAL] MyEmail1 is not set. Run: launchctl setenv MyEmail1 'you@example.com'"
exit 64
fi
if [[ -z "$MyEmail2_val" ]]; then
echo "[WARN] MyEmail2 is not set. Cc will be empty."
fi
title="Status of KLayout <nightlyBuild.py>"
addrFrom=$MyEmail1_val
addrTo=$MyEmail1_val
addrCc=$MyEmail2_val
mailer="sendmail"
sleeptime=5
####----------------------------------------------------------------------
#### Initialize
####----------------------------------------------------------------------
function Initialize()
{
cd "$workdir"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] PATH=$PATH PYBIN=$PYBIN" >> "$workdir/nightlyBuild-env.log"
if [ -f "$jobsCSV" ]; then
targetOp="--qttarget=$jobsCSV"
else
targetOp=""
fi
if [ -L ./nightlyBuild.py ]; then
echo "OK! nightlyBuild.py symbolic link exists ..."
else
ln -s ./macbuild/nightlyBuild.py .
fi
if [ "$dryrun" == "yes" ]; then
cmd=( "./nightlyBuild.py" $targetOp --build --pymod --dryrun )
dryout="> $drylog"
else
cmd=( "./nightlyBuild.py" $targetOp --build --pymod --test )
dryout=""
fi
line1="### Initialized Nightly Build ###"
line2=" OS: $osname"
msg1=$(printf "%s\n%s\n%s\n" "$line1" "$line2" "${cmd[*]}")
SendMail "$msg1"
}
####----------------------------------------------------------------------
#### Upload log files
####----------------------------------------------------------------------
function UploadLog()
{
shopt -s nullglob
logfiles=(*.log)
[ ${#logfiles[@]} -gt 0 ] && cp -p "${logfiles[@]}" "$passbox"
shopt -u nullglob
}
####----------------------------------------------------------------------
#### Build
####----------------------------------------------------------------------
function Build()
{
# Prepare build log file
buildlogfile="$workdir/nightlyBuild-$(date +%Y%m%d-%H%M%S).log"
timestamp_start=$(date "+%Y-%m-%d %H:%M:%S")
echo "===== Nightly Build Started at $timestamp_start =====" > $buildlogfile
# Start caffeinate in background to prevent sleep
# -d : Prevent display sleep
# -i : Prevent idle sleep
# -m : Prevent disk sleep
# -s : Prevent system sleep
# -u : Declare user activity (optional)
#caffeinate -dimsu &
caffeinate -imsu &
caff_pid=$!
logmsg1="Started caffeinate (PID=$caff_pid) to prevent sleep during build"
echo "$logmsg1" | tee -a $buildlogfile
# Prepare trap cleanup message
cleanupmsg="Cleaning up caffeinate (PID=$caff_pid)"
# Set trap
trap "echo '$cleanupmsg' | tee -a $buildlogfile; kill $caff_pid 2>/dev/null || true; wait $caff_pid 2>/dev/null || true" INT TERM EXIT
# Run the build and test command
build_test_cmd=( "$PYBIN" "${cmd[@]}" )
echo "Executing: ${build_test_cmd[*]} $dryout" | tee -a "$buildlogfile"
if [ "$dryrun" == "yes" ]; then
"${build_test_cmd[@]}" >"$drylog" 2>&1
else
"${build_test_cmd[@]}"
fi
build_status=$?
# Stop caffeinate
kill "$caff_pid" 2>/dev/null || true
wait "$caff_pid" 2>/dev/null || true
logmsg2="Stopped caffeinate (PID=$caff_pid)"
echo "$logmsg2" | tee -a $buildlogfile
# Clear trap
trap - INT TERM EXIT
# Build result handling
timestamp_end=$(date "+%Y-%m-%d %H:%M:%S")
echo "===== Nightly Build Ended at $timestamp_end =====" >> $buildlogfile
if [ $build_status -ne 0 ]; then
line1="!!! Failed Nightly Build !!!"
line2=" OS: $osname"
msg2=$(printf "%s\n%s\n%s\n" "$line1" "$line2" "${cmd[*]}")
echo "$msg2" >> $buildlogfile
UploadLog
else
line1="### Succeeded Nightly Build ###"
line2=" OS: $osname"
msg2=$(printf "%s\n%s\n%s\n" "$line1" "$line2" "${cmd[*]}")
echo "$msg2" >> $buildlogfile
if [ "$dryrun" == "yes" ]; then
UploadLog
fi
fi
# Send mail with entire build log
SendMail "$(cat $buildlogfile)"
return $build_status
}
####----------------------------------------------------------------------
#### Send mail
####----------------------------------------------------------------------
function SendMail() {
$mailer -i -t <<EOF
From: $addrFrom
To: $addrTo
Cc: $addrCc
Subject: $title
Content-Type: text/plain; charset=UTF-8
$1
EOF
}
####----------------------------------------------------------------------
#### Sleep
####----------------------------------------------------------------------
function Sleep()
{
sleep $1
# 10 = 10s => 10 seconds
# 5m => 5 minutes
}
####----------------------------------------------------------------------
#### Main
####----------------------------------------------------------------------
Initialize
Build
exit $?
# EOF

View File

@ -1,16 +1,18 @@
Relevant KLayout version: 0.30.2<br> Relevant KLayout version: 0.30.5<br>
Author: Kazzz-S<br> Author: Kazzz-S<br>
Last modified: 2025-05-30<br> Last modified: 2025-11-10<br>
# 1. Introduction # 1. Introduction
This directory **`macbuild`** contains various files required for building KLayout (http://www.klayout.de/) version 0.30.2 or later for different 64-bit macOS, including: This directory **`macbuild`** contains various files required for building KLayout (http://www.klayout.de/) version 0.30.5 or later for different 64-bit macOS, including:
* Tahoe (26.x) : really experimental (on M4 Mac Mini)
* Sequoia (15.x) : the primary development environment * Sequoia (15.x) : the primary development environment
* Sonoma (14.x) : experimental * Sonoma (14.x) : experimental
* Ventura (13.x) : -- ditto --
Building KLayout for the previous operating systems listed below has been discontinued.<br> Building KLayout for the previous operating systems listed below has been discontinued.<br>
Pre-built DMG packages are also not provided.<br> Pre-built DMG packages are also not provided.<br>
* Monterey (12.7.6; the build is still possible, but Homebrew stopped supporting this OS in September 2024) As of today (November 2025), Homebrew classifies macOS Catalina 10.15 - Ventura 13 as Tier 3.
* Ventura (13.7.8; the build would still be possible)
* Monterey (12.7.6; the build would still be possible)
* Big Sur (11.7.10) * Big Sur (11.7.10)
* Catalina (10.15.7) * Catalina (10.15.7)
* Mojave (10.14) * Mojave (10.14)
@ -19,8 +21,9 @@ Pre-built DMG packages are also not provided.<br>
* El Capitan (10.11) * El Capitan (10.11)
Throughout this document, the primary target machine is **Intel x86_64** with **macOS Sequoia**.<br> Throughout this document, the primary target machine is **Intel x86_64** with **macOS Sequoia**.<br>
All Apple (M1|M2|M3|M4) chips are still untested, as the author does not own an (M1|M2|M3|M4) Mac.<br> The author recently acquired an M4 Mac Mini and is attempting to build a native ARM64 version in the Tahoe environment **experimentally**.<br>
However, some kind volunteers told me they successfully built on an Apple silicon machine.<br> Therefore, this document does not include detailed build procedures for Apple Silicon environments.<br>
However, they are essentially the same as for an Intel Mac.
# 2. Qt Frameworks # 2. Qt Frameworks
@ -34,18 +37,17 @@ If you prefer "Qt6" from **Homebrew** (https://brew.sh/), which is usually locat
/usr/local/opt/qt@6/ /usr/local/opt/qt@6/
``` ```
You can also choose "Qt5" from Anaconda3 (https://www.anaconda.com/), which is usually located under: You can also choose "Qt6" from Anaconda3 (https://www.anaconda.com/), but its setup is a little complicated in this release.
``` First, install Anaconda3 under /Applications. <br>
$HOME/opt/anaconda3/pkgs/qt-{version} If you have installed Anaconda3 under $HOME/opt/anaconda3/ (or other location), make a symbolic link:
```
If you have installed Anaconda3 under $HOME/opt/anaconda3/, make a symbolic link:
``` ```
/Applications/anaconda3/ ---> $HOME/opt/anaconda3/ /Applications/anaconda3/ ---> $HOME/opt/anaconda3/
``` ```
Then, follow the instructions in Section 6F.
The migration work to "Qt6" is ongoing. You can try to use it; however, you might encounter some build or runtime errors.<br> The migration work to "Qt6" is ongoing. You can try to use it; however, you might encounter some build or runtime errors.<br>
If you use **Homebrew** to build KLayout >= 0.29.0, you need "Qt6" to address [the compilation issue](https://github.com/KLayout/klayout/issues/1599).<br> If you use **Homebrew** to build KLayout >= 0.29.0, you need "Qt6" to address [the compilation issue](https://github.com/KLayout/klayout/issues/1599).<br>
I have also tried migrating to "Python 3.12.x" (earlier, Python 3.11.x) in this version. I have also tried migrating to "Python 3.13.x" (earlier, Python 3.12.x) in this version.
# 3. Script language support: Ruby and Python # 3. Script language support: Ruby and Python
@ -57,8 +59,8 @@ You need to have the followings:
* The latest Xcode and command-line tool kit compliant with each OS * The latest Xcode and command-line tool kit compliant with each OS
* https://developer.apple.com/xcode/resources/ * https://developer.apple.com/xcode/resources/
* https://mac.install.guide/commandlinetools/4 * https://mac.install.guide/commandlinetools/4
* Qt5 package from MacPorts or Anaconda3. Qt6, from Homebrew. * Qt5 package from MacPorts. Qt6, from Homebrew or Anaconda3.
* libgit2 form MacPorts, Homebrew,or Anaconda3. * libgit2 form MacPorts, Homebrew, or Anaconda3.
* Optionally, Ruby and Python packages from MacPorts, Homebrew, or Anaconda3 * Optionally, Ruby and Python packages from MacPorts, Homebrew, or Anaconda3
#### For matching versions of Ruby and Python, please also refer to `build4mac_env.py`. #### For matching versions of Ruby and Python, please also refer to `build4mac_env.py`.
@ -70,32 +72,36 @@ The operating system type is detected automatically.
``` ```
----------------------------------------------------------------------------------------------------------- -----------------------------------------------------------------------------------------------------------
<< Usage of 'build4mac.py' >> << Usage of 'build4mac.py' >>
for building KLayout 0.30.2 or later on different Apple macOS platforms. for building KLayout 0.30.5 or later on different Apple macOS platforms.
$ [python] ./build4mac.py $ [python] ./build4mac.py
option & argument : descriptions (refer to 'macbuild/build4mac_env.py' for details) | default value option & argument : descriptions (refer to 'macbuild/build4mac_env.py' for details) | default value
----------------------------------------------------------------------------------------+--------------- ----------------------------------------------------------------------------------------+---------------
[-q|--qt <type>] : case-insensitive type=['Qt5MacPorts', 'Qt5Brew', 'Qt5Ana3', | qt5macports [-q|--qt <type>] : case-insensitive type=['Qt5MacPorts', 'Qt5Brew', 'Qt5Ana3', | qt5macports
: 'Qt6MacPorts', 'Qt6Brew'] | : 'Qt6MacPorts', 'Qt6Brew', 'Qt6Ana3''] |
: Qt5MacPorts: use Qt5 from MacPorts | : Qt5MacPorts: use Qt5 from MacPorts |
: Qt5Brew: use Qt5 from Homebrew | : Qt5Brew: use Qt5 from Homebrew |
: Qt5Ana3: use Qt5 from Anaconda3 | : Qt5Ana3: use Qt5 from Anaconda3 |
: Qt6MacPorts: use Qt6 from MacPorts (*) | : Qt6MacPorts: use Qt6 from MacPorts (*) |
: Qt6Brew: use Qt6 from Homebrew (*) | : Qt6Brew: use Qt6 from Homebrew (*) |
: Qt6Ana3: use Qt6 from Anaconda3 (*) |
: (*) migration to Qt6 is ongoing | : (*) migration to Qt6 is ongoing |
[-r|--ruby <type>] : case-insensitive type=['nil', 'Sys', 'MP33', 'HB34', 'Ana3'] | sys [-r|--ruby <type>] : case-insensitive type=['nil', 'Sys', 'MP34', 'HB34', 'Ana3'] | sys
: nil: don't bind Ruby | : nil: don't bind Ruby |
: Sys: use [Sequoia|Sonoma|Ventura|Monterey]-bundled Ruby 2.6 | : Sys: use [Tahoe|Sequoia|Sonoma]-bundled Ruby 2.6 |
: MP33: use Ruby 3.3 from MacPorts | : MP34: use Ruby 3.4 from MacPorts |
: HB34: use Ruby 3.4 from Homebrew | : HB34: use Ruby 3.4 from Homebrew |
: Ana3: use Ruby 3.2 from Anaconda3 | : Ana3: use Ruby 3.4 from Anaconda3 |
[-p|--python <type>] : case-insensitive type=['nil', 'Sys', 'MP312', 'HB312', 'Ana3', | sys [-p|--python <type>] : case-insensitive type=['nil', 'Sys', 'MP313', 'HB313', 'Ana3', | sys
: 'MP312', 'MP312', |
: 'MP311', 'HB311', 'HBAuto'] | : 'MP311', 'HB311', 'HBAuto'] |
: nil: don't bind Python | : nil: don't bind Python |
: Sys: use [Sequoia|Sonoma|Ventura|Monterey]-bundled Python 3.9 | : Sys: use [Tahoe|Sequoia|Sonoma]-bundled Python 3.9 |
: MP313: use Python 3.13 from MacPorts |
: HB313: use Python 3.13 from Homebrew |
: Ana3: use Python 3.13 from Anaconda3 |
: MP312: use Python 3.12 from MacPorts | : MP312: use Python 3.12 from MacPorts |
: HB312: use Python 3.12 from Homebrew | : HB312: use Python 3.12 from Homebrew |
: Ana3: use Python 3.12 from Anaconda3 |
: MP311: use Python 3.11 from MacPorts | : MP311: use Python 3.11 from MacPorts |
: HB311: use Python 3.11 from Homebrew (+) | : HB311: use Python 3.11 from Homebrew (+) |
: (+) required to provide the legacy pip in HW-*.dmg | : (+) required to provide the legacy pip in HW-*.dmg |
@ -141,7 +147,7 @@ Confirm that you have:
``` ```
As of this writing, the provided Python version is `3.9.6`. As of this writing, the provided Python version is `3.9.6`.
1. Invoke **`build4mac.py`** with the following options: **((Notes))** These options are the default values for Sequoia, Sonoma, and Ventura. 1. Invoke **`build4mac.py`** with the following options: **((Notes))** These options are the default values for Tahoe, Sequoia, and Sonoma.
``` ```
$ cd /where/'build.sh'/exists $ cd /where/'build.sh'/exists
$ ./build4mac.py -q qt5macports -r sys -p sys $ ./build4mac.py -q qt5macports -r sys -p sys
@ -160,21 +166,21 @@ $ ./build4mac.py -q qt5macports -r sys -p sys -y
* "RsysPsys" means that Ruby is 2.6 provided by OS; Python is 3.9 provided by OS. * "RsysPsys" means that Ruby is 2.6 provided by OS; Python is 3.9 provided by OS.
4. Copy/move the generated application bundle **`klayout.app`** to your **`/Applications`** directory for installation. 4. Copy/move the generated application bundle **`klayout.app`** to your **`/Applications`** directory for installation.
### 6B. Fully MacPorts-flavored build with MacPorts Ruby 3.3 and MacPorts Python 3.12 ### 6B. Fully MacPorts-flavored build with MacPorts Ruby 3.4 and MacPorts Python 3.13
0. Install MacPorts, then install Qt5, Ruby 3.3, Python 3.12, and libgit2 by 0. Install MacPorts, then install Qt5, Ruby 3.4, Python 3.13, and libgit2 by
``` ```
$ sudo port install coreutils $ sudo port install coreutils
$ sudo port install findutils $ sudo port install findutils
$ sudo port install qt5 $ sudo port install qt5
$ sudo port install ruby33 $ sudo port install ruby34
$ sudo port install python312 $ sudo port install python313
$ sudo port install py312-pip $ sudo port install py313-pip
$ sudo port install libgit2 $ sudo port install libgit2
``` ```
1. Invoke **`build4mac.py`** with the following options: 1. Invoke **`build4mac.py`** with the following options:
``` ```
$ cd /where/'build.sh'/exists $ cd /where/'build.sh'/exists
$ ./build4mac.py -q qt5macports -r mp33 -p mp312 $ ./build4mac.py -q qt5macports -r mp34 -p mp313
``` ```
2. Confirm successful build (it will take about one hour, depending on your machine spec). 2. Confirm successful build (it will take about one hour, depending on your machine spec).
3. Rerun **`build4mac.py`** with the same options used in 1. PLUS "-Y" to deploy executables and libraries under **`klayout.app`** bundle.<br> 3. Rerun **`build4mac.py`** with the same options used in 1. PLUS "-Y" to deploy executables and libraries under **`klayout.app`** bundle.<br>
@ -182,50 +188,50 @@ $ ./build4mac.py -q qt5macports -r mp33 -p mp312
If you use `--buildPymod` option in Step-1 and Step-3, the KLayout Standalone Python Package (\*.whl) will be built and deployed under **klayout.app/Contents/pymod-dist/**. If you use `--buildPymod` option in Step-1 and Step-3, the KLayout Standalone Python Package (\*.whl) will be built and deployed under **klayout.app/Contents/pymod-dist/**.
``` ```
$ ./build4mac.py -q qt5macports -r mp33 -p mp312 -Y $ ./build4mac.py -q qt5macports -r mp34 -p mp313 -Y
``` ```
The application bundle **`klayout.app`** is located under:<br> The application bundle **`klayout.app`** is located under:<br>
**`LW-qt5MP.pkg.macos-Sequoia-release-Rmp33Pmp312`** directory, where **`LW-qt5MP.pkg.macos-Sequoia-release-Rmp34Pmp313`** directory, where
* "LW-" means this is a lightweight package. * "LW-" means this is a lightweight package.
* "qt5MP" means that Qt5 from MacPorts is used. * "qt5MP" means that Qt5 from MacPorts is used.
* "Rmp33Pmp312" means that Ruby is 3.3 from MacPorts; Python is 3.12 from MacPorts. * "Rmp34Pmp313" means that Ruby is 3.4 from MacPorts; Python is 3.13 from MacPorts.
4. Copy/move the generated application bundle **`klayout.app`** to your **`/Applications`** directory for installation. 4. Copy/move the generated application bundle **`klayout.app`** to your **`/Applications`** directory for installation.
### 6C. Fully Homebrew-flavored build with Homebrew Ruby 3.3 and Homebrew Python 3.12 ### 6C. Fully Homebrew-flavored build with Homebrew Ruby 3.4 and Homebrew Python 3.13
> [!IMPORTANT] > [!IMPORTANT]
> To build KLayout >= 0.29.0, you need "Qt6" >= 6.7.0 to address [the compilation issue](https://github.com/KLayout/klayout/issues/1599).<br> > To build KLayout >= 0.29.0, you need "Qt6" >= 6.7.0 to address [the compilation issue](https://github.com/KLayout/klayout/issues/1599).<br>
0. Install Homebrew, then install Qt6, Ruby 3.4, Python 3.12, and libgit2 by 0. Install Homebrew, then install Qt6, Ruby 3.4, Python 3.13, and libgit2 by
``` ```
$ brew install qt@6 $ brew install qt@6
$ brew install ruby@3.4 $ brew install ruby@3.4
$ brew install python@3.12 $ brew install python@3.13
$ brew install libgit2 $ brew install libgit2
$ cd /where/'build.sh'/exists $ cd /where/'build.sh'/exists
$ cd macbuild $ cd macbuild
$ ./python3HB.py -v 3.12 $ ./python3HB.py -v 3.13
``` ```
1. Invoke **`build4mac.py`** with the following options: 1. Invoke **`build4mac.py`** with the following options:
``` ```
$ cd /where/'build.sh'/exists $ cd /where/'build.sh'/exists
$ ./build4mac.py -q qt6brew -r hb34 -p hb312 $ ./build4mac.py -q qt6brew -r hb34 -p hb313
``` ```
2. Confirm successful build (it will take about one hour, depending on your machine spec). 2. Confirm successful build (it will take about one hour, depending on your machine spec).
3. Rerun **`build4mac.py`** with the same options used in 1. PLUS "-Y" to deploy executables and libraries under **`klayout.app`** bundle.<br> 3. Rerun **`build4mac.py`** with the same options used in 1. PLUS "-Y" to deploy executables and libraries under **`klayout.app`** bundle.<br>
The buddy command-line tools (strm*) will also be deployed under **klayout.app/Contents/Buddy/** in this step.<br> The buddy command-line tools (strm*) will also be deployed under **klayout.app/Contents/Buddy/** in this step.<br>
``` ```
$ ./build4mac.py -q qt6brew -r hb34 -p hb312 -Y $ ./build4mac.py -q qt6brew -r hb34 -p hb313 -Y
``` ```
The application bundle **`klayout.app`** is located under:<br> The application bundle **`klayout.app`** is located under:<br>
**`LW-qt6Brew.pkg.macos-Sequoia-release-Rhb34Phb312`** directory, where **`LW-qt6Brew.pkg.macos-Sequoia-release-Rhb34Phb313`** directory, where
* "LW-" means this is a lightweight package. * "LW-" means this is a lightweight package.
* "qt6Brew" means that Qt6 from Homebrew is used. * "qt6Brew" means that Qt6 from Homebrew is used.
* "Rhb34Phb312" means that Ruby is 3.4 from Homebrew; Python is 3.12 from Homebrew. * "Rhb34Phb313" means that Ruby is 3.4 from Homebrew; Python is 3.13 from Homebrew.
4. Copy/move the generated application bundle **`klayout.app`** to your **`/Applications`** directory for installation. 4. Copy/move the generated application bundle **`klayout.app`** to your **`/Applications`** directory for installation.
> [!WARNING] > [!WARNING]
> We can no longer use the legacy **pip** command with Homebew Python@3.12, and we will get, > We can no longer use the legacy **pip** command with Homebew Python@3.13, and we will get,
> ``` > ```
> error: externally-managed-environment > error: externally-managed-environment
> ``` > ```
@ -306,17 +312,39 @@ $ ./build4mac.py -q qt5macports -r sys -p hb311 -y
* "RsysPhb311" means that Ruby is OS-bundled; Python is 3.11 from Homebrew. * "RsysPhb311" means that Ruby is OS-bundled; Python is 3.11 from Homebrew.
4. Copy/move the generated application bundle **`klayout.app`** to your **`/Applications`** directory for installation. 4. Copy/move the generated application bundle **`klayout.app`** to your **`/Applications`** directory for installation.
### 6F. Fully Anaconda3-flavored build with Anaconda3 Ruby 3.2 and Anaconda3 Python 3.12 ### 6F. Fully Anaconda3-flavored build with Anaconda3 Ruby 3.4 and Anaconda3 Python 3.13
0. Install Anaconda3 (Anaconda3-2024.10-1-MacOSX-x86_64.pkg), then install Ruby 3.2 and libgit2 by 0. Install Anaconda3 (Anaconda3-2025.06-0-MacOSX-x86_64.pkg) under `/Applications` then setup a new virtual environment.
``` ```
$ conda install ruby=3.2.2 1) Create a new env "klayout-qt6" (with stable solver & channels)
$ conda install libgit2=1.6.4 switch solver to libmamba for faster/more stable resolves
$ conda install -n base -y conda-libmamba-solver
$ conda config --set solver libmamba
Create the environment (on this x86_64 machine it will pull osx-64 builds)
$ conda create -n klayout-qt6 python=3.13 -y
$ conda activate klayout-qt6
In this env only, prefer conda-forge strictly (to avoid mixing)
$ conda config --env --add channels conda-forge
$ conda config --env --add channels defaults
$ conda config --env --set channel_priority strict
$ conda install -n base -y conda-libmamba-solver
$ conda config --set solver libmamba
2) Install Qt6 (qt6-main and qt6-multimedia) only from conda-forge
Qt6 core (builds that typically include Designer/UiTools)
$ conda install -y --override-channels -c conda-forge "qt6-main=6.9.3"
$ conda install -y --override-channels -c conda-forge "qt6-multimedia=6.9.3"
3) Additionally, install Ruby and libgit2 only from conda-forge
$ conda install -y --override-channels -c conda-forge "ruby=3.4.7"
$ conda install -y --override-channels -c conda-forge "libgit2=1.9.1"
``` ```
1. Invoke **`build4mac.py`** with the following options: 1. Invoke **`build4mac.py`** with the following options:
``` ```
$ cd /where/'build.sh'/exists $ cd /where/'build.sh'/exists
$ ./build4mac.py -q qt5ana3 -r ana3 -p ana3 $ ./build4mac.py -q qt6ana3 -r ana3 -p ana3
``` ```
2. Confirm successful build (it will take about one hour, depending on your machine spec). 2. Confirm successful build (it will take about one hour, depending on your machine spec).
3. Rerun **`build4mac.py`** with the same options used in 1. PLUS "-Y" to deploy executables and libraries under **`klayout.app`** bundle.<br> 3. Rerun **`build4mac.py`** with the same options used in 1. PLUS "-Y" to deploy executables and libraries under **`klayout.app`** bundle.<br>
@ -324,18 +352,16 @@ $ ./build4mac.py -q qt5ana3 -r ana3 -p ana3
If you use `--buildPymod` option in Step-1 and Step-3, the KLayout Standalone Python Package (\*.whl) will be built and deployed under **klayout.app/Contents/pymod-dist/**. If you use `--buildPymod` option in Step-1 and Step-3, the KLayout Standalone Python Package (\*.whl) will be built and deployed under **klayout.app/Contents/pymod-dist/**.
``` ```
$ ./build4mac.py -q qt5ana3 -r ana3 -p ana3 -Y $ ./build4mac.py -q qt6ana3 -r ana3 -p ana3 -Y
``` ```
The application bundle **`klayout.app`** is located under:<br> The application bundle **`klayout.app`** is located under:<br>
**`LW-qt5Ana3.pkg.macos-Sequoia-release-Rana3Pana3`** directory, where **`LW-qt6Ana3.pkg.macos-Sequoia-release-Rana3Pana3`** directory, where
* "LW-" means this is a lightweight package. * "LW-" means this is a lightweight package.
* "qt5Ana3" means that Qt5 from Anaconda3 is used. * "qt6Ana3" means that Qt6 from Anaconda3 is used.
* "Rana3Pana3" means that Ruby (3.2) is from Anaconda3; Python (3.12) is from Anaconda3. * "Rana3Pana3" means that Ruby (3.4) is from Anaconda3; Python (3.13) is from Anaconda3.
4. Copy/move the generated application bundle **`klayout.app`** to your **`/Applications`** directory for installation. 4. Copy/move the generated application bundle **`klayout.app`** to your **`/Applications`** directory for installation.
5. You may have to set the `PYTHONHOME` environment variable like: 5. Now, you need to create an Automator wrapper app (Launcher) to start the application.
``` See `Anaconda3User-ReadMeFirst.txt` in `Resources/script-bundle-A.zip` for details.
export PYTHONHOME=$HOME/opt/anaconda3
```
### 6G. Other combinations ### 6G. Other combinations
Logically, several module combinations other than 6A through 6F are possible, including `nil` choice.<br> Logically, several module combinations other than 6A through 6F are possible, including `nil` choice.<br>
@ -394,11 +420,11 @@ makeDMG4mac.py -> macbuild/makeDMG4mac.py
2. Invoke **`makeDMG4mac.py`** with -p and -m options, for example, 2. Invoke **`makeDMG4mac.py`** with -p and -m options, for example,
``` ```
$ cd /where/'build.sh'/exists $ cd /where/'build.sh'/exists
$ ./makeDMG4mac.py -p LW-qt5MP.pkg.macos-Sequoia-release-Rmp33Pmp312 -m $ ./makeDMG4mac.py -p LW-qt5MP.pkg.macos-Sequoia-release-Rmp34Pmp313 -m
``` ```
This command will generate the two files below:<br> This command will generate the two files below:<br>
* **`LW-klayout-0.30.2-macOS-Sequoia-1-qt5MP-Rmp33Pmp312.dmg`** ---(1) the main DMG file * **`LW-klayout-0.30.5-macOS-Sequoia-1-qt5MP-Rmp34Pmp313.dmg`** ---(1) the main DMG file
* **`LW-klayout-0.30.2-macOS-Sequoia-1-qt5MP-Rmp33Pmp312.dmg.md5`** ---(2) MD5-value text file * **`LW-klayout-0.30.5-macOS-Sequoia-1-qt5MP-Rmp34Pmp313.dmg.md5`** ---(2) MD5-value text file
# Known issues # Known issues
Because we assume some specific versions of non-OS-standard Ruby and Python, updating Homebrew, MacPorts, or Anaconda3 may cause build- and link errors.<br> Because we assume some specific versions of non-OS-standard Ruby and Python, updating Homebrew, MacPorts, or Anaconda3 may cause build- and link errors.<br>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

View File

@ -1,4 +1,5 @@
[Paths] [Paths]
Plugins = ../PlugIns Prefix = ..
Imports = ../Resources/qml Plugins = PlugIns
Qml2Imports = ../Resources/qml Imports = Resources/qml
Qml2Imports = Resources/qml

View File

@ -4,8 +4,8 @@
#=============================================================================== #===============================================================================
# File: "macbuild/build4mac.py" # File: "macbuild/build4mac.py"
# #
# The top Python script for building KLayout (http://www.klayout.de/index.php) # The top Python script for building KLayout (http://www.klayout.de/index.php).
# version 0.30.2 or later on different Apple Mac OSX platforms. # version 0.30.5 or later on different Apple Mac OSX platforms.
#=============================================================================== #===============================================================================
import sys import sys
import os import os
@ -17,6 +17,7 @@ import platform
import optparse import optparse
import subprocess import subprocess
import pprint import pprint
from pathlib import Path
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
## To import global dictionaries of different modules and utility functions ## To import global dictionaries of different modules and utility functions
@ -25,6 +26,7 @@ mydir = os.path.dirname(os.path.abspath(__file__))
sys.path.append( mydir + "/macbuild" ) sys.path.append( mydir + "/macbuild" )
from build4mac_env import * from build4mac_env import *
from build4mac_util import * from build4mac_util import *
from bundle_qtconf import generate_qtconf, QtConfError
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
## To generate the OS-wise usage strings and the default module set ## To generate the OS-wise usage strings and the default module set
@ -45,32 +47,36 @@ def GenerateUsage(platform):
usage = "\n" usage = "\n"
usage += "-----------------------------------------------------------------------------------------------------------\n" usage += "-----------------------------------------------------------------------------------------------------------\n"
usage += "<< Usage of 'build4mac.py' >>\n" usage += "<< Usage of 'build4mac.py' >>\n"
usage += " for building KLayout 0.30.2 or later on different Apple macOS platforms.\n" usage += " for building KLayout 0.30.5 or later on different Apple macOS platforms.\n"
usage += "\n" usage += "\n"
usage += "$ [python] ./build4mac.py\n" usage += "$ [python] ./build4mac.py\n"
usage += " option & argument : descriptions (refer to 'macbuild/build4mac_env.py' for details) | default value\n" usage += " option & argument : descriptions (refer to 'macbuild/build4mac_env.py' for details) | default value\n"
usage += " ----------------------------------------------------------------------------------------+---------------\n" usage += " ----------------------------------------------------------------------------------------+---------------\n"
usage += " [-q|--qt <type>] : case-insensitive type=['Qt5MacPorts', 'Qt5Brew', 'Qt5Ana3', | %s\n" % myQt56 usage += " [-q|--qt <type>] : case-insensitive type=['Qt5MacPorts', 'Qt5Brew', 'Qt5Ana3', | %s\n" % myQt56
usage += " : 'Qt6MacPorts', 'Qt6Brew'] |\n" usage += " : 'Qt6MacPorts', 'Qt6Brew', 'Qt6Ana3''] |\n"
usage += " : Qt5MacPorts: use Qt5 from MacPorts |\n" usage += " : Qt5MacPorts: use Qt5 from MacPorts |\n"
usage += " : Qt5Brew: use Qt5 from Homebrew |\n" usage += " : Qt5Brew: use Qt5 from Homebrew |\n"
usage += " : Qt5Ana3: use Qt5 from Anaconda3 |\n" usage += " : Qt5Ana3: use Qt5 from Anaconda3 |\n"
usage += " : Qt6MacPorts: use Qt6 from MacPorts (*) |\n" usage += " : Qt6MacPorts: use Qt6 from MacPorts (*) |\n"
usage += " : Qt6Brew: use Qt6 from Homebrew (*) |\n" usage += " : Qt6Brew: use Qt6 from Homebrew (*) |\n"
usage += " : Qt6Ana3: use Qt6 from Anaconda3 (*) |\n"
usage += " : (*) migration to Qt6 is ongoing |\n" usage += " : (*) migration to Qt6 is ongoing |\n"
usage += " [-r|--ruby <type>] : case-insensitive type=['nil', 'Sys', 'MP33', 'HB34', 'Ana3'] | %s\n" % myRuby usage += " [-r|--ruby <type>] : case-insensitive type=['nil', 'Sys', 'MP34', 'HB34', 'Ana3'] | %s\n" % myRuby
usage += " : nil: don't bind Ruby |\n" usage += " : nil: don't bind Ruby |\n"
usage += " : Sys: use [Sequoia|Sonoma|Ventura|Monterey]-bundled Ruby 2.6 |\n" usage += " : Sys: use [Tahoe|Sequoia|Sonoma]-bundled Ruby 2.6 |\n"
usage += " : MP33: use Ruby 3.3 from MacPorts |\n" usage += " : MP34: use Ruby 3.4 from MacPorts |\n"
usage += " : HB34: use Ruby 3.4 from Homebrew |\n" usage += " : HB34: use Ruby 3.4 from Homebrew |\n"
usage += " : Ana3: use Ruby 3.2 from Anaconda3 |\n" usage += " : Ana3: use Ruby 3.4 from Anaconda3 |\n"
usage += " [-p|--python <type>] : case-insensitive type=['nil', 'Sys', 'MP312', 'HB312', 'Ana3', | %s\n" % myPython usage += " [-p|--python <type>] : case-insensitive type=['nil', 'Sys', 'MP313', 'HB313', 'Ana3', | %s\n" % myPython
usage += " : 'MP312', 'MP312', |\n"
usage += " : 'MP311', 'HB311', 'HBAuto'] |\n" usage += " : 'MP311', 'HB311', 'HBAuto'] |\n"
usage += " : nil: don't bind Python |\n" usage += " : nil: don't bind Python |\n"
usage += " : Sys: use [Sequoia|Sonoma|Ventura|Monterey]-bundled Python 3.9 |\n" usage += " : Sys: use [Tahoe|Sequoia|Sonoma]-bundled Python 3.9 |\n"
usage += " : MP313: use Python 3.13 from MacPorts |\n"
usage += " : HB313: use Python 3.13 from Homebrew |\n"
usage += " : Ana3: use Python 3.13 from Anaconda3 |\n"
usage += " : MP312: use Python 3.12 from MacPorts |\n" usage += " : MP312: use Python 3.12 from MacPorts |\n"
usage += " : HB312: use Python 3.12 from Homebrew |\n" usage += " : HB312: use Python 3.12 from Homebrew |\n"
usage += " : Ana3: use Python 3.12 from Anaconda3 |\n"
usage += " : MP311: use Python 3.11 from MacPorts |\n" usage += " : MP311: use Python 3.11 from MacPorts |\n"
usage += " : HB311: use Python 3.11 from Homebrew (+) |\n" usage += " : HB311: use Python 3.11 from Homebrew (+) |\n"
usage += " : (+) required to provide the legacy pip in HW-*.dmg |\n" usage += " : (+) required to provide the legacy pip in HW-*.dmg |\n"
@ -273,15 +279,15 @@ def Parse_CLI_Args(config):
p = optparse.OptionParser(usage=Usage) p = optparse.OptionParser(usage=Usage)
p.add_option( '-q', '--qt', p.add_option( '-q', '--qt',
dest='type_qt', dest='type_qt',
help="Qt type=['Qt5MacPorts', 'Qt5Brew', 'Qt5Ana3', 'Qt6MacPorts', 'Qt6Brew']" ) help="Qt type=['Qt5MacPorts', 'Qt5Brew', 'Qt5Ana3', 'Qt6MacPorts', 'Qt6Brew', 'Qt6Ana3'']" )
p.add_option( '-r', '--ruby', p.add_option( '-r', '--ruby',
dest='type_ruby', dest='type_ruby',
help="Ruby type=['nil', 'Sys', 'MP33', 'HB34', 'Ana3']" ) help="Ruby type=['nil', 'Sys', 'MP34', 'HB34', 'Ana3']" )
p.add_option( '-p', '--python', p.add_option( '-p', '--python',
dest='type_python', dest='type_python',
help="Python type=['nil', 'Sys', 'MP312', 'HB312', 'Ana3', 'MP311', 'HB311', 'HBAuto']" ) help="Python type=['nil', 'Sys', 'MP313', 'HB313', 'Ana3', 'MP312', 'HB312', 'MP311', 'HB311', 'HBAuto']" )
p.add_option( '-P', '--buildPymod', p.add_option( '-P', '--buildPymod',
action='store_true', action='store_true',
@ -381,6 +387,7 @@ def Parse_CLI_Args(config):
candidates['QT5ANA3'] = 'Qt5Ana3' candidates['QT5ANA3'] = 'Qt5Ana3'
candidates['QT6MACPORTS'] = 'Qt6MacPorts' candidates['QT6MACPORTS'] = 'Qt6MacPorts'
candidates['QT6BREW'] = 'Qt6Brew' candidates['QT6BREW'] = 'Qt6Brew'
candidates['QT6ANA3'] = 'Qt6Ana3'
try: try:
ModuleQt = candidates[ opt.type_qt.upper() ] ModuleQt = candidates[ opt.type_qt.upper() ]
except KeyError: except KeyError:
@ -402,6 +409,8 @@ def Parse_CLI_Args(config):
choiceQt56 = 'qt6MP' choiceQt56 = 'qt6MP'
elif ModuleQt == "Qt6Brew": elif ModuleQt == "Qt6Brew":
choiceQt56 = 'qt6Brew' choiceQt56 = 'qt6Brew'
elif ModuleQt == "Qt6Ana3":
choiceQt56 = 'qt6Ana3'
# Check if non-OS-standard (-bundled) script languages (Ruby and Python) are used # Check if non-OS-standard (-bundled) script languages (Ruby and Python) are used
NonOSStdLang = False NonOSStdLang = False
@ -410,7 +419,7 @@ def Parse_CLI_Args(config):
candidates = dict() candidates = dict()
candidates['NIL'] = 'nil' candidates['NIL'] = 'nil'
candidates['SYS'] = 'Sys' candidates['SYS'] = 'Sys'
candidates['MP33'] = 'MP33' candidates['MP34'] = 'MP34'
candidates['HB34'] = 'HB34' candidates['HB34'] = 'HB34'
candidates['ANA3'] = 'Ana3' candidates['ANA3'] = 'Ana3'
try: try:
@ -433,14 +442,17 @@ def Parse_CLI_Args(config):
ModuleRuby = 'RubyVentura' ModuleRuby = 'RubyVentura'
elif Platform == "Monterey": elif Platform == "Monterey":
ModuleRuby = 'RubyMonterey' ModuleRuby = 'RubyMonterey'
elif choiceRuby == "MP33": elif choiceRuby == "MP34":
ModuleRuby = 'Ruby33MacPorts' ModuleRuby = 'Ruby34MacPorts'
NonOSStdLang = True NonOSStdLang = True
elif choiceRuby == "HB34": elif choiceRuby == "HB34":
ModuleRuby = 'Ruby34Brew' ModuleRuby = 'Ruby34Brew'
NonOSStdLang = True NonOSStdLang = True
elif choiceRuby == "Ana3": elif choiceRuby == "Ana3":
ModuleRuby = 'RubyAnaconda3' if choiceQt56 == 'qt5Ana3':
ModuleRuby = 'RubyAnaconda3V5'
else: # 'qt6Ana3'
ModuleRuby = 'RubyAnaconda3V6'
NonOSStdLang = True NonOSStdLang = True
if ModuleRuby == '': if ModuleRuby == '':
print("") print("")
@ -453,9 +465,11 @@ def Parse_CLI_Args(config):
candidates = dict() candidates = dict()
candidates['NIL'] = 'nil' candidates['NIL'] = 'nil'
candidates['SYS'] = 'Sys' candidates['SYS'] = 'Sys'
candidates['MP313'] = 'MP313'
candidates['HB313'] = 'HB313'
candidates['ANA3'] = 'Ana3'
candidates['MP312'] = 'MP312' candidates['MP312'] = 'MP312'
candidates['HB312'] = 'HB312' candidates['HB312'] = 'HB312'
candidates['ANA3'] = 'Ana3'
candidates['MP311'] = 'MP311' candidates['MP311'] = 'MP311'
candidates['HB311'] = 'HB311' candidates['HB311'] = 'HB311'
candidates['HBAUTO'] = 'HBAuto' candidates['HBAUTO'] = 'HBAuto'
@ -485,6 +499,21 @@ def Parse_CLI_Args(config):
elif Platform == "Monterey": elif Platform == "Monterey":
ModulePython = 'PythonMonterey' ModulePython = 'PythonMonterey'
OSPython3FW = MontereyPy3FW OSPython3FW = MontereyPy3FW
elif choicePython == "MP313":
ModulePython = 'Python313MacPorts'
OSPython3FW = None
NonOSStdLang = True
elif choicePython == "HB313":
ModulePython = 'Python313Brew'
OSPython3FW = None
NonOSStdLang = True
elif choicePython == "Ana3":
if choiceQt56 == 'qt5Ana3':
ModulePython = 'PythonAnaconda3V5'
else: # 'qt6Ana3'
ModulePython = 'PythonAnaconda3V6'
OSPython3FW = None
NonOSStdLang = True
elif choicePython == "MP312": elif choicePython == "MP312":
ModulePython = 'Python312MacPorts' ModulePython = 'Python312MacPorts'
OSPython3FW = None OSPython3FW = None
@ -493,10 +522,6 @@ def Parse_CLI_Args(config):
ModulePython = 'Python312Brew' ModulePython = 'Python312Brew'
OSPython3FW = None OSPython3FW = None
NonOSStdLang = True NonOSStdLang = True
elif choicePython == "Ana3":
ModulePython = 'PythonAnaconda3'
OSPython3FW = None
NonOSStdLang = True
elif choicePython == "HB311": elif choicePython == "HB311":
ModulePython = 'Python311Brew' ModulePython = 'Python311Brew'
OSPython3FW = None OSPython3FW = None
@ -692,7 +717,7 @@ def Get_Build_Parameters(config):
parameters['qt_lib_root'] = Qt56Dictionary[ModuleQt]['libdir'] parameters['qt_lib_root'] = Qt56Dictionary[ModuleQt]['libdir']
# (E) rpath # (E) rpath
if OSPython3FW in [ MontereyPy3FW, VenturaPy3FW, SonomaPy3FW, SequoiaPy3FW ]: if OSPython3FW in [ MontereyPy3FW, VenturaPy3FW, SonomaPy3FW, SequoiaPy3FW, TahoePy3FW ]:
parameters['rpath'] = OSPython3FW parameters['rpath'] = OSPython3FW
else: else:
parameters['rpath'] = "@executable_path/../Frameworks" parameters['rpath'] = "@executable_path/../Frameworks"
@ -740,10 +765,10 @@ def Get_Build_Parameters(config):
# (L) Extra parameters needed for <pymod> # (L) Extra parameters needed for <pymod>
# <pymod> will be built if: # <pymod> will be built if:
# BuildPymodWhl = True # BuildPymodWhl = True
# Platform = [ 'Sequoia', 'Sonoma', 'Ventura', 'Monterey'] # Platform = [ 'Tahoe', 'Sequoia', 'Sonoma', 'Ventura', 'Monterey' ]
# ModuleRuby = [ 'Ruby33MacPorts', 'Ruby34Brew', 'RubyAnaconda3' ] # ModuleRuby = [ 'Ruby34MacPorts', 'Ruby34Brew', 'RubyAnaconda3' ]
# ModulePython = [ 'Python312MacPorts', 'Python311MacPorts', # ModulePython = [ 'Python313MacPorts', 'Python312MacPorts', 'Python311MacPorts',
# 'Python311Brew', # 'Python313Brew', 'Python312Brew', 'Python311Brew',
# 'PythonAnaconda3' ] # 'PythonAnaconda3' ]
parameters['BuildPymodWhl'] = BuildPymodWhl parameters['BuildPymodWhl'] = BuildPymodWhl
parameters['Platform'] = Platform parameters['Platform'] = Platform
@ -752,12 +777,12 @@ def Get_Build_Parameters(config):
PymodDistDir = dict() PymodDistDir = dict()
if Platform in [ 'Tahoe', 'Sequoia', 'Sonoma', 'Ventura', 'Monterey' ]: if Platform in [ 'Tahoe', 'Sequoia', 'Sonoma', 'Ventura', 'Monterey' ]:
if ModuleRuby in [ 'Ruby33MacPorts', 'Ruby34Brew', 'RubyAnaconda3' ]: if ModuleRuby in [ 'Ruby34MacPorts', 'Ruby34Brew', 'RubyAnaconda3V5', 'RubyAnaconda3V6' ]:
if ModulePython in [ 'Python312MacPorts', 'Python311MacPorts' ]: if ModulePython in [ 'Python313MacPorts', 'Python312MacPorts', 'Python311MacPorts' ]:
PymodDistDir[ModulePython] = 'dist-MP3-%s' % ModuleQt PymodDistDir[ModulePython] = 'dist-MP3-%s' % ModuleQt
elif ModulePython in [ 'Python311Brew' ]: elif ModulePython in [ 'Python313Brew', 'Python312Brew', 'Python311Brew' ]:
PymodDistDir[ModulePython] = 'dist-HB3-%s' % ModuleQt PymodDistDir[ModulePython] = 'dist-HB3-%s' % ModuleQt
elif ModulePython in [ 'PythonAnaconda3' ]: elif ModulePython in [ 'PythonAnaconda3V5', 'PythonAnaconda3V6' ]:
PymodDistDir[ModulePython] = 'dist-ana3-%s' % ModuleQt PymodDistDir[ModulePython] = 'dist-ana3-%s' % ModuleQt
parameters['pymod_dist'] = PymodDistDir parameters['pymod_dist'] = PymodDistDir
return parameters return parameters
@ -774,11 +799,11 @@ def Build_pymod_wheel(parameters):
#----------------------------------------------------------------------------------------------------------- #-----------------------------------------------------------------------------------------------------------
# [1] <pymod> will be built if: # [1] <pymod> will be built if:
# BuildPymodWhl = True # BuildPymodWhl = True
# Platform = [ 'Sequoia', 'Sonoma', 'Ventura', 'Monterey'] # Platform = [ 'Tahoe', 'Sequoia', 'Sonoma', 'Ventura', 'Monterey' ]
# ModuleRuby = [ 'Ruby33MacPorts', 'Ruby34Brew', 'RubyAnaconda3' ] # ModuleRuby = [ 'Ruby34MacPorts', 'Ruby34Brew', 'RubyAnaconda3V5', 'RubyAnaconda3V6' ]
# ModulePython = [ 'Python312MacPorts', 'Python311MacPorts', # ModulePython = [ 'Python313MacPorts', 'Python312MacPorts', 'Python311MacPorts',
# 'Python311Brew', # 'Python313Brew', 'Python312Brew', 'Python311Brew',
# 'PythonAnaconda3' ] # 'PythonAnaconda3V5', 'PythonAnaconda3V6' ]
#----------------------------------------------------------------------------------------------------------- #-----------------------------------------------------------------------------------------------------------
BuildPymodWhl = parameters['BuildPymodWhl'] BuildPymodWhl = parameters['BuildPymodWhl']
Platform = parameters['Platform'] Platform = parameters['Platform']
@ -788,11 +813,11 @@ def Build_pymod_wheel(parameters):
return 0 return 0
if not Platform in [ 'Tahoe', 'Sequoia', 'Sonoma', 'Ventura', 'Monterey' ]: if not Platform in [ 'Tahoe', 'Sequoia', 'Sonoma', 'Ventura', 'Monterey' ]:
return 0 return 0
elif not ModuleRuby in [ 'Ruby33MacPorts', 'Ruby34Brew', 'RubyAnaconda3' ]: elif not ModuleRuby in [ 'Ruby34MacPorts', 'Ruby34Brew', 'RubyAnaconda3V5', 'RubyAnaconda3V6' ]:
return 0 return 0
elif not ModulePython in [ 'Python312MacPorts', 'Python311MacPorts', \ elif not ModulePython in [ 'Python313MacPorts', 'Python312MacPorts', 'Python311MacPorts', \
'Python311Brew', \ 'Python313Brew', 'Python312Brew', 'Python311Brew', \
'PythonAnaconda3' ]: 'PythonAnaconda3V5', 'PythonAnaconda3V6' ]:
return 0 return 0
#-------------------------------------------------------------------- #--------------------------------------------------------------------
@ -814,10 +839,15 @@ def Build_pymod_wheel(parameters):
addLibPath = "%s/lib" % DefaultHomebrewRoot # -- ditto -- addLibPath = "%s/lib" % DefaultHomebrewRoot # -- ditto --
whlTarget = "HB3" whlTarget = "HB3"
# Using Anaconda3 # Using Anaconda3
elif PymodDistDir[ModulePython].find('dist-ana3') >= 0: elif PymodDistDir[ModulePython].find('dist-ana3-Qt5Ana3') >= 0:
addBinPath = "/Applications/anaconda3/bin" addBinPath = "%s/bin" % Ana3VirEnv5
addIncPath = "/Applications/anaconda3/include" addIncPath = "%s/include" % Ana3VirEnv5
addLibPath = "/Applications/anaconda3/lib" addLibPath = "%s/lib" % Ana3VirEnv5
whlTarget = "ana3"
elif PymodDistDir[ModulePython].find('dist-ana3-Qt6Ana3') >= 0:
addBinPath = "%s/bin" % Ana3VirEnv6
addIncPath = "%s/include" % Ana3VirEnv6
addLibPath = "%s/lib" % Ana3VirEnv6
whlTarget = "ana3" whlTarget = "ana3"
else: else:
addBinPath = "" addBinPath = ""
@ -970,7 +1000,7 @@ def Build_pymod_wheel(parameters):
# V # V
# new: klayout-0.30.2-cp312-cp312-macosx_10_9_x86_64.whl # new: klayout-0.30.2-cp312-cp312-macosx_10_9_x86_64.whl
#------------------------------------------------------------------------ #------------------------------------------------------------------------
if whlTarget == "ana3": if whlTarget == "ana3" and Platform in ['Sequoia', 'Sonoma', 'Ventura', 'Monterey']:
wheels = glob.glob( "dist/*.whl" ) # like ['dist/klayout-0.30.2-cp312-cp312-macosx_10_15_x86_64.whl'] wheels = glob.glob( "dist/*.whl" ) # like ['dist/klayout-0.30.2-cp312-cp312-macosx_10_15_x86_64.whl']
if not len(wheels) == 1: if not len(wheels) == 1:
print( "", file=sys.stderr ) print( "", file=sys.stderr )
@ -1022,6 +1052,7 @@ def Run_Build_Command(config, parameters):
else: else:
jump2pymod_wheel = True jump2pymod_wheel = True
Append_qmake_Flags()
#----------------------------------------------------------------- #-----------------------------------------------------------------
# [1] Use the AddressSanitizer (ASan) in the debug build. # [1] Use the AddressSanitizer (ASan) in the debug build.
# This environment variable is tested in ../src/klayout.pri. # This environment variable is tested in ../src/klayout.pri.
@ -1050,8 +1081,11 @@ def Run_Build_Command(config, parameters):
addLibPath = "%s/lib" % DefaultHomebrewRoot # -- ditto -- addLibPath = "%s/lib" % DefaultHomebrewRoot # -- ditto --
# Using Anaconda3 # Using Anaconda3
elif ModuleQt.upper() in [ 'QT5ANA3' ]: elif ModuleQt.upper() in [ 'QT5ANA3' ]:
addIncPath = "/Applications/anaconda3/include" addIncPath = "%s/include" % Ana3VirEnv5
addLibPath = "/Applications/anaconda3/lib" addLibPath = "%s/lib" % Ana3VirEnv5
elif ModuleQt.upper() in [ 'QT6ANA3' ]:
addIncPath = "%s/include" % Ana3VirEnv6
addLibPath = "%s/lib" % Ana3VirEnv6
else: else:
addIncPath = "" addIncPath = ""
addLibPath = "" addLibPath = ""
@ -1226,6 +1260,8 @@ def Deploy_Binaries_For_Bundle(config, parameters):
NonOSStdLang = config['NonOSStdLang'] NonOSStdLang = config['NonOSStdLang']
DeploymentF = config['DeploymentF'] DeploymentF = config['DeploymentF']
DeploymentP = config['DeploymentP'] DeploymentP = config['DeploymentP']
PackagePrefix = config['PackagePrefix']
ModuleQt = config['ModuleQt']
MacPkgDir = config['MacPkgDir'] MacPkgDir = config['MacPkgDir']
Version = config['Version'] Version = config['Version']
DeployVerbose = config['DeployVerbose'] DeployVerbose = config['DeployVerbose']
@ -1304,8 +1340,10 @@ def Deploy_Binaries_For_Bundle(config, parameters):
# +-- Contents/+ # +-- Contents/+
# +-- Info.plist # +-- Info.plist
# +-- PkgInfo # +-- PkgInfo
# +-- PlugIns/
# +-- Resources/+ # +-- Resources/+
# | +-- 'klayout.icns' # | +-- 'klayout.icns'
# | +-- 'qt.conf'
# +-- Frameworks/+ # +-- Frameworks/+
# | +-- '*.framework' # | +-- '*.framework'
# | +-- '*.dylib' # | +-- '*.dylib'
@ -1793,6 +1831,45 @@ def Deploy_Binaries_For_Bundle(config, parameters):
for item in glob.glob( pymodDistDir + "/*.whl" ): for item in glob.glob( pymodDistDir + "/*.whl" ):
shutil.copy2( item, targetDirP ) shutil.copy2( item, targetDirP )
#------------------------------------------------------------------------
# (D) generate a proper "qt.conf" using the "bundle_qtconf.py" module
#------------------------------------------------------------------------
mode = None # ["st", "hw", "lw"]
lw_qt_major = None # [5, 6]
lw_stack = None # ["macports", "homebrew", "anaconda"]
arch_hint = "auto"
if PackagePrefix == "ST-":
mode = "st"
elif PackagePrefix == "HW-":
mode = "hw"
elif PackagePrefix == "LW-":
mode = "lw"
else:
raise Exception( f"! unsupported PackagePrefix {PackagePrefix}" )
# ModuleQt = ["Qt5MacPorts", "Qt5Brew", "Qt5Ana3", "Qt6MacPorts", "Qt6Brew", "Qt6Ana3"]
lw_qt_major = int(ModuleQt[2])
rest = ModuleQt[3:].lower()
if rest == "macports":
lw_stack = "macports"
elif rest == "brew":
lw_stack = "homebrew"
elif rest == "ana3":
lw_stack = "anaconda"
else:
raise Exception( f"! unknown ModuleQt {ModuleQt}" )
try:
text = generate_qtconf( mode=mode, lw_stack=lw_stack, lw_qt_major=lw_qt_major )
# print(text) # -> "[Paths]\nPlugins=/opt/local/libexec/qt5/plugins\n"
except QtConfError as e:
raise Exception(f"Failed: {e}")
qtconf = targetDirR + "/qt.conf"
with open( qtconf, "w", encoding="utf-8" ) as f:
f.write(text)
os.chmod( qtconf, 0o0644 )
print( " [7] Setting and changing the identification names of KLayout's libraries in each executable ..." ) print( " [7] Setting and changing the identification names of KLayout's libraries in each executable ..." )
#------------------------------------------------------------------------------------ #------------------------------------------------------------------------------------
# [7] Set and change the library identification name(s) of different executable(s) # [7] Set and change the library identification name(s) of different executable(s)
@ -1828,10 +1905,6 @@ def Deploy_Binaries_For_Bundle(config, parameters):
options = macdepQtOpt + verbose options = macdepQtOpt + verbose
deploytool = parameters['deploy_tool'] deploytool = parameters['deploy_tool']
# Without the following, the plugin cocoa would not be found properly.
shutil.copy2( sourceDir2 + "/qt.conf", targetDirM )
os.chmod( targetDirM + "/qt.conf", 0o0644 )
os.chdir(ProjectDir) os.chdir(ProjectDir)
os.chdir(MacPkgDir) os.chdir(MacPkgDir)
command = "%s %s %s" % ( deploytool, app_bundle, options ) command = "%s %s %s" % ( deploytool, app_bundle, options )
@ -2190,7 +2263,21 @@ def Deploy_Binaries_For_Bundle(config, parameters):
print( " [8] Skipped deploying Qt's Frameworks and optional Python/Ruby Frameworks..." ) print( " [8] Skipped deploying Qt's Frameworks and optional Python/Ruby Frameworks..." )
print( "##### Finished deploying the libraries and executables for <klayout.app> #####" ) print( "##### Finished deploying the libraries and executables for <klayout.app> #####" )
print("") print("")
os.chdir(ProjectDir)
#-------------------------------------------------------------
# [11] Sign the application bundle
#-------------------------------------------------------------
if Platform in ['Tahoe']:
print( " [11] Signing the macOS application bundle (ad-hoc) after all post-build edits (install_name_tool/strip)..." )
appbundle = "%s/klayout.app" % AbsMacPkgDir
res = Sign_App_Bundle(appbundle)
print(res["ok"], res["verify_codesign_ok"], res["verify_spctl_ok"])
if not res["ok"]:
print("ERROR:", res.get("error",""))
for tag, ok, out in res["log"][-6:]:
print(f"[{tag}] ok={ok}\n{out}")
os.chdir(ProjectDir)
print("")
return 0 return 0
#------------------------------------------------------------------------------ #------------------------------------------------------------------------------

View File

@ -6,9 +6,9 @@
# #
# Here are dictionaries of ... # Here are dictionaries of ...
# different modules for building KLayout (http://www.klayout.de/index.php) # different modules for building KLayout (http://www.klayout.de/index.php)
# version 0.30.2 or later on different Apple Mac OSX platforms. # version 0.30.5 or later on different Apple Mac OSX platforms.
# #
# This file is imported by 'build4mac.py' script. # This file is imported by the 'build4mac.py' script.
#=============================================================================== #===============================================================================
import os import os
import re import re
@ -27,14 +27,20 @@ XcodeToolChain = { 'nameID': '/usr/bin/install_name_tool -id ',
(System, Node, Release, MacVersion, Machine, Processor) = platform.uname() (System, Node, Release, MacVersion, Machine, Processor) = platform.uname()
if Machine == "arm64": # Apple Silicon! if Machine == "arm64": # Apple Silicon!
DefaultHomebrewRoot = '/opt/homebrew' DefaultHomebrewRoot = '/opt/homebrew'
DefaultAnaconda3Root = '/opt/anaconda3'
Ana3VirEnv5 = '%s/envs/klayout-qt5' % DefaultAnaconda3Root
Ana3VirEnv6 = '%s/envs/klayout-qt6' % DefaultAnaconda3Root
HomebrewSearchPathFilter1 = '\t+%s/opt' % DefaultHomebrewRoot HomebrewSearchPathFilter1 = '\t+%s/opt' % DefaultHomebrewRoot
HomebrewSearchPathFilter2 = '\t+@loader_path/../../../../../../../../../../opt' HomebrewSearchPathFilter2 = '\t+@loader_path/../../../../../../../../../../opt'
HomebrewSearchPathFilter3 = '@loader_path/../../../../../../../../../../opt' # no leading white space HomebrewSearchPathFilter3 = '@loader_path/../../../../../../../../../../opt' # no leading white space
# 1: absolute path as seen in ~python@3.9.17 # 1: absolute path as seen in ~python@3.9.17
# 2: relative path as seen in python@3.9.18 # 2: relative path as seen in python@3.9.18
else: else: # x86_64|Intel
DefaultHomebrewRoot = '/usr/local' DefaultHomebrewRoot = '/usr/local'
DefaultAnaconda3Root = '/Applications/anaconda3'
Ana3VirEnv5 = '%s/envs/klayout-qt5' % DefaultAnaconda3Root
Ana3VirEnv6 = '%s/envs/klayout-qt6' % DefaultAnaconda3Root
HomebrewSearchPathFilter1 = '\t+%s/opt' % DefaultHomebrewRoot HomebrewSearchPathFilter1 = '\t+%s/opt' % DefaultHomebrewRoot
HomebrewSearchPathFilter2 = '\t+@loader_path/../../../../../../../../../../opt' HomebrewSearchPathFilter2 = '\t+@loader_path/../../../../../../../../../../opt'
HomebrewSearchPathFilter3 = '@loader_path/../../../../../../../../../../opt' # no leading white space HomebrewSearchPathFilter3 = '@loader_path/../../../../../../../../../../opt' # no leading white space
@ -63,7 +69,7 @@ del System, Node, Release, MacVersion, Machine, Processor
# [1] Qt5 or Qt6 # [1] Qt5 or Qt6
#----------------------------------------------------- #-----------------------------------------------------
Qts = [ 'Qt5MacPorts', 'Qt5Brew', 'Qt5Ana3' ] Qts = [ 'Qt5MacPorts', 'Qt5Brew', 'Qt5Ana3' ]
Qts += [ 'Qt6MacPorts', 'Qt6Brew' ] Qts += [ 'Qt6MacPorts', 'Qt6Brew', 'Qt6Ana3' ]
#----------------------------------------------------- #-----------------------------------------------------
# Whereabouts of different components of Qt5 # Whereabouts of different components of Qt5
@ -84,13 +90,67 @@ Qt5Brew = { 'qmake' : '%s/opt/qt@5/bin/qmake' % DefaultHomebrewRoot,
'libdir': '%s/opt/qt@5/lib' % DefaultHomebrewRoot 'libdir': '%s/opt/qt@5/lib' % DefaultHomebrewRoot
} }
# Qt5 bundled with anaconda3 installed under /Applications/anaconda3/ #---------------------------------------------------------------------------------------------------
# The standard installation deploys the tool under $HOME/opt/anaconda3/. # [Apple Silicon]
# If so, you need to make a symbolic link: /Applications/anaconda3 ---> $HOME/opt/anaconda3/ # Qt5 is to be installed under /opt/anaconda3/envs/klayout-qt5
# after installing "Anaconda3-2025.06-0-MacOSX-arm64.pkg" under /opt/anaconda3/.
#
# 1) Create a new env "klayout-qt5" (with stable solver & channels)
# switch solver to libmamba for faster/more stable resolves
# $ conda install -n base -y conda-libmamba-solver
# $ conda config --set solver libmamba
#
# Create the environment (on this ARM machine it will pull osx-arm64 builds)
# $ conda create -n klayout-qt5 python=3.13 -y
# $ conda activate klayout-qt5
#
# In this env only, prefer conda-forge strictly (to avoid mixing)
# $ conda config --env --add channels conda-forge
# $ conda config --env --add channels defaults
# $ conda config --env --set channel_priority strict
# $ conda install -n base -y conda-libmamba-solver
# $ conda config --set solver libmamba
#
# 2) Install Qt5 (qt-main) only from conda-forge
# Qt5 core (builds that typically include Designer/UiTools)
# $ conda install -y --override-channels -c conda-forge "qt-main=5.15.15"
#
# 3) Additionally, install Ruby and libgit2 only from conda-forge
# $ conda install -y --override-channels -c conda-forge "ruby=3.4.7"
# $ conda install -y --override-channels -c conda-forge "libgit2=1.9.1"
#---------------------------------------------------------------------------------------------------
# [x86_64|Intel]
# Qt5 is to be installed under /Applications/anaconda3/envs/klayout-qt5
# after installing "Anaconda3-2025.06-0-MacOSX-x86_64.pkg" under /Applications/anaconda3/.
#
# 1) Create a new env "klayout-qt5" (with stable solver & channels)
# switch solver to libmamba for faster/more stable resolves
# $ conda install -n base -y conda-libmamba-solver
# $ conda config --set solver libmamba
#
# Create the environment (on this x86_64 machine it will pull osx-64 builds)
# $ conda create -n klayout-qt5 python=3.13 -y
# $ conda activate klayout-qt5
#
# In this env only, prefer conda-forge strictly (to avoid mixing)
# $ conda config --env --add channels conda-forge
# $ conda config --env --add channels defaults
# $ conda config --env --set channel_priority strict
# $ conda install -n base -y conda-libmamba-solver
# $ conda config --set solver libmamba
#
# 2) Install Qt5 (qt-main) only from conda-forge
# Qt5 core (builds that typically include Designer/UiTools)
# $ conda install -y --override-channels -c conda-forge "qt-main=5.15.15"
#
# 3) Additionally, install Ruby and libgit2 only from conda-forge
# $ conda install -y --override-channels -c conda-forge "ruby=3.4.7"
# $ conda install -y --override-channels -c conda-forge "libgit2=1.9.1"
#---------------------------------------------------------------------------------------------------
# [Key Type Name] = 'Qt5Ana3' # [Key Type Name] = 'Qt5Ana3'
Qt5Ana3 = { 'qmake' : '/Applications/anaconda3/bin/qmake', Qt5Ana3 = { 'qmake' : '%s/bin/qmake' % Ana3VirEnv5,
'deploy': '/Applications/anaconda3/bin/macdeployqt', 'deploy': '%s/bin/macdeployqt' % Ana3VirEnv5,
'libdir': '/Applications/anaconda3/lib' 'libdir': '%s/lib' % Ana3VirEnv5
} }
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
@ -112,12 +172,78 @@ Qt6Brew = { 'qmake' : '%s/opt/qt@6/bin/qmake' % DefaultHomebrewRoot,
'libdir': '%s/opt/qt@6/lib' % DefaultHomebrewRoot 'libdir': '%s/opt/qt@6/lib' % DefaultHomebrewRoot
} }
#---------------------------------------------------------------------------------------------------
# [Apple Silicon]
# Qt6 is to be installed under /opt/anaconda3/envs/klayout-qt6
# after installing "Anaconda3-2025.06-0-MacOSX-arm64.pkg" under /opt/anaconda3/.
#
# 1) Create a new env "klayout-qt6" (with stable solver & channels)
# switch solver to libmamba for faster/more stable resolves
# $ conda install -n base -y conda-libmamba-solver
# $ conda config --set solver libmamba
#
# Create the environment (on this ARM machine it will pull osx-arm64 builds)
# $ conda create -n klayout-qt6 python=3.13 -y
# $ conda activate klayout-qt6
#
# In this env only, prefer conda-forge strictly (to avoid mixing)
# $ conda config --env --add channels conda-forge
# $ conda config --env --add channels defaults
# $ conda config --env --set channel_priority strict
# $ conda install -n base -y conda-libmamba-solver
# $ conda config --set solver libmamba
#
# 2) Install Qt6 (qt6-main and qt6-multimedia) only from conda-forge
# Qt6 core (builds that typically include Designer/UiTools)
# $ conda install -y --override-channels -c conda-forge "qt6-main=6.9.3"
# $ conda install -y --override-channels -c conda-forge "qt6-multimedia=6.9.3"
#
# 3) Additionally, install Ruby and libgit2 only from conda-forge
# $ conda install -y --override-channels -c conda-forge "ruby=3.4.7"
# $ conda install -y --override-channels -c conda-forge "libgit2=1.9.1"
#---------------------------------------------------------------------------------------------------
# [x86_64|Intel]
# Qt6 is to be installed under /Applications/anaconda3/envs/klayout-qt6
# after installing "Anaconda3-2025.06-0-MacOSX-x86_64.pkg" under /Applications/anaconda3/.
#
# 1) Create a new env "klayout-qt6" (with stable solver & channels)
# switch solver to libmamba for faster/more stable resolves
# $ conda install -n base -y conda-libmamba-solver
# $ conda config --set solver libmamba
#
# Create the environment (on this x86_64 machine it will pull osx-64 builds)
# $ conda create -n klayout-qt6 python=3.13 -y
# $ conda activate klayout-qt6
#
# In this env only, prefer conda-forge strictly (to avoid mixing)
# $ conda config --env --add channels conda-forge
# $ conda config --env --add channels defaults
# $ conda config --env --set channel_priority strict
# $ conda install -n base -y conda-libmamba-solver
# $ conda config --set solver libmamba
#
# 2) Install Qt6 (qt6-main and qt6-multimedia) only from conda-forge
# Qt6 core (builds that typically include Designer/UiTools)
# $ conda install -y --override-channels -c conda-forge "qt6-main=6.9.3"
# $ conda install -y --override-channels -c conda-forge "qt6-multimedia=6.9.3"
#
# 3) Additionally, install Ruby and libgit2 only from conda-forge
# $ conda install -y --override-channels -c conda-forge "ruby=3.4.7"
# $ conda install -y --override-channels -c conda-forge "libgit2=1.9.1"
#---------------------------------------------------------------------------------------------------
# [Key Type Name] = 'Qt6Ana3'
Qt6Ana3 = { 'qmake' : '%s/bin/qmake6' % Ana3VirEnv6,
'deploy': '%s/bin/macdeployqt6' % Ana3VirEnv6,
'libdir': '%s/lib' % Ana3VirEnv6
}
# Consolidated dictionary kit for Qt[5|6] # Consolidated dictionary kit for Qt[5|6]
Qt56Dictionary = { 'Qt5MacPorts': Qt5MacPorts, Qt56Dictionary = { 'Qt5MacPorts': Qt5MacPorts,
'Qt5Brew' : Qt5Brew, 'Qt5Brew' : Qt5Brew,
'Qt5Ana3' : Qt5Ana3, 'Qt5Ana3' : Qt5Ana3,
'Qt6MacPorts': Qt6MacPorts, 'Qt6MacPorts': Qt6MacPorts,
'Qt6Brew' : Qt6Brew 'Qt6Brew' : Qt6Brew,
'Qt6Ana3' : Qt6Ana3
} }
#----------------------------------------------------- #-----------------------------------------------------
@ -129,8 +255,8 @@ Qt56Dictionary = { 'Qt5MacPorts': Qt5MacPorts,
# for the previous states. # for the previous states.
#----------------------------------------------------- #-----------------------------------------------------
RubyNil = [ 'nil' ] RubyNil = [ 'nil' ]
RubySys = [ 'RubyMonterey', 'RubyVentura', 'RubySonoma', 'RubySequoia' ] RubySys = [ 'RubyMonterey', 'RubyVentura', 'RubySonoma', 'RubySequoia', 'RubyTahoe' ]
RubyExt = [ 'Ruby33MacPorts', 'Ruby34Brew', 'RubyAnaconda3' ] RubyExt = [ 'Ruby34MacPorts', 'Ruby34Brew', 'RubyAnaconda3' ]
Rubies = RubyNil + RubySys + RubyExt Rubies = RubyNil + RubySys + RubyExt
#----------------------------------------------------- #-----------------------------------------------------
@ -188,12 +314,22 @@ RubySequoia = { 'exe': '/System/Library/Frameworks/Ruby.framework/Versions
'lib': '%s/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/libruby.tbd' % SequoiaXcSDK 'lib': '%s/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/libruby.tbd' % SequoiaXcSDK
} }
# Ruby 3.3 from MacPorts (https://www.macports.org/) # Bundled with Tahoe (26.x)
# install with 'sudo port install ruby33' # [Key Type Name] = 'Sys'
# [Key Type Name] = 'MP33' TahoeXcSDK = "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk"
Ruby33MacPorts = { 'exe': '/opt/local/bin/ruby3.3', TahoeCLTSDK = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"
'inc': '/opt/local/include/ruby-3.3.9', RubyTahoe = { 'exe': '/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/bin/ruby',
'lib': '/opt/local/lib/libruby.3.3.dylib' 'inc': '%s/System/Library/Frameworks/Ruby.framework/Headers' % TahoeXcSDK,
'inc2': '%s/System/Library/Frameworks/Ruby.framework/Headers/ruby' % TahoeXcSDK,
'lib': '%s/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/libruby.tbd' % TahoeXcSDK
}
# Ruby 3.4 from MacPorts (https://www.macports.org/)
# install with 'sudo port install ruby34'
# [Key Type Name] = 'MP34'
Ruby34MacPorts = { 'exe': '/opt/local/bin/ruby3.4',
'inc': '/opt/local/include/ruby-3.4.7',
'lib': '/opt/local/lib/libruby.3.4.dylib'
} }
# Ruby 3.4 from Homebrew # Ruby 3.4 from Homebrew
@ -205,24 +341,30 @@ Ruby34Brew = { 'exe': '%s/bin/ruby' % HBRuby34Path,
'lib': '%s/lib/libruby.3.4.dylib' % HBRuby34Path 'lib': '%s/lib/libruby.3.4.dylib' % HBRuby34Path
} }
# Ruby 3.2 bundled with anaconda3 installed under /Applications/anaconda3/ # Ruby 3.4 installed under [/opt|/Applications]/anaconda3/envs/klayout-qt[5|6]
# The standard installation deploys the tool under $HOME/opt/anaconda3/. # See the [Qt5] [Qt6] section above.
# If so, you need to make a symbolic link: /Applications/anaconda3 ---> $HOME/opt/anaconda3/
# [Key Type Name] = 'Ana3' # [Key Type Name] = 'Ana3'
RubyAnaconda3 = { 'exe': '/Applications/anaconda3/bin/ruby', RubyAnaconda3V5 = { 'exe': '%s/bin/ruby' % Ana3VirEnv5,
'inc': '/Applications/anaconda3/include/ruby-3.2.0', 'inc': '%s/include/ruby-3.4.0' % Ana3VirEnv5,
'lib': '/Applications/anaconda3/lib/libruby.3.2.dylib' 'lib': '%s/lib/libruby.3.4.dylib' % Ana3VirEnv5
} }
RubyAnaconda3V6 = { 'exe': '%s/bin/ruby' % Ana3VirEnv6,
'inc': '%s/include/ruby-3.4.0' % Ana3VirEnv6,
'lib': '%s/lib/libruby.3.4.dylib' % Ana3VirEnv6
}
# Consolidated dictionary kit for Ruby # Consolidated dictionary kit for Ruby
RubyDictionary = { 'nil' : None, RubyDictionary = { 'nil' : None,
'RubyMonterey' : RubyMonterey, 'RubyMonterey' : RubyMonterey,
'RubyVentura' : RubyVentura, 'RubyVentura' : RubyVentura,
'RubySonoma' : RubySonoma, 'RubySonoma' : RubySonoma,
'RubySequoia' : RubySequoia, 'RubySequoia' : RubySequoia,
'Ruby33MacPorts': Ruby33MacPorts, 'RubyTahoe' : RubyTahoe,
'Ruby34Brew' : Ruby34Brew, 'Ruby34MacPorts' : Ruby34MacPorts,
'RubyAnaconda3' : RubyAnaconda3 'Ruby34Brew' : Ruby34Brew,
'RubyAnaconda3V5' : RubyAnaconda3V5,
'RubyAnaconda3V6' : RubyAnaconda3V6
} }
#----------------------------------------------------- #-----------------------------------------------------
@ -234,9 +376,9 @@ RubyDictionary = { 'nil' : None,
# for the previous states. # for the previous states.
#----------------------------------------------------- #-----------------------------------------------------
PythonNil = [ 'nil' ] PythonNil = [ 'nil' ]
PythonSys = [ 'PythonMonterey', 'PythonVentura', 'PythonSonoma', 'PythonSequoia' ] PythonSys = [ 'PythonMonterey', 'PythonVentura', 'PythonSonoma', 'PythonSequoia', 'PythonTahoe' ]
PythonExt = [ 'Python311MacPorts', 'Python312MacPorts' ] PythonExt = [ 'Python311MacPorts', 'Python312MacPorts', 'Python313MacPorts' ]
PythonExt += [ 'Python311Brew', 'Python312Brew', 'PythonAutoBrew' ] PythonExt += [ 'Python311Brew', 'Python312Brew', 'Python313Brew', 'PythonAutoBrew' ]
PythonExt += [ 'PythonAnaconda3' ] PythonExt += [ 'PythonAnaconda3' ]
Pythons = PythonNil + PythonSys + PythonExt Pythons = PythonNil + PythonSys + PythonExt
@ -279,6 +421,15 @@ PythonSequoia = { 'exe': '%s/Python3.framework/Versions/3.9/bin/python3.9' %
'lib': '%s/Python3.framework/Versions/3.9/lib/libpython3.9.dylib' % SequoiaPy3FW 'lib': '%s/Python3.framework/Versions/3.9/lib/libpython3.9.dylib' % SequoiaPy3FW
} }
# Bundled with Tahoe (26.x)
# [Key Type Name] = 'Sys'
TahoePy3FWXc = "/Applications/Xcode.app/Contents/Developer/Library/Frameworks"
TahoePy3FW = "/Library/Developer/CommandLineTools/Library/Frameworks"
PythonTahoe = { 'exe': '%s/Python3.framework/Versions/3.9/bin/python3.9' % TahoePy3FW,
'inc': '%s/Python3.framework/Versions/3.9/include/python3.9' % TahoePy3FW,
'lib': '%s/Python3.framework/Versions/3.9/lib/libpython3.9.dylib' % TahoePy3FW
}
# Python 3.11 from MacPorts (https://www.macports.org/) # Python 3.11 from MacPorts (https://www.macports.org/)
# install with 'sudo port install python311' # install with 'sudo port install python311'
# [Key Type Name] = 'MP311' # [Key Type Name] = 'MP311'
@ -295,6 +446,14 @@ Python312MacPorts = { 'exe': '/opt/local/Library/Frameworks/Python.framework/Ver
'lib': '/opt/local/Library/Frameworks/Python.framework/Versions/3.12/lib/libpython3.12.dylib' 'lib': '/opt/local/Library/Frameworks/Python.framework/Versions/3.12/lib/libpython3.12.dylib'
} }
# Python 3.13 from MacPorts (https://www.macports.org/)
# install with 'sudo port install python313'
# [Key Type Name] = 'MP313'
Python313MacPorts = { 'exe': '/opt/local/Library/Frameworks/Python.framework/Versions/3.13/bin/python3.13',
'inc': '/opt/local/Library/Frameworks/Python.framework/Versions/3.13/include/python3.13',
'lib': '/opt/local/Library/Frameworks/Python.framework/Versions/3.13/lib/libpython3.13.dylib'
}
# Python 3.11 from Homebrew # Python 3.11 from Homebrew
# install with 'brew install python@3.11' # install with 'brew install python@3.11'
# [Key Type Name] = 'HB311' # [Key Type Name] = 'HB311'
@ -313,14 +472,27 @@ Python312Brew = { 'exe': '%s/Versions/3.12/bin/python3.12' % HBPython312Fram
'lib': '%s/Versions/3.12/lib/libpython3.12.dylib' % HBPython312FrameworkPath 'lib': '%s/Versions/3.12/lib/libpython3.12.dylib' % HBPython312FrameworkPath
} }
# Python 3.12 bundled with anaconda3 installed under /Applications/anaconda3/ # Python 3.13 from Homebrew
# The standard installation deploys the tool under $HOME/opt/anaconda3/. # install with 'brew install python@3.13'
# If so, you need to make a symbolic link: /Applications/anaconda3 ---> $HOME/opt/anaconda3/ # [Key Type Name] = 'HB313'
HBPython313FrameworkPath = '%s/opt/python@3.13/Frameworks/Python.framework' % DefaultHomebrewRoot
Python313Brew = { 'exe': '%s/Versions/3.13/bin/python3.13' % HBPython313FrameworkPath,
'inc': '%s/Versions/3.13/include/python3.13' % HBPython313FrameworkPath,
'lib': '%s/Versions/3.13/lib/libpython3.13.dylib' % HBPython313FrameworkPath
}
# Python 3.13 installed under [/opt|/Applications]/anaconda3/klayout-qt[5|6]
# See the [Qt5] [Qt6] section above.
# [Key Type Name] = 'Ana3' # [Key Type Name] = 'Ana3'
PythonAnaconda3 = { 'exe': '/Applications/anaconda3/bin/python3.12', PythonAnaconda3V5 = { 'exe': '%s/bin/python3.13' % Ana3VirEnv5,
'inc': '/Applications/anaconda3/include/python3.12', 'inc': '%s/include/python3.13' % Ana3VirEnv5,
'lib': '/Applications/anaconda3/lib/libpython3.12.dylib' 'lib': '%s/lib/libpython3.13.dylib' % Ana3VirEnv5
} }
PythonAnaconda3V6 = { 'exe': '%s/bin/python3.13' % Ana3VirEnv6,
'inc': '%s/include/python3.13' % Ana3VirEnv6,
'lib': '%s/lib/libpython3.13.dylib' % Ana3VirEnv6
}
# Latest Python from Homebrew # Latest Python from Homebrew
# install with 'brew install python' # install with 'brew install python'
@ -373,16 +545,20 @@ else:
_have_Homebrew_Python = True _have_Homebrew_Python = True
# Consolidated dictionary kit for Python # Consolidated dictionary kit for Python
PythonDictionary = { 'nil' : None, PythonDictionary = { 'nil' : None,
'PythonMonterey' : PythonMonterey, 'PythonMonterey' : PythonMonterey,
'PythonVentura' : PythonVentura, 'PythonVentura' : PythonVentura,
'PythonSonoma' : PythonSonoma, 'PythonSonoma' : PythonSonoma,
'PythonSequoia' : PythonSequoia, 'PythonSequoia' : PythonSequoia,
'Python312MacPorts': Python312MacPorts, 'PythonTahoe' : PythonTahoe,
'Python312Brew' : Python312Brew, 'Python313MacPorts' : Python313MacPorts,
'PythonAnaconda3' : PythonAnaconda3, 'Python313Brew' : Python313Brew,
'Python311MacPorts': Python311MacPorts, 'PythonAnaconda3V5' : PythonAnaconda3V5,
'Python311Brew' : Python311Brew 'PythonAnaconda3V6' : PythonAnaconda3V6,
'Python312MacPorts' : Python312MacPorts,
'Python312Brew' : Python312Brew,
'Python311MacPorts' : Python311MacPorts,
'Python311Brew' : Python311Brew
} }
if _have_Homebrew_Python: if _have_Homebrew_Python:
PythonDictionary['PythonAutoBrew'] = PythonAutoBrew PythonDictionary['PythonAutoBrew'] = PythonAutoBrew

View File

@ -6,7 +6,7 @@
# #
# Here are utility functions and classes ... # Here are utility functions and classes ...
# for building KLayout (http://www.klayout.de/index.php) # for building KLayout (http://www.klayout.de/index.php)
# version 0.30.2 or later on different Apple Mac OSX platforms. # version 0.30.5 or later on different Apple Mac OSX platforms.
# #
# This file is imported by 'build4mac.py' script. # This file is imported by 'build4mac.py' script.
#======================================================================================== #========================================================================================
@ -131,8 +131,10 @@ def SetChangeIdentificationNameOfDyLib( libdic, pathDic ):
# +-- Contents/+ # +-- Contents/+
# +-- Info.plist # +-- Info.plist
# +-- PkgInfo # +-- PkgInfo
# +-- PlugIns/
# +-- Resources/+ # +-- Resources/+
# | +-- 'klayout.icns' # | +-- 'klayout.icns'
# | +-- 'qt.conf'
# +-- Frameworks/+ # +-- Frameworks/+
# | +-- '*.framework' # | +-- '*.framework'
# | +-- '*.dylib' # | +-- '*.dylib'
@ -146,11 +148,15 @@ def SetChangeIdentificationNameOfDyLib( libdic, pathDic ):
# | +-- pymod/ # | +-- pymod/
# | # |
# +-- Buddy/+ # +-- Buddy/+
# +-- 'strm2cif' # | +-- 'strm2cif'
# +-- 'strm2dxf' # | +-- 'strm2dxf'
# : # | :
# +-- 'strmxor' # | +-- 'strmxor'
# |
# +-- pymod-dist/+ (created only if *.whl is available)
# +-- klayout-0.27.8-cp38-cp38-macosx_10_9_x86_64.whl (example)(1)
# #
# (1) *.whl is install with 'pip3'
# @return 0 on success; non-zero on failure # @return 0 on success; non-zero on failure
#---------------------------------------------------------------------------------------- #----------------------------------------------------------------------------------------
def SetChangeLibIdentificationName( executable, relativedir ): def SetChangeLibIdentificationName( executable, relativedir ):
@ -870,6 +876,255 @@ def DumpDependencyDicPair( title, depDic, pathDic ):
return return
#----------------------------------------------------------------------------------------
## To append qmake LFLAGS with -Wl,-adhoc_codesign only if the linker supports it.
# [ChatGPT]
#
# Call this once BEFORE running qmake.
#
# @return void
#----------------------------------------------------------------------------------------
def Append_qmake_Flags():
import os, subprocess, tempfile, textwrap, shutil
def _run(cmd):
"""Return True if command exits successfully, False otherwise."""
try:
subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True)
return True
except Exception:
return False
# Allow disabling the probe via environment variable, if needed.
if os.environ.get("DISABLE_ADHOC_CODESIGN_PROBE") == "1":
return
# Prefer clang; fall back to cc if clang is not available.
compiler = shutil.which("clang") or shutil.which("cc")
if not compiler:
# No compiler to probe with; skip injecting the flag.
return
# Probe: attempt a tiny link with -Wl,-adhoc_codesign (toolchain dependent).
probe_src = textwrap.dedent("int main(){return 0;}\n")
with tempfile.TemporaryDirectory() as td:
src = os.path.join(td, "t.c")
out = os.path.join(td, "t.out")
with open(src, "w") as f:
f.write(probe_src)
supported = _run([compiler, src, "-Wl,-adhoc_codesign", "-o", out])
if not supported:
# Older ld64 (e.g., some Monterey toolchains) may not support this flag.
# Skip injecting; post-build signing will still make the app runnable.
return
def _append(name, extra):
"""Append 'extra' to env var 'name' with a space if it already exists."""
prev = os.environ.get(name, "")
os.environ[name] = (prev + " " + extra).strip() if prev else extra
extra = "-Wl,-adhoc_codesign"
# Cover all target types to ensure both executables and shared libs get the flag.
for var in ("QMAKE_LFLAGS", "QMAKE_LFLAGS_APP", "QMAKE_LFLAGS_SHLIB", "QMAKE_LFLAGS_PLUGIN"):
_append(var, extra)
#------------------------------------------------------------------------------------------------
## Sign a macOS application bundle (ad-hoc) after all post-build edits (install_name_tool/strip).
# [ChatGPT]
#
# What it does:
# - Removes quarantine recursively
# - Drops exec bits on *.so (prevents dyld from treating them as executables)
# - Signs all Mach-O candidates (.dylib, .so, executables), inner code first
# - Deep-signs the .app
# - Verifies with codesign & spctl
#
# Always returns a dict with these keys:
# ok (bool), main_executable (str), so_execbits_dropped (list[str]),
# sign_errors (list[str]), verify_codesign_ok (bool), verify_spctl_ok (bool),
# verify_codesign_out (str), verify_spctl_out (str), log (list[tuple]), error (str)
#
# Usage example:
# res = Sign_App_Bundle("/Applications/klayout.app")
# print(res["ok"], res["verify_codesign_ok"], res["verify_spctl_ok"])
#------------------------------------------------------------------------------------------------
def Sign_App_Bundle(app_path: str, gatekeeper_required: bool = False) -> dict:
"""
Ad-hoc sign a macOS .app bundle for local execution.
If gatekeeper_required=False (default), overall 'ok' reflects codesign verification only.
If gatekeeper_required=True, overall 'ok' requires both codesign AND spctl to pass.
@param[in] app_path: Path to the .app bundle
@param[in] gatekeeper_required: Whether to require Gatekeeper assessment (spctl) to pass
@return: Dict with keys:
ok, main_executable, so_execbits_dropped, sign_errors,
verify_codesign_ok, verify_spctl_ok,
verify_codesign_out, verify_spctl_out, log, error
"""
import os, subprocess, plistlib, shutil
from pathlib import Path
def _blank(error_msg=""):
return {
"ok": False,
"main_executable": "",
"so_execbits_dropped": [],
"sign_errors": [],
"verify_codesign_ok": False,
"verify_spctl_ok": False,
"verify_codesign_out": "",
"verify_spctl_out": "",
"log": [],
"error": error_msg,
}
def _run(cmd):
try:
out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True)
return True, out
except subprocess.CalledProcessError as e:
return False, e.output
except FileNotFoundError as e:
return False, str(e)
# Tools
for tool in ("codesign", "spctl", "xattr"):
if not shutil.which(tool):
return _blank(f"Required tool not found: {tool}")
app = Path(app_path).resolve()
if not app.exists():
return _blank(f"App not found: {app}")
info_plist = app / "Contents" / "Info.plist"
if not info_plist.exists():
return _blank(f"Info.plist not found: {info_plist}")
# CFBundleExecutable
try:
with info_plist.open("rb") as f:
info = plistlib.load(f)
except Exception as e:
return _blank(f"Failed to read Info.plist: {e}")
exe_name = info.get("CFBundleExecutable")
if not exe_name:
return _blank("CFBundleExecutable not set in Info.plist")
main_bin = app / "Contents" / "MacOS" / exe_name
steps_log = []
result = _blank()
result["main_executable"] = str(main_bin)
# 0) Clear quarantine
ok, out = _run(["xattr", "-dr", "com.apple.quarantine", str(app)])
steps_log.append(("xattr_clear_quarantine", ok, out))
# 1) Ensure qt.conf is under Resources (harmless if already correct)
macos_qtconf = app / "Contents" / "MacOS" / "qt.conf"
if macos_qtconf.exists():
try:
resdir = app / "Contents" / "Resources"
resdir.mkdir(parents=True, exist_ok=True)
target = resdir / "qt.conf"
if target.exists():
target.unlink()
shutil.move(str(macos_qtconf), str(target))
steps_log.append(("relocate_qt_conf", True, f"Moved to {target}"))
except Exception as e:
steps_log.append(("relocate_qt_conf", False, f"{e}"))
# 2) Drop exec bits on *.so
so_execbits_dropped = []
for p in app.rglob("*.so"):
try:
if p.is_symlink():
continue
mode = p.stat().st_mode
if mode & 0o111:
os.chmod(p, mode & ~0o111)
so_execbits_dropped.append(str(p))
except Exception as e:
steps_log.append(("chmod_so", False, f"{p}: {e}"))
result["so_execbits_dropped"] = so_execbits_dropped
# 3) Collect inner targets (exclude main)
inner = []
for sub in ("Contents/MacOS", "Contents/Buddy"):
root = app / sub
if root.exists():
for p in root.rglob("*"):
try:
if p.is_symlink() or not p.is_file():
continue
if p == main_bin:
continue
if os.access(p, os.X_OK):
inner.append(p)
except Exception:
pass
for ext in ("*.dylib", "*.so"):
for p in app.rglob(ext):
if p.is_symlink() or not p.is_file():
continue
inner.append(p)
# De-duplicate while preserving order
seen = set()
inner_unique = []
for p in inner:
sp = str(p)
if sp not in seen:
seen.add(sp)
inner_unique.append(p)
# 4) Sign inner
sign_errors = []
for p in inner_unique:
ok, out = _run(["codesign", "-f", "-s", "-", "--timestamp=none", str(p)])
steps_log.append(("codesign_inner", ok, f"{p}\n{out}"))
if not ok:
sign_errors.append(str(p))
result["sign_errors"] = sign_errors
# 5) Sign main
if not main_bin.exists():
result["log"] = steps_log
result["error"] = f"Main executable not found: {main_bin}"
return result
ok, out = _run(["codesign", "-f", "-s", "-", "--timestamp=none", str(main_bin)])
steps_log.append(("codesign_main", ok, f"{main_bin}\n{out}"))
if not ok:
result["log"] = steps_log
result["error"] = "Failed to sign main executable"
return result
# 6) Deep-sign app
ok, out = _run(["codesign", "-f", "-s", "-", "--timestamp=none", "--deep", str(app)])
steps_log.append(("codesign_app_deep", ok, out))
if not ok:
result["log"] = steps_log
result["error"] = "Deep codesign failed"
return result
# 7) Verify
ok1, out1 = _run(["codesign", "--verify", "--deep", "--strict", "--verbose=4", str(app)])
ok2, out2 = _run(["spctl", "--assess", "--type", "execute", "--verbose=4", str(app)])
steps_log.append(("verify_codesign", ok1, out1))
steps_log.append(("assess_spctl", ok2, out2))
result["verify_codesign_ok"] = bool(ok1)
result["verify_spctl_ok"] = bool(ok2)
result["verify_codesign_out"] = out1
result["verify_spctl_out"] = out2
result["log"] = steps_log
# Overall result: choose policy based on gatekeeper_required
result["ok"] = bool(ok1 and ok2) if gatekeeper_required else bool(ok1)
return result
#---------------- #----------------
# End of File # End of File
#---------------- #----------------

473
macbuild/bundle_qtconf.py Executable file
View File

@ -0,0 +1,473 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
File: "macbuild/bundle_qtconf.py"
Author: ChatGPT + Kazzz-S
Utilities to generate and embed a proper qt.conf into a macOS .app bundle
for KLayout (or any Qt-based app), supporting two strategies:
- ST/HW (Qt embedded in the bundle): relative qt.conf (Prefix=.., Plugins=PlugIns)
- LW (use system-wide Qt): absolute qt.conf (Plugins=<absolute path>)
Policy:
- The bundle creator ("macbuild/build4mac.py") decides the target stack at build time.
- The distributed app contains exactly ONE qt.conf (no scripts post-distribution).
Command-line test usage:
python bundle_qtconf.py --mode lw --stack macports --qt 5
python bundle_qtconf.py --app ./dist/klayout.app --mode st --plugins /opt/local/libexec/qt5/plugins
Typical usage:
from pathlib import Path
from bundle_qtconf import generate_qtconf, QtConfError
# 1) LW + MacPorts Qt5 (print-only)
try:
text = generate_qtconf(
mode="lw",
lw_stack="macports",
lw_qt_major=5,
)
print(text)
except QtConfError as e:
print(f"Failed: {e}")
# 2) LW + Homebrew Qt6 (write into app)
try:
text = generate_qtconf(
app_path="dist/klayout.app",
mode="lw",
lw_stack="homebrew",
lw_qt_major=6,
arch_hint="arm64", # "x86_64" for Intel; "auto" works too
)
print(text)
except QtConfError as e:
print(f"Failed: {e}")
# 3) LW + Anaconda (Automator-safe: pass explicit prefix if needed)
try:
text = generate_qtconf(
app_path=Path("dist/klayout.app"),
mode="lw",
lw_stack="anaconda",
conda_prefix="/opt/anaconda3",
)
print(text)
except QtConfError as e:
print(f"Failed: {e}")
# 4) ST/HW (Qt embedded in the bundle)
try:
text = generate_qtconf(
app_path="dist/klayout.app",
mode="st", # or "hw"
embedded_plugins_src="/opt/local/libexec/qt5/plugins",
validate=True,
)
print(text)
except QtConfError as e:
print(f"Failed: {e}")
"""
from __future__ import annotations
import os
import shutil
import subprocess
import argparse
from pathlib import Path
from typing import Iterable, Optional, Tuple, List, Union
class QtConfError(RuntimeError):
"""Raised when qt.conf generation or validation fails."""
# -----------------------------------------------------------------------------
# Utility helpers
# -----------------------------------------------------------------------------
def _app_paths(app_path: Path) -> Tuple[Path, Path, Path]:
"""Return (Resources, PlugIns, MacOS) directories for the .app bundle."""
app_path = app_path.resolve()
contents = app_path / "Contents"
resources = contents / "Resources"
plugins = contents / "PlugIns"
macos = contents / "MacOS"
return resources, plugins, macos
def _ensure_dir(p: Path) -> None:
p.mkdir(parents=True, exist_ok=True)
def choose_homebrew_root(arch_hint: str = "auto") -> Path:
"""Choose Homebrew prefix based on architecture hint."""
if arch_hint == "arm64" or (arch_hint == "auto" and Path("/opt/homebrew").is_dir()):
return Path("/opt/homebrew")
return Path("/usr/local")
def _is_executable(p: Path) -> bool:
try:
return p.is_file() and os.access(str(p), os.X_OK)
except Exception:
return False
def _expand_candidates_with_glob(candidates: List[Path]) -> List[Path]:
expanded: List[Path] = []
for c in candidates:
s = str(c)
if "*" in s or "?" in s or "[" in s:
try:
expanded.extend(Path(x) for x in sorted(map(str, c.parent.glob(c.name))))
except Exception:
pass
else:
expanded.append(c)
return expanded
def _first_existing_platforms_dir(candidates: List[Path]) -> Optional[Path]:
for c in _expand_candidates_with_glob(candidates):
if (c / "platforms").is_dir():
return c
return None
def _home_dir() -> Path:
"""Return user's home directory, safe for Automator/launchd environments."""
try:
h = os.environ.get("HOME")
if h:
return Path(h)
except Exception:
pass
return Path.home()
# -----------------------------------------------------------------------------
# LW plugin dir resolvers (MacPorts / Homebrew / Anaconda)
# -----------------------------------------------------------------------------
def find_plugins_dir_lw(
lw_stack: str,
lw_qt_major: Optional[int] = None,
arch_hint: str = "auto",
conda_prefix: Optional[Path] = None,
) -> Path:
"""Resolve the absolute Qt plugins directory for LW mode."""
stack = lw_stack.lower().strip()
# --- MacPorts ---
if stack == "macports":
if lw_qt_major not in (5, 6):
raise QtConfError("MacPorts requires lw_qt_major to be 5 or 6.")
return Path(f"/opt/local/libexec/qt{lw_qt_major}/plugins")
# --- Homebrew ---
if stack == "homebrew":
if lw_qt_major not in (5, 6):
raise QtConfError("Homebrew requires lw_qt_major to be 5 or 6.")
hb = choose_homebrew_root(arch_hint)
def _looks_like_qt6(p):
s = str(p)
return "/qt6/" in s or s.endswith("/share/qt/plugins") or "/qtbase/" in s
def _looks_like_qt5(p):
s = str(p)
return "/qt@5/" in s or "/qt5/" in s
candidates: List[Path] = []
if lw_qt_major == 6:
# Prefer qtpaths from qt or qtbase (Qt6 split)
for formula in ("qt", "qtbase"):
qtpaths_bin = hb / "opt" / formula / "bin" / "qtpaths"
if _is_executable(qtpaths_bin):
try:
out = subprocess.check_output([str(qtpaths_bin), "--plugin-dir"], text=True).strip()
p = Path(out)
if (p / "platforms").is_dir():
return p
except Exception:
pass
candidates += [
hb / "opt" / "qt" / "share" / "qt" / "plugins",
hb / "opt" / "qtbase" / "share" / "qt" / "plugins",
hb / "Cellar" / "qt" / "*" / "share" / "qt" / "plugins",
hb / "Cellar" / "qtbase" / "*" / "share" / "qt" / "plugins",
hb / "opt" / "qt" / "lib" / "qt6" / "plugins",
hb / "opt" / "qt" / "plugins",
]
found = _first_existing_platforms_dir(candidates)
if found and _looks_like_qt6(found):
return found
else:
qtpaths_bin = hb / "opt" / "qt@5" / "bin" / "qtpaths"
if _is_executable(qtpaths_bin):
try:
out = subprocess.check_output([str(qtpaths_bin), "--plugin-dir"], text=True).strip()
p = Path(out)
if (p / "platforms").is_dir() and _looks_like_qt5(p):
return p
except Exception:
pass
candidates += [
hb / "opt" / "qt@5" / "plugins",
hb / "opt" / "qt@5" / "lib" / "qt5" / "plugins",
hb / "Cellar" / "qt@5" / "*" / "plugins",
hb / "Cellar" / "qt@5" / "*" / "lib" / "qt5" / "plugins",
]
for c in _expand_candidates_with_glob(candidates):
if (c / "platforms").is_dir() and _looks_like_qt5(c):
return c
raise QtConfError(
f"Homebrew Qt{lw_qt_major} plugins not found under {hb}. Checked: "
+ ", ".join(str(p) for p in _expand_candidates_with_glob(candidates))
)
# --- Anaconda / Miniconda / Mambaforge / Miniforge ---
if stack == "anaconda":
def _env_plugins_candidates(env_root, qt_major):
if qt_major == 6:
return [Path(env_root) / "lib" / "qt6" / "plugins"]
else:
return [Path(env_root) / "plugins"]
def _base_preferred_envs(base_root, qt_major):
names = ["klayout-qt6"] if qt_major == 6 else ["klayout-qt5"]
env_roots = [Path(base_root) / "envs" / n for n in names]
cands = []
for er in env_roots:
cands.extend(_env_plugins_candidates(er, qt_major))
return cands
def _scan_all_envs(base_root, qt_major):
cands = []
envs_dir = Path(base_root) / "envs"
if envs_dir.is_dir():
for er in sorted(envs_dir.iterdir()):
if not er.is_dir():
continue
n = er.name.lower()
if qt_major == 6 and "qt6" in n:
cands.extend(_env_plugins_candidates(er, 6))
elif qt_major == 5 and "qt5" in n:
cands.extend(_env_plugins_candidates(er, 5))
for er in sorted(envs_dir.iterdir()):
if er.is_dir():
cands.extend(_env_plugins_candidates(er, qt_major))
return cands
def _base_generic_candidates(base_root):
return [
Path(base_root) / "plugins",
Path(base_root) / "lib" / "qt" / "plugins",
Path(base_root) / "lib" / "qt5" / "plugins",
Path(base_root) / "lib" / "qt6" / "plugins",
]
qt_major = lw_qt_major or 6
roots: List[Path] = []
if conda_prefix:
roots.append(Path(conda_prefix))
env_prefix = os.environ.get("CONDA_PREFIX", "")
if env_prefix:
roots.append(Path(env_prefix))
home = _home_dir()
roots += [
Path("/opt/anaconda3"),
Path("/usr/local/anaconda3"),
home / "opt" / "anaconda3",
home / "anaconda3",
Path("/opt/miniconda3"),
Path("/usr/local/miniconda3"),
home / "miniconda3",
Path("/opt/mambaforge"),
home / "mambaforge",
Path("/opt/miniforge3"),
home / "miniforge3",
Path("/Applications/anaconda3"),
Path("/Applications/miniconda3"),
Path("/Applications/mambaforge"),
Path("/Applications/miniforge3"),
]
plugin_candidates: List[Path] = []
if conda_prefix:
cp = Path(conda_prefix)
if (cp / "conda-meta").is_dir() and not (cp / "envs").is_dir():
plugin_candidates.extend(_env_plugins_candidates(cp, qt_major))
for base in roots:
b = Path(base)
try:
b = b.resolve()
except Exception:
pass
plugin_candidates.extend(_base_preferred_envs(b, qt_major))
plugin_candidates.extend(_scan_all_envs(b, qt_major))
plugin_candidates.extend(_base_generic_candidates(b))
# Highest priority: Intel GUI installer layout
apps_direct = Path("/Applications/anaconda3/plugins")
if apps_direct.exists():
plugin_candidates.insert(0, apps_direct)
found = _first_existing_platforms_dir(plugin_candidates)
if found:
return found
raise QtConfError(
"Anaconda plugins not found. Checked: "
+ ", ".join(str(p) for p in _expand_candidates_with_glob(plugin_candidates))
)
raise QtConfError(f"Unknown lw_stack: {lw_stack}")
# -----------------------------------------------------------------------------
# Core functions
# -----------------------------------------------------------------------------
def _validate_libqcocoa(plugins_dir: Path) -> None:
"""Ensure libqcocoa.dylib exists under <plugins_dir>/platforms."""
lib = plugins_dir / "platforms" / "libqcocoa.dylib"
if not lib.is_file():
raise QtConfError(f"libqcocoa.dylib not found: {lib}")
def copy_embedded_plugins(
embedded_plugins_src: Path,
bundle_plugins_dir: Path,
subdirs: Iterable[str] = ("platforms",),
overwrite: bool = True,
) -> None:
"""Copy selected plugin subdirectories into the bundle."""
embedded_plugins_src = embedded_plugins_src.resolve()
bundle_plugins_dir = bundle_plugins_dir.resolve()
_ensure_dir(bundle_plugins_dir)
for d in subdirs:
src = embedded_plugins_src / d
dst = bundle_plugins_dir / d
if not src.is_dir():
raise QtConfError(f"Missing plugin subdir at source: {src}")
if dst.exists() and overwrite:
shutil.rmtree(dst)
shutil.copytree(src, dst)
def make_qtconf_text_relative() -> str:
"""Return relative qt.conf text for ST/HW bundles."""
return (
"[Paths]\n"
"Prefix=..\n"
"Plugins=PlugIns\n"
"# Uncomment if QML is embedded:\n"
"# Imports=Resources/qml\n"
"# Qml2Imports=Resources/qml\n"
)
def make_qtconf_text_absolute(plugins_dir: Path) -> str:
"""Return absolute qt.conf text for LW bundles."""
return f"[Paths]\nPlugins={plugins_dir}\n"
def generate_qtconf(
app_path: Optional[Union[str, Path]] = None,
*,
mode: str,
embedded_plugins_src: Optional[Union[str, Path]] = None,
lw_stack: Optional[str] = None,
lw_qt_major: Optional[int] = None,
arch_hint: str = "auto",
conda_prefix: Optional[Union[str, Path]] = None,
validate: bool = True,
) -> str:
"""Generate qt.conf content (and optionally write it to the bundle)."""
app_path_p: Optional[Path] = Path(app_path).resolve() if app_path else None
qtconf_text: str
if mode in ("st", "hw"):
qtconf_text = make_qtconf_text_relative()
if app_path_p:
resources, plugins, _macos = _app_paths(app_path_p)
_ensure_dir(resources)
if embedded_plugins_src:
copy_embedded_plugins(Path(embedded_plugins_src), plugins)
if validate:
_validate_libqcocoa(plugins)
(resources / "qt.conf").write_text(qtconf_text, encoding="utf-8")
elif mode == "lw":
if lw_stack is None:
raise QtConfError("lw_stack is required for LW mode (macports|homebrew|anaconda).")
plugins_dir = find_plugins_dir_lw(
lw_stack=lw_stack,
lw_qt_major=lw_qt_major,
arch_hint=arch_hint,
conda_prefix=Path(conda_prefix) if conda_prefix else None,
)
if validate:
_validate_libqcocoa(plugins_dir)
qtconf_text = make_qtconf_text_absolute(plugins_dir)
if app_path_p:
resources, _, _ = _app_paths(app_path_p)
_ensure_dir(resources)
(resources / "qt.conf").write_text(qtconf_text, encoding="utf-8")
else:
raise QtConfError(f"Unknown mode: {mode}")
return qtconf_text
# -----------------------------------------------------------------------------
# CLI for testing
# -----------------------------------------------------------------------------
def main() -> None:
"""Standalone CLI for testing or dry-run output."""
parser = argparse.ArgumentParser(description="Generate qt.conf or print its content.")
parser.add_argument("--app", help="Path to the .app bundle (optional; if omitted, only print the content)")
parser.add_argument("--mode", choices=["st", "hw", "lw"], required=True, help="Bundle mode")
parser.add_argument("--stack", choices=["macports", "homebrew", "anaconda"], help="LW: Qt stack type")
parser.add_argument("--qt", type=int, choices=[5, 6], help="LW: Qt major version (5 or 6)")
parser.add_argument("--arch", default="auto", choices=["auto", "arm64", "x86_64"], help="LW: arch hint for Homebrew")
parser.add_argument("--plugins", help="ST/HW: source path of Qt plugins")
parser.add_argument("--no-validate", action="store_true", help="Skip validation of libqcocoa.dylib existence")
parser.add_argument("--conda-prefix", help="LW(Anaconda) only: explicit CONDA_PREFIX to use")
args = parser.parse_args()
try:
qtconf_text = generate_qtconf(
app_path=args.app,
mode=args.mode,
embedded_plugins_src=args.plugins,
lw_stack=args.stack,
lw_qt_major=args.qt,
arch_hint=args.arch,
conda_prefix=args.conda_prefix,
validate=not args.no_validate,
)
if args.app:
print(f"[OK] qt.conf written to bundle: {args.app}")
print("----- qt.conf content -----")
print(qtconf_text.strip())
print("---------------------------")
except QtConfError as e:
print(f"[ERROR] {e}")
raise SystemExit(1)
if __name__ == "__main__":
main()

14
macbuild/cleanQAT.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
# File: macbuild/cleanQAT.sh
#
# Task: cleanup qt*.macQAT directories
for d in *macQAT*; do
if [ -d "$d" ]; then
echo "Processing: $d"
(
cd "$d" || exit 1
\rm -rf QATest*
)
fi
done

72
macbuild/mac_no_agl.pri Normal file
View File

@ -0,0 +1,72 @@
#---------------------------------------------------------------------------------------
# File: src/mac_no_agl.pri
#
# Aims: To avoid linking "AGL" when building with macOS SDK >= 26 (Xcode 26 or later)
#
# Refs: https://github.com/KLayout/klayout/issues/2159
#
# Usage: Include this file in all leaf "*.pro" files, for example,
# ---> src/tl/tl/tl.pro
# include($$PWD/../../lib.pri)
# include($$PWD/../../mac_no_agl.pri) <===
# --- src/tl/unit_tests/unit_tests.pro
# include($$PWD/../../lib_ut.pri)
# include($$PWD/../../mac_no_agl.pri) <===
#---------------------------------------------------------------------------------------
macx {
# Prevent qmake from injecting dependencies from .prl (most reliable protection)
CONFIG -= link_prl
# QMAKE_MAC_SDK examples: "macosx26.0", "macosx26", "macosx27.1"
SDK_TAG = $$QMAKE_MAC_SDK
SDK_VER_STR = $$replace(SDK_TAG, "macosx", "")
SDK_VER_MAJOR = $$section(SDK_VER_STR, ., 0, 0)
# Fallback: when parsing fails, also match explicit "macosx26"
contains(SDK_TAG, "macosx26") {
SDK_VER_MAJOR = 26
}
# --- fetch actual SDK info when QMAKE_MAC_SDK only gives "macosx" ---
SDK_PATH = $$system("/usr/bin/xcrun --sdk macosx --show-sdk-path")
SDK_VER_STR = $$system("/usr/bin/xcrun --sdk macosx --show-sdk-version")
# Backup extraction: derive version from SDK path (e.g., MacOSX26.0.sdk → 26.0)
isEmpty(SDK_VER_STR) {
SDK_BASE = $$basename($$SDK_PATH) # MacOSX26.0.sdk
SDK_VER_STR = $$replace(SDK_BASE, "MacOSX", "")
SDK_VER_STR = $$replace(SDK_VER_STR, ".sdk", "")
}
# Extract only the major version number (e.g., 26.0 → 26)
SDK_VER_MAJOR = $$section(SDK_VER_STR, ., 0, 0)
# Debug output
message("DEBUG: SDK_PATH = $$SDK_PATH")
message("DEBUG: SDK_VER_STR = $$SDK_VER_STR")
message("DEBUG: SDK_VER_MAJOR = $$SDK_VER_MAJOR")
# Apply AGL removal if SDK version >= 26
greaterThan(SDK_VER_MAJOR, 25) {
message("Detected macOS SDK >= 26 ($$SDK_VER_STR). Adjusting flags...")
# Aggressively remove AGL in case its inserted by Qt or manually
LIBS -= -framework
LIBS -= AGL
QMAKE_LIBS_OPENGL -= -framework
QMAKE_LIBS_OPENGL -= AGL
QMAKE_LIBS_OPENGL = -framework OpenGL
# Set consistent minimum deployment target for modern macOS/Apple Silicon
QMAKE_CXXFLAGS -= -mmacosx-version-min=10.13
QMAKE_LFLAGS -= -mmacosx-version-min=10.13
QMAKE_MACOSX_DEPLOYMENT_TARGET = 12.0
QMAKE_CXXFLAGS += -mmacosx-version-min=12.0
QMAKE_LFLAGS += -mmacosx-version-min=12.0
}
# --- stop execution after printing ---
#error("DEBUG STOP: printed all mac_no_agl.pri variables, stopping qmake.")
}

View File

@ -78,13 +78,13 @@ def SetGlobals():
Usage = "\n" Usage = "\n"
Usage += "---------------------------------------------------------------------------------------------------------\n" Usage += "---------------------------------------------------------------------------------------------------------\n"
Usage += "<< Usage of 'makeDMG4mac.py' >>\n" Usage += "<< Usage of 'makeDMG4mac.py' >>\n"
Usage += " for making a DMG file of KLayout 0.30.2 or later on different Apple macOS platforms.\n" Usage += " for making a DMG file of KLayout 0.30.5 or later on different Apple macOS platforms.\n"
Usage += "\n" Usage += "\n"
Usage += "$ [python] ./makeDMG4mac.py\n" Usage += "$ [python] ./makeDMG4mac.py\n"
Usage += " option & argument : descriptions | default value\n" Usage += " option & argument : descriptions | default value\n"
Usage += " ----------------------------------------------------------------------------------+-----------------\n" Usage += " ----------------------------------------------------------------------------------+-----------------\n"
Usage += " <-p|--pkg <dir>> : package directory created by `build4mac.py` with [-y|-Y] | ``\n" Usage += " <-p|--pkg <dir>> : package directory created by `build4mac.py` with [-y|-Y] | ``\n"
Usage += " : like 'LW-qt5MP.pkg.macos-Sequoia-release-Rmp33Pmp312' | \n" Usage += " : like 'LW-qt5MP.pkg.macos-Sequoia-release-Rmp34Pmp313' | \n"
Usage += " <-c|--clean> : clean the work directory | disabled\n" Usage += " <-c|--clean> : clean the work directory | disabled\n"
Usage += " <-m|--make> : make a compressed DMG file | disabled\n" Usage += " <-m|--make> : make a compressed DMG file | disabled\n"
Usage += " : <-c|--clean> and <-m|--make> are mutually exclusive | \n" Usage += " : <-c|--clean> and <-m|--make> are mutually exclusive | \n"
@ -106,7 +106,11 @@ def SetGlobals():
release = int( Release.split(".")[0] ) # take the first of ['21', '0', '0'] release = int( Release.split(".")[0] ) # take the first of ['21', '0', '0']
LatestOS = "" LatestOS = ""
if release == 24: if release == 25:
GenOSName = "macOS"
Platform = "Tahoe"
LatestOS = Platform
elif release == 24:
GenOSName = "macOS" GenOSName = "macOS"
Platform = "Sequoia" Platform = "Sequoia"
LatestOS = Platform LatestOS = Platform
@ -131,7 +135,7 @@ def SetGlobals():
if not Machine == "x86_64": if not Machine == "x86_64":
# with an Apple Silicon Chip? # with an Apple Silicon Chip?
if Machine == "arm64" and Platform in ["Sequoia", "Sonoma", "Ventura", "Monterey"]: if Machine == "arm64" and Platform in ["Tahoe", "Sequoia", "Sonoma", "Ventura", "Monterey"]:
print("") print("")
print( "### Your Mac equips an Apple Silicon Chip ###" ) print( "### Your Mac equips an Apple Silicon Chip ###" )
print("") print("")
@ -293,9 +297,15 @@ def CheckPkgDirectory():
PackagePrefix = pkgdirComponents[0] PackagePrefix = pkgdirComponents[0]
QtIdentification = pkgdirComponents[2] QtIdentification = pkgdirComponents[2]
if QtIdentification.find('qt5') == 0: if QtIdentification.find('qt5') == 0:
BackgroundPNG = "KLayoutDMG-BackQt5.png" if Machine == "x86_64":
BackgroundPNG = "KLayoutDMG-BackQt5-X86.png"
else: # arm64
BackgroundPNG = "KLayoutDMG-BackQt5-Mx.png"
elif QtIdentification.find('qt6') == 0: elif QtIdentification.find('qt6') == 0:
BackgroundPNG = "KLayoutDMG-BackQt6.png" if Machine == "x86_64":
BackgroundPNG = "KLayoutDMG-BackQt6-X86.png"
else: # arm64
BackgroundPNG = "KLayoutDMG-BackQt6-Mx.png"
else: else:
BackgroundPNG = None BackgroundPNG = None
raise Exception( "! neither qt5 nor qt6" ) raise Exception( "! neither qt5 nor qt6" )
@ -319,16 +329,16 @@ def CheckPkgDirectory():
LatestOSMacPorts = Platform == LatestOS LatestOSMacPorts = Platform == LatestOS
LatestOSMacPorts &= PackagePrefix == "LW" LatestOSMacPorts &= PackagePrefix == "LW"
LatestOSMacPorts &= QtIdentification in [ "qt5MP", "qt6MP" ] LatestOSMacPorts &= QtIdentification in [ "qt5MP", "qt6MP" ]
LatestOSMacPorts &= RubyPythonID in [ "Rmp33Pmp312", "Rmp33Pmp311" ] LatestOSMacPorts &= RubyPythonID in [ "Rmp34Pmp313", "Rmp34Pmp312", "Rmp34Pmp311" ]
LatestOSHomebrew = Platform == LatestOS LatestOSHomebrew = Platform == LatestOS
LatestOSHomebrew &= PackagePrefix == "LW" LatestOSHomebrew &= PackagePrefix == "LW"
LatestOSHomebrew &= QtIdentification in [ "qt5Brew", "qt6Brew", "qt5MP", "qt6MP" ] # "qt[5|6]MP" are the alternatives LatestOSHomebrew &= QtIdentification in [ "qt5Brew", "qt6Brew", "qt5MP", "qt6MP" ] # "qt[5|6]MP" are the alternatives
LatestOSHomebrew &= RubyPythonID in [ "Rhb34Phb312", "Rhb34Phb311", "Rhb34Phbauto" ] LatestOSHomebrew &= RubyPythonID in [ "Rhb34Phb313", "Rhb34Phb312", "Rhb34Phb311", "Rhb34Phbauto" ]
LatestOSAnaconda3 = Platform == LatestOS LatestOSAnaconda3 = Platform == LatestOS
LatestOSAnaconda3 &= PackagePrefix == "LW" LatestOSAnaconda3 &= PackagePrefix == "LW"
LatestOSAnaconda3 &= QtIdentification in [ "qt5Ana3" ] LatestOSAnaconda3 &= QtIdentification in [ "qt5Ana3", "qt6Ana3" ]
LatestOSAnaconda3 &= RubyPythonID in [ "Rana3Pana3" ] LatestOSAnaconda3 &= RubyPythonID in [ "Rana3Pana3" ]
LatestOSHomebrewH = Platform == LatestOS LatestOSHomebrewH = Platform == LatestOS

View File

@ -30,14 +30,16 @@ import pandas as pd
# #
# @return matching platform name on success; "" on failure # @return matching platform name on success; "" on failure
#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
def Test_My_Platform( platforms=[ 'Monterey', 'Ventura', 'Sonoma', 'Sequoia'] ): def Test_My_Platform( platforms=[ 'Monterey', 'Ventura', 'Sonoma', 'Sequoia', 'Tahoe' ] ):
(System, Node, Release, MacVersion, Machine, Processor) = platform.uname() (System, Node, Release, MacVersion, Machine, Processor) = platform.uname()
if not System == "Darwin": if not System == "Darwin":
return "" return ""
release = int( Release.split(".")[0] ) # take the first of ['21', '0', '0'] release = int( Release.split(".")[0] ) # take the first of ['21', '0', '0']
if release == 24: if release == 25:
Platform = "Tahoe"
elif release == 24:
Platform = "Sequoia" Platform = "Sequoia"
elif release == 23: elif release == 23:
Platform = "Sonoma" Platform = "Sonoma"
@ -101,15 +103,15 @@ def Get_Build_Options( targetDic, platform ):
buildOp[(qtVer, "std", "d")] = [ '-q', '%sMacPorts' % qtType, '-r', 'sys', '-p', 'sys', '--debug' ] buildOp[(qtVer, "std", "d")] = [ '-q', '%sMacPorts' % qtType, '-r', 'sys', '-p', 'sys', '--debug' ]
logfile[(qtVer, "std", "d")] = "%sMP.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "debug", "RsysPsys") logfile[(qtVer, "std", "d")] = "%sMP.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "debug", "RsysPsys")
elif target == "ports": elif target == "ports":
buildOp[(qtVer, "ports", "r")] = [ '-q', '%sMacPorts' % qtType, '-r', 'MP33', '-p', 'MP312' ] buildOp[(qtVer, "ports", "r")] = [ '-q', '%sMacPorts' % qtType, '-r', 'MP34', '-p', 'MP313' ]
logfile[(qtVer, "ports", "r")] = "%sMP.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "release", "Rmp33Pmp312") logfile[(qtVer, "ports", "r")] = "%sMP.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "release", "Rmp34Pmp313")
buildOp[(qtVer, "ports", "d")] = [ '-q', '%sMacPorts' % qtType, '-r', 'MP33', '-p', 'MP312', '--debug' ] buildOp[(qtVer, "ports", "d")] = [ '-q', '%sMacPorts' % qtType, '-r', 'MP34', '-p', 'MP313', '--debug' ]
logfile[(qtVer, "ports", "d")] = "%sMP.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "debug", "Rmp33Pmp312") logfile[(qtVer, "ports", "d")] = "%sMP.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "debug", "Rmp34Pmp313")
elif target == "brew": elif target == "brew":
buildOp[(qtVer, "brew", "r")] = [ '-q', '%sBrew' % qtType, '-r', 'HB34', '-p', 'HB312' ] buildOp[(qtVer, "brew", "r")] = [ '-q', '%sBrew' % qtType, '-r', 'HB34', '-p', 'HB313' ]
logfile[(qtVer, "brew", "r")] = "%sBrew.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "release", "Rhb34Phb312") logfile[(qtVer, "brew", "r")] = "%sBrew.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "release", "Rhb34Phb313")
buildOp[(qtVer, "brew", "d")] = [ '-q', '%sBrew' % qtType, '-r', 'HB34', '-p', 'HB312', '--debug' ] buildOp[(qtVer, "brew", "d")] = [ '-q', '%sBrew' % qtType, '-r', 'HB34', '-p', 'HB313', '--debug' ]
logfile[(qtVer, "brew", "d")] = "%sBrew.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "debug", "Rhb34Phb312") logfile[(qtVer, "brew", "d")] = "%sBrew.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "debug", "Rhb34Phb313")
elif target == "brewHW": elif target == "brewHW":
buildOp[(qtVer, "brewHW", "r")] = [ '-q', '%sBrew' % qtType, '-r', 'sys', '-p', 'HB311' ] buildOp[(qtVer, "brewHW", "r")] = [ '-q', '%sBrew' % qtType, '-r', 'sys', '-p', 'HB311' ]
logfile[(qtVer, "brewHW", "r")] = "%sBrew.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "release", "RsysPhb311") logfile[(qtVer, "brewHW", "r")] = "%sBrew.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "release", "RsysPhb311")
@ -131,10 +133,10 @@ def Get_Build_Options( targetDic, platform ):
buildOp[(qtVer, "brewAHW", "d")] = [ '-q', '%sBrew' % qtType, '-r', 'sys', '-p', 'HBAuto', '--debug' ] buildOp[(qtVer, "brewAHW", "d")] = [ '-q', '%sBrew' % qtType, '-r', 'sys', '-p', 'HBAuto', '--debug' ]
logfile[(qtVer, "brewAHW", "d")] = "%sBrew.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "debug", "RsysPhbauto") logfile[(qtVer, "brewAHW", "d")] = "%sBrew.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "debug", "RsysPhbauto")
elif target == "pbrew": elif target == "pbrew":
buildOp[(qtVer, "pbrew", "r")] = [ '-q', '%sMacPorts' % qtType, '-r', 'HB34', '-p', 'HB312' ] buildOp[(qtVer, "pbrew", "r")] = [ '-q', '%sMacPorts' % qtType, '-r', 'HB34', '-p', 'HB313' ]
logfile[(qtVer, "pbrew", "r")] = "%sMP.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "release", "Rhb34Phb312") logfile[(qtVer, "pbrew", "r")] = "%sMP.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "release", "Rhb34Phb313")
buildOp[(qtVer, "pbrew", "d")] = [ '-q', '%sMacPorts' % qtType, '-r', 'HB34', '-p', 'HB312', '--debug' ] buildOp[(qtVer, "pbrew", "d")] = [ '-q', '%sMacPorts' % qtType, '-r', 'HB34', '-p', 'HB313', '--debug' ]
logfile[(qtVer, "pbrew", "d")] = "%sMP.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "debug", "Rhb34Phb312") logfile[(qtVer, "pbrew", "d")] = "%sMP.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "debug", "Rhb34Phb313")
elif target == "pbrewHW": elif target == "pbrewHW":
buildOp[(qtVer, "pbrewHW", "r")] = [ '-q', '%sMacPorts' % qtType, '-r', 'sys', '-p', 'HB311' ] buildOp[(qtVer, "pbrewHW", "r")] = [ '-q', '%sMacPorts' % qtType, '-r', 'sys', '-p', 'HB311' ]
logfile[(qtVer, "pbrewHW", "r")] = "%sMP.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "release", "RsysPhb311") logfile[(qtVer, "pbrewHW", "r")] = "%sMP.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "release", "RsysPhb311")
@ -177,11 +179,11 @@ def Get_QAT_Directory( targetDic, platform ):
dirQAT[(qtVer, "std", "r")] = '%sMP.build.macos-%s-release-RsysPsys.macQAT' % (qtType.lower(), platform) dirQAT[(qtVer, "std", "r")] = '%sMP.build.macos-%s-release-RsysPsys.macQAT' % (qtType.lower(), platform)
dirQAT[(qtVer, "std", "d")] = '%sMP.build.macos-%s-debug-RsysPsys.macQAT' % (qtType.lower(), platform) dirQAT[(qtVer, "std", "d")] = '%sMP.build.macos-%s-debug-RsysPsys.macQAT' % (qtType.lower(), platform)
elif target == "ports": elif target == "ports":
dirQAT[(qtVer, "ports", "r")] = '%sMP.build.macos-%s-release-Rmp33Pmp312.macQAT' % (qtType.lower(), platform) dirQAT[(qtVer, "ports", "r")] = '%sMP.build.macos-%s-release-Rmp34Pmp313.macQAT' % (qtType.lower(), platform)
dirQAT[(qtVer, "ports", "d")] = '%sMP.build.macos-%s-debug-Rmp33Pmp312.macQAT' % (qtType.lower(), platform) dirQAT[(qtVer, "ports", "d")] = '%sMP.build.macos-%s-debug-Rmp34Pmp313.macQAT' % (qtType.lower(), platform)
elif target == "brew": elif target == "brew":
dirQAT[(qtVer, "brew", "r")] = '%sBrew.build.macos-%s-release-Rhb34Phb312.macQAT' % (qtType.lower(), platform) dirQAT[(qtVer, "brew", "r")] = '%sBrew.build.macos-%s-release-Rhb34Phb313.macQAT' % (qtType.lower(), platform)
dirQAT[(qtVer, "brew", "d")] = '%sBrew.build.macos-%s-debug-Rhb34Phb312.macQAT' % (qtType.lower(), platform) dirQAT[(qtVer, "brew", "d")] = '%sBrew.build.macos-%s-debug-Rhb34Phb313.macQAT' % (qtType.lower(), platform)
elif target == "brewHW": elif target == "brewHW":
dirQAT[(qtVer, "brewHW", "r")] = '%sBrew.build.macos-%s-release-RsysPhb311.macQAT' % (qtType.lower(), platform) dirQAT[(qtVer, "brewHW", "r")] = '%sBrew.build.macos-%s-release-RsysPhb311.macQAT' % (qtType.lower(), platform)
dirQAT[(qtVer, "brewHW", "d")] = '%sBrew.build.macos-%s-debug-RsysPhb311.macQAT' % (qtType.lower(), platform) dirQAT[(qtVer, "brewHW", "d")] = '%sBrew.build.macos-%s-debug-RsysPhb311.macQAT' % (qtType.lower(), platform)
@ -195,8 +197,8 @@ def Get_QAT_Directory( targetDic, platform ):
dirQAT[(qtVer, "brewAHW", "r")] = '%sBrew.build.macos-%s-release-RsysPhbauto.macQAT' % (qtType.lower(), platform) dirQAT[(qtVer, "brewAHW", "r")] = '%sBrew.build.macos-%s-release-RsysPhbauto.macQAT' % (qtType.lower(), platform)
dirQAT[(qtVer, "brewAHW", "d")] = '%sBrew.build.macos-%s-debug-RsysPhbauto.macQAT' % (qtType.lower(), platform) dirQAT[(qtVer, "brewAHW", "d")] = '%sBrew.build.macos-%s-debug-RsysPhbauto.macQAT' % (qtType.lower(), platform)
elif target == "pbrew": elif target == "pbrew":
dirQAT[(qtVer, "pbrew", "r")] = '%sMP.build.macos-%s-release-Rhb34Phb312.macQAT' % (qtType.lower(), platform) dirQAT[(qtVer, "pbrew", "r")] = '%sMP.build.macos-%s-release-Rhb34Phb313.macQAT' % (qtType.lower(), platform)
dirQAT[(qtVer, "pbrew", "d")] = '%sMP.build.macos-%s-debug-Rhb34Phb312.macQAT' % (qtType.lower(), platform) dirQAT[(qtVer, "pbrew", "d")] = '%sMP.build.macos-%s-debug-Rhb34Phb313.macQAT' % (qtType.lower(), platform)
elif target == "pbrewHW": elif target == "pbrewHW":
dirQAT[(qtVer, "pbrewHW", "r")] = '%sMP.build.macos-%s-release-RsysPhb311.macQAT' % (qtType.lower(), platform) dirQAT[(qtVer, "pbrewHW", "r")] = '%sMP.build.macos-%s-release-RsysPhb311.macQAT' % (qtType.lower(), platform)
dirQAT[(qtVer, "pbrewHW", "d")] = '%sMP.build.macos-%s-debug-RsysPhb311.macQAT' % (qtType.lower(), platform) dirQAT[(qtVer, "pbrewHW", "d")] = '%sMP.build.macos-%s-debug-RsysPhb311.macQAT' % (qtType.lower(), platform)
@ -235,14 +237,14 @@ def Get_Package_Options( targetDic, platform, srlDMG, makeflag ):
packOp[(qtVer, "std", "d")] = [ '-p', 'ST-%sMP.pkg.macos-%s-debug-RsysPsys' % (qtType.lower(), platform), packOp[(qtVer, "std", "d")] = [ '-p', 'ST-%sMP.pkg.macos-%s-debug-RsysPsys' % (qtType.lower(), platform),
'-s', '%d' % srlDMG, '%s' % flag ] '-s', '%d' % srlDMG, '%s' % flag ]
elif target == "ports": elif target == "ports":
packOp[(qtVer, "ports", "r")] = [ '-p', 'LW-%sMP.pkg.macos-%s-release-Rmp33Pmp312' % (qtType.lower(), platform), packOp[(qtVer, "ports", "r")] = [ '-p', 'LW-%sMP.pkg.macos-%s-release-Rmp34Pmp313' % (qtType.lower(), platform),
'-s', '%d' % srlDMG, '%s' % flag ] '-s', '%d' % srlDMG, '%s' % flag ]
packOp[(qtVer, "ports", "d")] = [ '-p', 'LW-%sMP.pkg.macos-%s-debug-Rmp33Pmp312' % (qtType.lower(), platform), packOp[(qtVer, "ports", "d")] = [ '-p', 'LW-%sMP.pkg.macos-%s-debug-Rmp34Pmp313' % (qtType.lower(), platform),
'-s', '%d' % srlDMG, '%s' % flag ] '-s', '%d' % srlDMG, '%s' % flag ]
elif target == "brew": elif target == "brew":
packOp[(qtVer, "brew", "r")] = [ '-p', 'LW-%sBrew.pkg.macos-%s-release-Rhb34Phb312' % (qtType.lower(), platform), packOp[(qtVer, "brew", "r")] = [ '-p', 'LW-%sBrew.pkg.macos-%s-release-Rhb34Phb313' % (qtType.lower(), platform),
'-s', '%d' % srlDMG, '%s' % flag ] '-s', '%d' % srlDMG, '%s' % flag ]
packOp[(qtVer, "brew", "d")] = [ '-p', 'LW-%sBrew.pkg.macos-%s-debug-Rhb34Phb312' % (qtType.lower(), platform), packOp[(qtVer, "brew", "d")] = [ '-p', 'LW-%sBrew.pkg.macos-%s-debug-Rhb34Phb313' % (qtType.lower(), platform),
'-s', '%d' % srlDMG, '%s' % flag ] '-s', '%d' % srlDMG, '%s' % flag ]
elif target == "brewHW": elif target == "brewHW":
packOp[(qtVer, "brewHW", "r")] = [ '-p', 'HW-%sBrew.pkg.macos-%s-release-RsysPhb311' % (qtType.lower(), platform), packOp[(qtVer, "brewHW", "r")] = [ '-p', 'HW-%sBrew.pkg.macos-%s-release-RsysPhb311' % (qtType.lower(), platform),
@ -265,9 +267,9 @@ def Get_Package_Options( targetDic, platform, srlDMG, makeflag ):
packOp[(qtVer, "brewAHW", "d")] = [ '-p', 'HW-%sBrew.pkg.macos-%s-debug-RsysPhbauto' % (qtType.lower(), platform), packOp[(qtVer, "brewAHW", "d")] = [ '-p', 'HW-%sBrew.pkg.macos-%s-debug-RsysPhbauto' % (qtType.lower(), platform),
'-s', '%d' % srlDMG, '%s' % flag ] '-s', '%d' % srlDMG, '%s' % flag ]
elif target == "pbrew": elif target == "pbrew":
packOp[(qtVer, "pbrew", "r")] = [ '-p', 'LW-%sMP.pkg.macos-%s-release-Rhb34Phb312' % (qtType.lower(), platform), packOp[(qtVer, "pbrew", "r")] = [ '-p', 'LW-%sMP.pkg.macos-%s-release-Rhb34Phb313' % (qtType.lower(), platform),
'-s', '%d' % srlDMG, '%s' % flag ] '-s', '%d' % srlDMG, '%s' % flag ]
packOp[(qtVer, "pbrew", "d")] = [ '-p', 'LW-%sMP.pkg.macos-%s-debug-Rhb34Phb312' % (qtType.lower(), platform), packOp[(qtVer, "pbrew", "d")] = [ '-p', 'LW-%sMP.pkg.macos-%s-debug-Rhb34Phb313' % (qtType.lower(), platform),
'-s', '%d' % srlDMG, '%s' % flag ] '-s', '%d' % srlDMG, '%s' % flag ]
elif target == "pbrewHW": elif target == "pbrewHW":
packOp[(qtVer, "pbrewHW", "r")] = [ '-p', 'HW-%sMP.pkg.macos-%s-release-RsysPhb311' % (qtType.lower(), platform), packOp[(qtVer, "pbrewHW", "r")] = [ '-p', 'HW-%sMP.pkg.macos-%s-release-RsysPhb311' % (qtType.lower(), platform),
@ -285,6 +287,7 @@ def Parse_CommandLine_Arguments():
global Target # target list global Target # target list
global QtTarget # list of (Qt, target, bdType)-tuple global QtTarget # list of (Qt, target, bdType)-tuple
global Build # operation flag global Build # operation flag
global Deploy # operation flag
global WithPymod # operation flag global WithPymod # operation flag
global QATest # operation flag global QATest # operation flag
global QACheck # operation flag global QACheck # operation flag
@ -296,7 +299,7 @@ def Parse_CommandLine_Arguments():
global DryRun # True for dry-run global DryRun # True for dry-run
platform = Test_My_Platform() platform = Test_My_Platform()
if platform in [ "Sequoia", "Sonoma", "Ventura", "Monterey" ]: if platform in [ "Tahoe", "Sequoia", "Sonoma", "Ventura", "Monterey" ]:
targetopt = "0,1,2,13,4" targetopt = "0,1,2,13,4"
else: else:
targetopt = "" targetopt = ""
@ -320,10 +323,11 @@ def Parse_CommandLine_Arguments():
Usage += " + You can use this option multiple times. |\n" Usage += " + You can use this option multiple times. |\n"
Usage += " + Or you can pass those list by the 'nightlyBuild.csv' file. |\n" Usage += " + Or you can pass those list by the 'nightlyBuild.csv' file. |\n"
Usage += " A sample file 'macbuild/nightlyBuild.sample.csv' is available. |\n" Usage += " A sample file 'macbuild/nightlyBuild.sample.csv' is available. |\n"
Usage += " [--build] : build and deploy | disabled\n" Usage += " [--build] : build and deploy | disabled\n"
Usage += " [--pymod] : build and deploy Pymod, too (release build only) | disabled\n" Usage += " [--deploy] : deploy only | disabled\n"
Usage += " [--test] : run the QA Test | disabled\n" Usage += " [--pymod] : build and deploy Pymod, too (release build only) | disabled\n"
Usage += " [--check] : check the QA Test results | disabled\n" Usage += " [--test] : run the QA Test | disabled\n"
Usage += " [--check] : check the QA Test results | disabled\n"
Usage += " [--makedmg|--cleandmg <srlno>] : make or clean DMGs | disabled\n" Usage += " [--makedmg|--cleandmg <srlno>] : make or clean DMGs | disabled\n"
Usage += " [--upload <dropbox>] : upload DMGs to $HOME/Dropbox/klayout/<dropbox> | disabled\n" Usage += " [--upload <dropbox>] : upload DMGs to $HOME/Dropbox/klayout/<dropbox> | disabled\n"
Usage += " [--dryrun] : dry-run for --build option | disabled\n" Usage += " [--dryrun] : dry-run for --build option | disabled\n"
@ -363,6 +367,12 @@ def Parse_CommandLine_Arguments():
default=False, default=False,
help='build and deploy' ) help='build and deploy' )
p.add_option( '--deploy',
action='store_true',
dest='deploy',
default=False,
help='deploy only' )
p.add_option( '--pymod', p.add_option( '--pymod',
action='store_true', action='store_true',
dest='with_pymod', dest='with_pymod',
@ -409,6 +419,7 @@ def Parse_CommandLine_Arguments():
targets = "%s" % targetopt, targets = "%s" % targetopt,
qt_target = list(), qt_target = list(),
build = False, build = False,
deploy = False,
with_pymod = False, with_pymod = False,
qa_test = False, qa_test = False,
qa_check = False, qa_check = False,
@ -421,24 +432,28 @@ def Parse_CommandLine_Arguments():
opt, args = p.parse_args() opt, args = p.parse_args()
if opt.checkusage: if opt.checkusage:
print(Usage) print(Usage)
quit() sys.exit(0)
myPlatform = Test_My_Platform( [ 'Monterey', 'Ventura', 'Sonoma', 'Sequoia' ] ) myPlatform = Test_My_Platform( [ 'Monterey', 'Ventura', 'Sonoma', 'Sequoia', 'Tahoe' ] )
if myPlatform == "": if myPlatform == "":
print( "! Current platform is not [ 'Monterey', 'Ventura', 'Sonoma', 'Sequoia' ]" ) print( "! Current platform is not [ 'Monterey', 'Ventura', 'Sonoma', 'Sequoia', 'Tahoe' ]" )
print(Usage) print(Usage)
quit() sys.exit(0)
QtType = int(opt.qt_type) QtType = int(opt.qt_type)
if not QtType in [5, 6]: if not QtType in [5, 6]:
print( "! Invalid Qt type <%d>" % QtType ) print( "! Invalid Qt type <%d>" % QtType )
print(Usage) print(Usage)
quit() sys.exit(0)
targetIdx = list() targetIdx = list()
for target in [ int(item) for item in opt.targets.split(",") ]: raw = (opt.targets or "").strip()
if not target in targetIdx: targets = [int(item) for item in raw.split(",") if item.strip() != ""]
targetIdx.append(target) # first appeared and non-duplicated index print(targets)
if len(targets) != 0:
for target in targets:
if not target in targetIdx:
targetIdx.append(target) # first appeared and non-duplicated index
targetDic = Get_Build_Target_Dict() targetDic = Get_Build_Target_Dict()
Target = list() Target = list()
@ -462,14 +477,14 @@ def Parse_CommandLine_Arguments():
if len(df) == 0: if len(df) == 0:
print( "! --qttarget==nightlyBuild.csv is used but DataFrame is empty" ) print( "! --qttarget==nightlyBuild.csv is used but DataFrame is empty" )
print(Usage) print(Usage)
quit() sys.exit(0)
for i in range(0, len(df)): for i in range(0, len(df)):
qt = df.iloc[i,0] qt = df.iloc[i,0]
idx = df.iloc[i,1] idx = df.iloc[i,1]
bdType = df.iloc[i,2].lower()[0] bdType = df.iloc[i,2].lower()[0]
if (qt == 5 and idx in [0,1,2,3,4,5,6,12,13] and bdType in ['r']) or \ if (qt == 5 and idx in [0,1,2,3,4,5,6,12,13] and bdType in ['r']) or \
(qt == 5 and idx in [0,1,2,3, 5,6,12,13] and bdType in ['d']) or \ (qt == 5 and idx in [0,1,2,3, 5,6,12,13] and bdType in ['d']) or \
(qt == 6 and idx in [0,1,2,3, 5,6,12,13] and bdType in ['r', 'd']): (qt == 6 and idx in [0,1,2,3,4,5,6,12,13] and bdType in ['r', 'd']):
QtTarget.append( (qt, targetDic[idx], bdType) ) QtTarget.append( (qt, targetDic[idx], bdType) )
elif len(opt.qt_target) > 0: elif len(opt.qt_target) > 0:
QtTarget = list() QtTarget = list()
@ -480,7 +495,7 @@ def Parse_CommandLine_Arguments():
bdType = (item.split(",")[2]).lower()[0] bdType = (item.split(",")[2]).lower()[0]
if (qt == 5 and idx in [0,1,2,3,4,5,6,12,13] and bdType in ['r']) or \ if (qt == 5 and idx in [0,1,2,3,4,5,6,12,13] and bdType in ['r']) or \
(qt == 5 and idx in [0,1,2,3, 5,6,12,13] and bdType in ['d']) or \ (qt == 5 and idx in [0,1,2,3, 5,6,12,13] and bdType in ['d']) or \
(qt == 6 and idx in [0,1,2,3, 5,6,12,13] and bdType in ['r', 'd']): (qt == 6 and idx in [0,1,2,3,4,5,6,12,13] and bdType in ['r', 'd']):
QtTarget.append( (qt, targetDic[idx], bdType) ) QtTarget.append( (qt, targetDic[idx], bdType) )
else: else:
withqttarget = False withqttarget = False
@ -492,9 +507,10 @@ def Parse_CommandLine_Arguments():
else: else:
print( "! --qttarget is used but there is no valid (Qt, target, bdTye)-tuple" ) print( "! --qttarget is used but there is no valid (Qt, target, bdTye)-tuple" )
print(Usage) print(Usage)
quit() sys.exit(0)
Build = opt.build Build = opt.build
Deploy = opt.deploy
WithPymod = opt.with_pymod WithPymod = opt.with_pymod
QATest = opt.qa_test QATest = opt.qa_test
QACheck = opt.qa_check QACheck = opt.qa_check
@ -514,27 +530,27 @@ def Parse_CommandLine_Arguments():
if MakeDMG and CleanDMG: if MakeDMG and CleanDMG:
print( "! --makedmg and --cleandmg cannot be used simultaneously" ) print( "! --makedmg and --cleandmg cannot be used simultaneously" )
print(Usage) print(Usage)
quit() sys.exit(0)
if not opt.upload == "": if not opt.upload == "":
Upload = True Upload = True
Dropbox = opt.upload Dropbox = opt.upload
if not (Build or QATest or QACheck or MakeDMG or CleanDMG or Upload): if not (Build or Deploy or QATest or QACheck or MakeDMG or CleanDMG or Upload):
print( "! No action selected" ) print( "! No action selected" )
print(Usage) print(Usage)
quit() sys.exit(0)
#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
## To build and deploy ## To build and deploy
#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
def Build_Deploy(): def Build_Deploy( deployonly=False ):
pyBuilder = "./build4mac.py" pyBuilder = "./build4mac.py"
myPlatform = Test_My_Platform() myPlatform = Test_My_Platform()
buildOp, logfile = Get_Build_Options( Get_Build_Target_Dict(), myPlatform ) buildOp, logfile = Get_Build_Options( Get_Build_Target_Dict(), myPlatform )
for qttype, key, bdType in QtTarget: for qttype, key, bdType in QtTarget:
if key == "ana3" and (qttype == 6 or bdType == 'd'): # anaconda3 does not provide Qt6 | debug_lib if key == "ana3" and bdType == 'd': # anaconda3 does not provide debug_lib
continue continue
deplog = logfile[(qttype, key, bdType)].replace( ".log", ".dep.log" ) deplog = logfile[(qttype, key, bdType)].replace( ".log", ".dep.log" )
@ -565,19 +581,20 @@ def Build_Deploy():
print( "" ) print( "" )
continue continue
if subprocess.call( command1, shell=False ) != 0: if not deployonly:
print( "", file=sys.stderr ) if subprocess.call( command1, shell=False ) != 0:
print( "-----------------------------------------------------------------", file=sys.stderr ) print( "", file=sys.stderr )
print( "!!! <%s>: failed to build KLayout" % pyBuilder, file=sys.stderr ) print( "-----------------------------------------------------------------", file=sys.stderr )
print( "-----------------------------------------------------------------", file=sys.stderr ) print( "!!! <%s>: failed to build KLayout" % pyBuilder, file=sys.stderr )
print( "", file=sys.stderr ) print( "-----------------------------------------------------------------", file=sys.stderr )
sys.exit(1) print( "", file=sys.stderr )
else: sys.exit(1)
print( "", file=sys.stderr ) else:
print( "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++", file=sys.stderr ) print( "", file=sys.stderr )
print( "### <%s>: successfully built KLayout" % pyBuilder, file=sys.stderr ) print( "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++", file=sys.stderr )
print( "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++", file=sys.stderr ) print( "### <%s>: successfully built KLayout" % pyBuilder, file=sys.stderr )
print( "", file=sys.stderr ) print( "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++", file=sys.stderr )
print( "", file=sys.stderr )
if subprocess.call( command2, shell=True ) != 0: if subprocess.call( command2, shell=True ) != 0:
print( "", file=sys.stderr ) print( "", file=sys.stderr )
@ -604,7 +621,7 @@ def Run_QATest( excludeList ):
dirQAT = Get_QAT_Directory( Get_Build_Target_Dict(), myPlatform ) dirQAT = Get_QAT_Directory( Get_Build_Target_Dict(), myPlatform )
for qttype, key, bdType in QtTarget: for qttype, key, bdType in QtTarget:
if key == "ana3" and (qttype == 6 or bdType == 'd'): # anaconda3 does not provide Qt6 | debug_lib if key == "ana3" and bdType == 'd': # anaconda3 does not provide debug_lib
continue continue
if key == "ana3": if key == "ana3":
@ -645,11 +662,19 @@ def Check_QATest_Results( lines ):
dirQAT = Get_QAT_Directory( Get_Build_Target_Dict(), myPlatform ) dirQAT = Get_QAT_Directory( Get_Build_Target_Dict(), myPlatform )
for qttype, key, bdType in QtTarget: for qttype, key, bdType in QtTarget:
if key == "ana3" and (qttype == 6 or bdType == 'd'): # anaconda3 does not provide Qt6 | debug_lib if key == "ana3" and bdType == 'd': # anaconda3 does not provide debug_lib
continue continue
os.chdir( dirQAT[(qttype, key, bdType)] ) os.chdir( dirQAT[(qttype, key, bdType)] )
logfile = glob.glob( "*.log" ) logfile = glob.glob( "*.log" )
if not logfile:
print( f"", file=sys.stderr )
print( f"[skip] No *.log files found in '{dirQAT[(qttype, key, bdType)]}'", file=sys.stderr )
print( f"", file=sys.stderr )
os.chdir("../")
continue
command1 = [ tailCommand ] + [ '-n', '%d' % lines ] + logfile command1 = [ tailCommand ] + [ '-n', '%d' % lines ] + logfile
print( dirQAT[(qttype, key, bdType)], command1 ) print( dirQAT[(qttype, key, bdType)], command1 )
#continue #continue
@ -686,7 +711,7 @@ def DMG_Make( srlDMG ):
os.mkdir( stashDMG ) os.mkdir( stashDMG )
for qttype, key, bdType in QtTarget: for qttype, key, bdType in QtTarget:
if key == "ana3" and (qttype == 6 or bdType == 'd'): # anaconda3 does not provide Qt6 | debug_lib if key == "ana3" and bdType == 'd': # anaconda3 does not provide debug_lib
continue continue
command1 = [ pyDMGmaker ] + packOp[(qttype, key, bdType)] command1 = [ pyDMGmaker ] + packOp[(qttype, key, bdType)]
@ -726,7 +751,7 @@ def DMG_Clean( srlDMG ):
shutil.rmtree( stashDMG ) shutil.rmtree( stashDMG )
for qttype, key, bdType in QtTarget: for qttype, key, bdType in QtTarget:
if key == "ana3" and (qttype == 6 or bdType == 'd'): # anaconda3 does not provide Qt6 | debug_lib if key == "ana3" and bdType == 'd': # anaconda3 does not provide debug_lib
continue continue
command1 = [ pyDMGmaker ] + packOp[(qttype, key, bdType)] command1 = [ pyDMGmaker ] + packOp[(qttype, key, bdType)]
@ -770,7 +795,10 @@ def Main():
Parse_CommandLine_Arguments() Parse_CommandLine_Arguments()
if Build: if Build:
Build_Deploy() Build_Deploy(deployonly=False)
if Deploy:
Build_Deploy(deployonly=True)
sys.exit(0)
if QATest: if QATest:
Run_QATest( [] ) # ex. ['pymod', 'pya'] Run_QATest( [] ) # ex. ['pymod', 'pya']
if QACheck: if QACheck:

295
macbuild/pureARM64.py Executable file
View File

@ -0,0 +1,295 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
File: "macbuild/pureARM64.py"
Author: ChatGPT + Kazzz-S
------------
Goal:
- Verify that Mach-O binaries inside a .app bundle (and optionally their external
dependencies) are *pure arm64* (no arm64e / no x86_64).
- Summarize results as OK / WARN / FAIL with an appropriate exit code.
Why:
- On macOS/Apple Silicon, mixing arm64e or x86_64 into a process that expects arm64
can trigger dyld / code-signing validation issues (e.g., SIGKILL: Invalid Page).
Requirements:
- macOS with /usr/bin/file, /usr/bin/otool, /usr/bin/lipo available.
Usage:
$ python3 pureARM64.py "/Applications/klayout.app"
# Do not follow external dependencies:
$ python3 pureARM64.py "/Applications/klayout.app" --no-deps
# Limit which external paths are checked (default covers Homebrew/MacPorts/Conda):
$ python3 pureARM64.py "/Applications/klayout.app" \
--deps-filter '^/(opt/homebrew|opt/local|usr/local|Users/.*/(mambaforge|miniforge|anaconda3))'
# Also show whether each Mach-O has LC_CODE_SIGNATURE:
$ python3 pureARM64.py "/Applications/klayout.app" --show-code-sign
Exit codes:
0 = all OK (pure arm64)
1 = warnings present (fat mix includes arm64; investigate)
2 = failures present (no arm64 slice, or only arm64e/x86_64)
"""
import argparse
import os
import re
import shlex
import subprocess
import sys
# Default external dependency filter (you can customize this per project)
DEFAULT_DEPS_FILTER = r'^/(opt/homebrew|opt/local|usr/local|Users/.*/(mambaforge|miniforge|anaconda3))'
# File name patterns that are likely to be Mach-O payloads
MACHO_EXTS = ('.dylib', '.so', '.bundle')
# ANSI color if stdout is a TTY
COLOR = sys.stdout.isatty()
def color(text: str, code: str) -> str:
if not COLOR:
return text
return f'\033[{code}m{text}\033[0m'
OKC = lambda s: color(s, '32') # green
WRNC = lambda s: color(s, '33') # yellow
ERRC = lambda s: color(s, '31') # red
DIM = lambda s: color(s, '2') # dim
def run_cmd(cmd: list[str]) -> tuple[int, str]:
"""Run a command and capture stdout+stderr as text."""
try:
out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True)
return 0, out
except subprocess.CalledProcessError as e:
return e.returncode, e.output
except FileNotFoundError:
return 127, f"command not found: {' '.join(map(shlex.quote, cmd))}"
def is_executable(path: str) -> bool:
"""Return True if file is executable by mode bit."""
try:
st = os.stat(path)
return bool(st.st_mode & 0o111)
except FileNotFoundError:
return False
def looks_like_macho(path: str) -> bool:
"""Heuristic: use 'file' to check if the payload is Mach-O."""
rc, out = run_cmd(["/usr/bin/file", path])
return rc == 0 and "Mach-O" in out
def list_bundle_machos(app_path: str) -> list[str]:
"""
Enumerate Mach-O candidates inside a .app:
- Contents/MacOS
- Contents/Frameworks
- Contents/PlugIns
- Contents/Buddy
Follow symlinks to real paths; deduplicate by real path.
"""
roots = [
os.path.join(app_path, "Contents", "MacOS"),
os.path.join(app_path, "Contents", "Frameworks"),
os.path.join(app_path, "Contents", "PlugIns"),
os.path.join(app_path, "Contents", "Buddy"),
]
seen: set[str] = set()
results: list[str] = []
for root in roots:
if not os.path.isdir(root):
continue
for dirpath, _dirnames, filenames in os.walk(root):
try:
real_dirpath = os.path.realpath(dirpath)
except OSError:
real_dirpath = dirpath
for name in filenames:
p = os.path.join(real_dirpath, name)
rp = os.path.realpath(p)
if rp in seen:
continue
is_macho = looks_like_macho(p)
# Include only: explicit Mach-O, typical Mach-O extensions (.dylib/.so/.bundle),
# or executables that are confirmed Mach-O.
if name.endswith(MACHO_EXTS) or is_macho or (is_executable(p) and is_macho):
seen.add(rp)
results.append(rp)
results.sort()
return results
def parse_archs_via_lipo(path: str) -> list[str]:
"""
Parse architecture slices using lipo/file.
Prefer lipo -info. Fallback to 'file' if necessary.
Returns a sorted unique list like ['arm64'] or ['arm64', 'arm64e'] etc.
"""
rc, out = run_cmd(["/usr/bin/lipo", "-info", path])
if rc == 0 and "Architectures in the fat file" in out:
# e.g. "Architectures in the fat file: QtGui are: arm64 arm64e"
m = re.search(r'are:\s+(.+)$', out.strip())
if m:
return sorted(set(m.group(1).split()))
if rc == 0 and "Non-fat file" in out:
# e.g. "Non-fat file: foo is architecture: arm64"
m = re.search(r'architecture:\s+(\S+)', out)
if m:
return [m.group(1)]
# Fallback to 'file'
rc2, out2 = run_cmd(["/usr/bin/file", path])
if rc2 == 0:
# e.g. "Mach-O 64-bit dynamically linked shared library arm64"
archs = re.findall(r'\b(arm64e?|x86_64)\b', out2)
if archs:
return sorted(set(archs))
return []
_OT_LIB_RE = re.compile(r'^\s+(/.+?)\s+\(')
def deps_from_otool(path: str) -> list[str]:
"""
Extract absolute dependency paths from 'otool -L'.
Only returns absolute paths found in the listing.
"""
rc, out = run_cmd(["/usr/bin/otool", "-L", path])
deps: list[str] = []
if rc != 0:
return deps
for line in out.splitlines():
m = _OT_LIB_RE.match(line)
if m:
deps.append(m.group(1))
return deps
def classify_archs(archs: list[str]) -> tuple[str, str]:
"""
Classify architecture set:
- OK : exactly {'arm64'}
- WARN : contains 'arm64' plus others (e.g., {'arm64', 'arm64e'})
- FAIL : does not contain 'arm64' (e.g., {'arm64e'} or {'x86_64'})
Returns (state, reason).
"""
s = set(archs)
if not s:
return "FAIL", "no-arch-detected"
if s == {"arm64"}:
return "OK", "pure-arm64"
if "arm64" in s and (("arm64e" in s) or ("x86_64" in s)):
return "WARN", "fat-mix:" + ",".join(sorted(s))
return "FAIL", "unsupported-arch:" + ",".join(sorted(s))
def has_code_signature(path: str) -> bool:
"""
Check whether Mach-O has LC_CODE_SIGNATURE.
Note: this indicates a code signature load command exists; it does NOT validate it.
"""
rc, out = run_cmd(["/usr/bin/otool", "-l", path])
return (rc == 0 and "LC_CODE_SIGNATURE" in out)
def main() -> int:
parser = argparse.ArgumentParser(
description="Check Mach-O binaries for pure arm64 (no arm64e/x86_64)."
)
parser.add_argument("target", help=".app path or a single Mach-O path")
parser.add_argument(
"--no-deps", action="store_true",
help="Do not follow external dependencies (ignore 'otool -L')."
)
parser.add_argument(
"--deps-filter", default=DEFAULT_DEPS_FILTER,
help=f"Regex for which absolute dependency paths to include (default: {DEFAULT_DEPS_FILTER})"
)
parser.add_argument(
"--show-code-sign", action="store_true",
help="Also indicate whether LC_CODE_SIGNATURE exists (informational)."
)
args = parser.parse_args()
target = os.path.abspath(args.target)
is_app = target.endswith(".app") and os.path.isdir(target)
# Collect Mach-O files in the bundle or single target
if is_app:
macho_files = list_bundle_machos(target)
if not macho_files:
print(ERRC("No Mach-O files found under .app bundle"), file=sys.stderr)
return 2
else:
if not os.path.exists(target):
print(ERRC(f"Not found: {target}"), file=sys.stderr)
return 2
macho_files = [target]
# Optionally add external dependencies filtered by regex
deps_pat = None if args.no_deps else re.compile(args.deps_filter)
all_targets: set[str] = set(macho_files)
if deps_pat:
for mf in list(macho_files):
for dep in deps_from_otool(mf):
if deps_pat.search(dep):
all_targets.add(dep)
# Header
print(DIM("# pureARM64: verify pure arm64 (detect arm64e/x86_64)"))
print(DIM(f"# target: {target}"))
print(DIM(f"# deps: {'OFF' if deps_pat is None else 'ON'}"))
if deps_pat is not None:
print(DIM(f"# deps-filter: {deps_pat.pattern}"))
print()
# Evaluate each file
warn = 0
fail = 0
for p in sorted(all_targets):
archs = parse_archs_via_lipo(p)
state, reason = classify_archs(archs)
head = {"OK": OKC("[OK] "), "WARN": WRNC("[WARN] "), "FAIL": ERRC("[FAIL] ")}[state]
tail = f" ({reason})"
if args.show_code_sign:
tail += " " + (DIM("[csig]") if has_code_signature(p) else DIM("[no-sig]"))
arch_str = ",".join(archs) if archs else "?"
print(f"{head}{p}\n arch={arch_str}{tail}")
if state == "WARN":
warn += 1
elif state == "FAIL":
fail += 1
# Summary and exit code
total = len(all_targets)
ok = total - warn - fail
print()
print(f"Summary: {OKC('OK='+str(ok))}, {WRNC('WARN='+str(warn))}, {ERRC('FAIL='+str(fail))}")
if fail > 0:
return 2
if warn > 0:
return 1
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:
print(ERRC("\nInterrupted"), file=sys.stderr)
sys.exit(130)

View File

@ -4,6 +4,9 @@
# capnp needs C++ 14 in version 1.0.1 # capnp needs C++ 14 in version 1.0.1
# Qt6 comes with C++ 17 requirement. # Qt6 comes with C++ 17 requirement.
equals(HAVE_QT, "0") || lessThan(QT_MAJOR_VERSION, 6) { equals(HAVE_QT, "0") || lessThan(QT_MAJOR_VERSION, 6) {
# (1) and (2) are required by (macOS Sonoma) x (Qt5 MacPorts)
CONFIG -= c++11 # (1)
CONFIG += c++14 # (2)
QMAKE_CXXFLAGS += -std=c++14 QMAKE_CXXFLAGS += -std=c++14
} }