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>
Last modified: 2025-05-30<br>
Last modified: 2025-11-10<br>
# 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
* Sonoma (14.x) : experimental
* Ventura (13.x) : -- ditto --
Building KLayout for the previous operating systems listed below has been discontinued.<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)
* Catalina (10.15.7)
* Mojave (10.14)
@ -19,8 +21,9 @@ Pre-built DMG packages are also not provided.<br>
* El Capitan (10.11)
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>
However, some kind volunteers told me they successfully built on an Apple silicon machine.<br>
The author recently acquired an M4 Mac Mini and is attempting to build a native ARM64 version in the Tahoe environment **experimentally**.<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
@ -34,18 +37,17 @@ If you prefer "Qt6" from **Homebrew** (https://brew.sh/), which is usually locat
/usr/local/opt/qt@6/
```
You can also choose "Qt5" from Anaconda3 (https://www.anaconda.com/), which is usually located under:
```
$HOME/opt/anaconda3/pkgs/qt-{version}
```
If you have installed Anaconda3 under $HOME/opt/anaconda3/, make a symbolic link:
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>
If you have installed Anaconda3 under $HOME/opt/anaconda3/ (or other location), make a symbolic link:
```
/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>
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
@ -57,8 +59,8 @@ You need to have the followings:
* The latest Xcode and command-line tool kit compliant with each OS
* https://developer.apple.com/xcode/resources/
* https://mac.install.guide/commandlinetools/4
* Qt5 package from MacPorts or Anaconda3. Qt6, from Homebrew.
* libgit2 form MacPorts, Homebrew,or Anaconda3.
* Qt5 package from MacPorts. Qt6, from Homebrew or Anaconda3.
* libgit2 form 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`.
@ -70,32 +72,36 @@ The operating system type is detected automatically.
```
-----------------------------------------------------------------------------------------------------------
<< 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
option & argument : descriptions (refer to 'macbuild/build4mac_env.py' for details) | default value
----------------------------------------------------------------------------------------+---------------
[-q|--qt <type>] : case-insensitive type=['Qt5MacPorts', 'Qt5Brew', 'Qt5Ana3', | qt5macports
: 'Qt6MacPorts', 'Qt6Brew'] |
: 'Qt6MacPorts', 'Qt6Brew', 'Qt6Ana3''] |
: Qt5MacPorts: use Qt5 from MacPorts |
: Qt5Brew: use Qt5 from Homebrew |
: Qt5Ana3: use Qt5 from Anaconda3 |
: Qt6MacPorts: use Qt6 from MacPorts (*) |
: Qt6Brew: use Qt6 from Homebrew (*) |
: Qt6Ana3: use Qt6 from Anaconda3 (*) |
: (*) 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 |
: Sys: use [Sequoia|Sonoma|Ventura|Monterey]-bundled Ruby 2.6 |
: MP33: use Ruby 3.3 from MacPorts |
: Sys: use [Tahoe|Sequoia|Sonoma]-bundled Ruby 2.6 |
: MP34: use Ruby 3.4 from MacPorts |
: HB34: use Ruby 3.4 from Homebrew |
: Ana3: use Ruby 3.2 from Anaconda3 |
[-p|--python <type>] : case-insensitive type=['nil', 'Sys', 'MP312', 'HB312', 'Ana3', | sys
: Ana3: use Ruby 3.4 from Anaconda3 |
[-p|--python <type>] : case-insensitive type=['nil', 'Sys', 'MP313', 'HB313', 'Ana3', | sys
: 'MP312', 'MP312', |
: 'MP311', 'HB311', 'HBAuto'] |
: 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 |
: HB312: use Python 3.12 from Homebrew |
: Ana3: use Python 3.12 from Anaconda3 |
: MP311: use Python 3.11 from MacPorts |
: HB311: use Python 3.11 from Homebrew (+) |
: (+) 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`.
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
$ ./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.
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
0. Install MacPorts, then install Qt5, Ruby 3.3, Python 3.12, and libgit2 by
### 6B. Fully MacPorts-flavored build with MacPorts Ruby 3.4 and MacPorts Python 3.13
0. Install MacPorts, then install Qt5, Ruby 3.4, Python 3.13, and libgit2 by
```
$ sudo port install coreutils
$ sudo port install findutils
$ sudo port install qt5
$ sudo port install ruby33
$ sudo port install python312
$ sudo port install py312-pip
$ sudo port install ruby34
$ sudo port install python313
$ sudo port install py313-pip
$ sudo port install libgit2
```
1. Invoke **`build4mac.py`** with the following options:
```
$ 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).
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/**.
```
$ ./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>
**`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.
* "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.
### 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]
> 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 ruby@3.4
$ brew install python@3.12
$ brew install python@3.13
$ brew install libgit2
$ cd /where/'build.sh'/exists
$ cd macbuild
$ ./python3HB.py -v 3.12
$ ./python3HB.py -v 3.13
```
1. Invoke **`build4mac.py`** with the following options:
```
$ 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).
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>
```
$ ./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>
**`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.
* "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.
> [!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
> ```
@ -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.
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
0. Install Anaconda3 (Anaconda3-2024.10-1-MacOSX-x86_64.pkg), then install Ruby 3.2 and libgit2 by
### 6F. Fully Anaconda3-flavored build with Anaconda3 Ruby 3.4 and Anaconda3 Python 3.13
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
$ conda install libgit2=1.6.4
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"
```
1. Invoke **`build4mac.py`** with the following options:
```
$ 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).
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/**.
```
$ ./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>
**`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.
* "qt5Ana3" means that Qt5 from Anaconda3 is used.
* "Rana3Pana3" means that Ruby (3.2) is from Anaconda3; Python (3.12) is from Anaconda3.
* "qt6Ana3" means that Qt6 from Anaconda3 is used.
* "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.
5. You may have to set the `PYTHONHOME` environment variable like:
```
export PYTHONHOME=$HOME/opt/anaconda3
```
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.
### 6G. Other combinations
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,
```
$ 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>
* **`LW-klayout-0.30.2-macOS-Sequoia-1-qt5MP-Rmp33Pmp312.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`** ---(1) the main DMG file
* **`LW-klayout-0.30.5-macOS-Sequoia-1-qt5MP-Rmp34Pmp313.dmg.md5`** ---(2) MD5-value text file
# 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>

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]
Plugins = ../PlugIns
Imports = ../Resources/qml
Qml2Imports = ../Resources/qml
Prefix = ..
Plugins = PlugIns
Imports = Resources/qml
Qml2Imports = Resources/qml

View File

@ -4,8 +4,8 @@
#===============================================================================
# File: "macbuild/build4mac.py"
#
# 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.
# The top Python script for building KLayout (http://www.klayout.de/index.php).
# version 0.30.5 or later on different Apple Mac OSX platforms.
#===============================================================================
import sys
import os
@ -17,6 +17,7 @@ import platform
import optparse
import subprocess
import pprint
from pathlib import Path
#-------------------------------------------------------------------------------
## 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" )
from build4mac_env import *
from build4mac_util import *
from bundle_qtconf import generate_qtconf, QtConfError
#-------------------------------------------------------------------------------
## To generate the OS-wise usage strings and the default module set
@ -45,32 +47,36 @@ def GenerateUsage(platform):
usage = "\n"
usage += "-----------------------------------------------------------------------------------------------------------\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 += "$ [python] ./build4mac.py\n"
usage += " option & argument : descriptions (refer to 'macbuild/build4mac_env.py' for details) | default value\n"
usage += " ----------------------------------------------------------------------------------------+---------------\n"
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 += " : Qt5Brew: use Qt5 from Homebrew |\n"
usage += " : Qt5Ana3: use Qt5 from Anaconda3 |\n"
usage += " : Qt6MacPorts: use Qt6 from MacPorts (*) |\n"
usage += " : Qt6Brew: use Qt6 from Homebrew (*) |\n"
usage += " : Qt6Ana3: use Qt6 from Anaconda3 (*) |\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 += " : Sys: use [Sequoia|Sonoma|Ventura|Monterey]-bundled Ruby 2.6 |\n"
usage += " : MP33: use Ruby 3.3 from MacPorts |\n"
usage += " : Sys: use [Tahoe|Sequoia|Sonoma]-bundled Ruby 2.6 |\n"
usage += " : MP34: use Ruby 3.4 from MacPorts |\n"
usage += " : HB34: use Ruby 3.4 from Homebrew |\n"
usage += " : Ana3: use Ruby 3.2 from Anaconda3 |\n"
usage += " [-p|--python <type>] : case-insensitive type=['nil', 'Sys', 'MP312', 'HB312', 'Ana3', | %s\n" % myPython
usage += " : Ana3: use Ruby 3.4 from Anaconda3 |\n"
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 += " : 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 += " : 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 += " : HB311: use Python 3.11 from Homebrew (+) |\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.add_option( '-q', '--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',
dest='type_ruby',
help="Ruby type=['nil', 'Sys', 'MP33', 'HB34', 'Ana3']" )
help="Ruby type=['nil', 'Sys', 'MP34', 'HB34', 'Ana3']" )
p.add_option( '-p', '--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',
action='store_true',
@ -381,6 +387,7 @@ def Parse_CLI_Args(config):
candidates['QT5ANA3'] = 'Qt5Ana3'
candidates['QT6MACPORTS'] = 'Qt6MacPorts'
candidates['QT6BREW'] = 'Qt6Brew'
candidates['QT6ANA3'] = 'Qt6Ana3'
try:
ModuleQt = candidates[ opt.type_qt.upper() ]
except KeyError:
@ -402,6 +409,8 @@ def Parse_CLI_Args(config):
choiceQt56 = 'qt6MP'
elif ModuleQt == "Qt6Brew":
choiceQt56 = 'qt6Brew'
elif ModuleQt == "Qt6Ana3":
choiceQt56 = 'qt6Ana3'
# Check if non-OS-standard (-bundled) script languages (Ruby and Python) are used
NonOSStdLang = False
@ -410,7 +419,7 @@ def Parse_CLI_Args(config):
candidates = dict()
candidates['NIL'] = 'nil'
candidates['SYS'] = 'Sys'
candidates['MP33'] = 'MP33'
candidates['MP34'] = 'MP34'
candidates['HB34'] = 'HB34'
candidates['ANA3'] = 'Ana3'
try:
@ -433,14 +442,17 @@ def Parse_CLI_Args(config):
ModuleRuby = 'RubyVentura'
elif Platform == "Monterey":
ModuleRuby = 'RubyMonterey'
elif choiceRuby == "MP33":
ModuleRuby = 'Ruby33MacPorts'
elif choiceRuby == "MP34":
ModuleRuby = 'Ruby34MacPorts'
NonOSStdLang = True
elif choiceRuby == "HB34":
ModuleRuby = 'Ruby34Brew'
NonOSStdLang = True
elif choiceRuby == "Ana3":
ModuleRuby = 'RubyAnaconda3'
if choiceQt56 == 'qt5Ana3':
ModuleRuby = 'RubyAnaconda3V5'
else: # 'qt6Ana3'
ModuleRuby = 'RubyAnaconda3V6'
NonOSStdLang = True
if ModuleRuby == '':
print("")
@ -453,9 +465,11 @@ def Parse_CLI_Args(config):
candidates = dict()
candidates['NIL'] = 'nil'
candidates['SYS'] = 'Sys'
candidates['MP313'] = 'MP313'
candidates['HB313'] = 'HB313'
candidates['ANA3'] = 'Ana3'
candidates['MP312'] = 'MP312'
candidates['HB312'] = 'HB312'
candidates['ANA3'] = 'Ana3'
candidates['MP311'] = 'MP311'
candidates['HB311'] = 'HB311'
candidates['HBAUTO'] = 'HBAuto'
@ -485,6 +499,21 @@ def Parse_CLI_Args(config):
elif Platform == "Monterey":
ModulePython = 'PythonMonterey'
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":
ModulePython = 'Python312MacPorts'
OSPython3FW = None
@ -493,10 +522,6 @@ def Parse_CLI_Args(config):
ModulePython = 'Python312Brew'
OSPython3FW = None
NonOSStdLang = True
elif choicePython == "Ana3":
ModulePython = 'PythonAnaconda3'
OSPython3FW = None
NonOSStdLang = True
elif choicePython == "HB311":
ModulePython = 'Python311Brew'
OSPython3FW = None
@ -692,7 +717,7 @@ def Get_Build_Parameters(config):
parameters['qt_lib_root'] = Qt56Dictionary[ModuleQt]['libdir']
# (E) rpath
if OSPython3FW in [ MontereyPy3FW, VenturaPy3FW, SonomaPy3FW, SequoiaPy3FW ]:
if OSPython3FW in [ MontereyPy3FW, VenturaPy3FW, SonomaPy3FW, SequoiaPy3FW, TahoePy3FW ]:
parameters['rpath'] = OSPython3FW
else:
parameters['rpath'] = "@executable_path/../Frameworks"
@ -740,10 +765,10 @@ def Get_Build_Parameters(config):
# (L) Extra parameters needed for <pymod>
# <pymod> will be built if:
# BuildPymodWhl = True
# Platform = [ 'Sequoia', 'Sonoma', 'Ventura', 'Monterey']
# ModuleRuby = [ 'Ruby33MacPorts', 'Ruby34Brew', 'RubyAnaconda3' ]
# ModulePython = [ 'Python312MacPorts', 'Python311MacPorts',
# 'Python311Brew',
# Platform = [ 'Tahoe', 'Sequoia', 'Sonoma', 'Ventura', 'Monterey' ]
# ModuleRuby = [ 'Ruby34MacPorts', 'Ruby34Brew', 'RubyAnaconda3' ]
# ModulePython = [ 'Python313MacPorts', 'Python312MacPorts', 'Python311MacPorts',
# 'Python313Brew', 'Python312Brew', 'Python311Brew',
# 'PythonAnaconda3' ]
parameters['BuildPymodWhl'] = BuildPymodWhl
parameters['Platform'] = Platform
@ -752,12 +777,12 @@ def Get_Build_Parameters(config):
PymodDistDir = dict()
if Platform in [ 'Tahoe', 'Sequoia', 'Sonoma', 'Ventura', 'Monterey' ]:
if ModuleRuby in [ 'Ruby33MacPorts', 'Ruby34Brew', 'RubyAnaconda3' ]:
if ModulePython in [ 'Python312MacPorts', 'Python311MacPorts' ]:
if ModuleRuby in [ 'Ruby34MacPorts', 'Ruby34Brew', 'RubyAnaconda3V5', 'RubyAnaconda3V6' ]:
if ModulePython in [ 'Python313MacPorts', 'Python312MacPorts', 'Python311MacPorts' ]:
PymodDistDir[ModulePython] = 'dist-MP3-%s' % ModuleQt
elif ModulePython in [ 'Python311Brew' ]:
elif ModulePython in [ 'Python313Brew', 'Python312Brew', 'Python311Brew' ]:
PymodDistDir[ModulePython] = 'dist-HB3-%s' % ModuleQt
elif ModulePython in [ 'PythonAnaconda3' ]:
elif ModulePython in [ 'PythonAnaconda3V5', 'PythonAnaconda3V6' ]:
PymodDistDir[ModulePython] = 'dist-ana3-%s' % ModuleQt
parameters['pymod_dist'] = PymodDistDir
return parameters
@ -774,11 +799,11 @@ def Build_pymod_wheel(parameters):
#-----------------------------------------------------------------------------------------------------------
# [1] <pymod> will be built if:
# BuildPymodWhl = True
# Platform = [ 'Sequoia', 'Sonoma', 'Ventura', 'Monterey']
# ModuleRuby = [ 'Ruby33MacPorts', 'Ruby34Brew', 'RubyAnaconda3' ]
# ModulePython = [ 'Python312MacPorts', 'Python311MacPorts',
# 'Python311Brew',
# 'PythonAnaconda3' ]
# Platform = [ 'Tahoe', 'Sequoia', 'Sonoma', 'Ventura', 'Monterey' ]
# ModuleRuby = [ 'Ruby34MacPorts', 'Ruby34Brew', 'RubyAnaconda3V5', 'RubyAnaconda3V6' ]
# ModulePython = [ 'Python313MacPorts', 'Python312MacPorts', 'Python311MacPorts',
# 'Python313Brew', 'Python312Brew', 'Python311Brew',
# 'PythonAnaconda3V5', 'PythonAnaconda3V6' ]
#-----------------------------------------------------------------------------------------------------------
BuildPymodWhl = parameters['BuildPymodWhl']
Platform = parameters['Platform']
@ -788,11 +813,11 @@ def Build_pymod_wheel(parameters):
return 0
if not Platform in [ 'Tahoe', 'Sequoia', 'Sonoma', 'Ventura', 'Monterey' ]:
return 0
elif not ModuleRuby in [ 'Ruby33MacPorts', 'Ruby34Brew', 'RubyAnaconda3' ]:
elif not ModuleRuby in [ 'Ruby34MacPorts', 'Ruby34Brew', 'RubyAnaconda3V5', 'RubyAnaconda3V6' ]:
return 0
elif not ModulePython in [ 'Python312MacPorts', 'Python311MacPorts', \
'Python311Brew', \
'PythonAnaconda3' ]:
elif not ModulePython in [ 'Python313MacPorts', 'Python312MacPorts', 'Python311MacPorts', \
'Python313Brew', 'Python312Brew', 'Python311Brew', \
'PythonAnaconda3V5', 'PythonAnaconda3V6' ]:
return 0
#--------------------------------------------------------------------
@ -814,10 +839,15 @@ def Build_pymod_wheel(parameters):
addLibPath = "%s/lib" % DefaultHomebrewRoot # -- ditto --
whlTarget = "HB3"
# Using Anaconda3
elif PymodDistDir[ModulePython].find('dist-ana3') >= 0:
addBinPath = "/Applications/anaconda3/bin"
addIncPath = "/Applications/anaconda3/include"
addLibPath = "/Applications/anaconda3/lib"
elif PymodDistDir[ModulePython].find('dist-ana3-Qt5Ana3') >= 0:
addBinPath = "%s/bin" % Ana3VirEnv5
addIncPath = "%s/include" % Ana3VirEnv5
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"
else:
addBinPath = ""
@ -970,7 +1000,7 @@ def Build_pymod_wheel(parameters):
# V
# 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']
if not len(wheels) == 1:
print( "", file=sys.stderr )
@ -1022,6 +1052,7 @@ def Run_Build_Command(config, parameters):
else:
jump2pymod_wheel = True
Append_qmake_Flags()
#-----------------------------------------------------------------
# [1] Use the AddressSanitizer (ASan) in the debug build.
# This environment variable is tested in ../src/klayout.pri.
@ -1050,8 +1081,11 @@ def Run_Build_Command(config, parameters):
addLibPath = "%s/lib" % DefaultHomebrewRoot # -- ditto --
# Using Anaconda3
elif ModuleQt.upper() in [ 'QT5ANA3' ]:
addIncPath = "/Applications/anaconda3/include"
addLibPath = "/Applications/anaconda3/lib"
addIncPath = "%s/include" % Ana3VirEnv5
addLibPath = "%s/lib" % Ana3VirEnv5
elif ModuleQt.upper() in [ 'QT6ANA3' ]:
addIncPath = "%s/include" % Ana3VirEnv6
addLibPath = "%s/lib" % Ana3VirEnv6
else:
addIncPath = ""
addLibPath = ""
@ -1226,6 +1260,8 @@ def Deploy_Binaries_For_Bundle(config, parameters):
NonOSStdLang = config['NonOSStdLang']
DeploymentF = config['DeploymentF']
DeploymentP = config['DeploymentP']
PackagePrefix = config['PackagePrefix']
ModuleQt = config['ModuleQt']
MacPkgDir = config['MacPkgDir']
Version = config['Version']
DeployVerbose = config['DeployVerbose']
@ -1304,8 +1340,10 @@ def Deploy_Binaries_For_Bundle(config, parameters):
# +-- Contents/+
# +-- Info.plist
# +-- PkgInfo
# +-- PlugIns/
# +-- Resources/+
# | +-- 'klayout.icns'
# | +-- 'qt.conf'
# +-- Frameworks/+
# | +-- '*.framework'
# | +-- '*.dylib'
@ -1793,6 +1831,45 @@ def Deploy_Binaries_For_Bundle(config, parameters):
for item in glob.glob( pymodDistDir + "/*.whl" ):
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 ..." )
#------------------------------------------------------------------------------------
# [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
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(MacPkgDir)
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( "##### Finished deploying the libraries and executables for <klayout.app> #####" )
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
#------------------------------------------------------------------------------

View File

@ -6,9 +6,9 @@
#
# Here are dictionaries of ...
# 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 re
@ -27,14 +27,20 @@ XcodeToolChain = { 'nameID': '/usr/bin/install_name_tool -id ',
(System, Node, Release, MacVersion, Machine, Processor) = platform.uname()
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
HomebrewSearchPathFilter2 = '\t+@loader_path/../../../../../../../../../../opt'
HomebrewSearchPathFilter3 = '@loader_path/../../../../../../../../../../opt' # no leading white space
# 1: absolute path as seen in ~python@3.9.17
# 2: relative path as seen in python@3.9.18
else:
DefaultHomebrewRoot = '/usr/local'
else: # x86_64|Intel
DefaultHomebrewRoot = '/usr/local'
DefaultAnaconda3Root = '/Applications/anaconda3'
Ana3VirEnv5 = '%s/envs/klayout-qt5' % DefaultAnaconda3Root
Ana3VirEnv6 = '%s/envs/klayout-qt6' % DefaultAnaconda3Root
HomebrewSearchPathFilter1 = '\t+%s/opt' % DefaultHomebrewRoot
HomebrewSearchPathFilter2 = '\t+@loader_path/../../../../../../../../../../opt'
HomebrewSearchPathFilter3 = '@loader_path/../../../../../../../../../../opt' # no leading white space
@ -63,7 +69,7 @@ del System, Node, Release, MacVersion, Machine, Processor
# [1] Qt5 or Qt6
#-----------------------------------------------------
Qts = [ 'Qt5MacPorts', 'Qt5Brew', 'Qt5Ana3' ]
Qts += [ 'Qt6MacPorts', 'Qt6Brew' ]
Qts += [ 'Qt6MacPorts', 'Qt6Brew', 'Qt6Ana3' ]
#-----------------------------------------------------
# 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
}
# Qt5 bundled with anaconda3 installed under /Applications/anaconda3/
# The standard installation deploys the tool under $HOME/opt/anaconda3/.
# If so, you need to make a symbolic link: /Applications/anaconda3 ---> $HOME/opt/anaconda3/
#---------------------------------------------------------------------------------------------------
# [Apple Silicon]
# 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'
Qt5Ana3 = { 'qmake' : '/Applications/anaconda3/bin/qmake',
'deploy': '/Applications/anaconda3/bin/macdeployqt',
'libdir': '/Applications/anaconda3/lib'
Qt5Ana3 = { 'qmake' : '%s/bin/qmake' % Ana3VirEnv5,
'deploy': '%s/bin/macdeployqt' % Ana3VirEnv5,
'libdir': '%s/lib' % Ana3VirEnv5
}
#-------------------------------------------------------------------------
@ -112,12 +172,78 @@ Qt6Brew = { 'qmake' : '%s/opt/qt@6/bin/qmake' % 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]
Qt56Dictionary = { 'Qt5MacPorts': Qt5MacPorts,
'Qt5Brew' : Qt5Brew,
'Qt5Ana3' : Qt5Ana3,
'Qt6MacPorts': Qt6MacPorts,
'Qt6Brew' : Qt6Brew
'Qt6Brew' : Qt6Brew,
'Qt6Ana3' : Qt6Ana3
}
#-----------------------------------------------------
@ -129,8 +255,8 @@ Qt56Dictionary = { 'Qt5MacPorts': Qt5MacPorts,
# for the previous states.
#-----------------------------------------------------
RubyNil = [ 'nil' ]
RubySys = [ 'RubyMonterey', 'RubyVentura', 'RubySonoma', 'RubySequoia' ]
RubyExt = [ 'Ruby33MacPorts', 'Ruby34Brew', 'RubyAnaconda3' ]
RubySys = [ 'RubyMonterey', 'RubyVentura', 'RubySonoma', 'RubySequoia', 'RubyTahoe' ]
RubyExt = [ 'Ruby34MacPorts', 'Ruby34Brew', 'RubyAnaconda3' ]
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
}
# Ruby 3.3 from MacPorts (https://www.macports.org/)
# install with 'sudo port install ruby33'
# [Key Type Name] = 'MP33'
Ruby33MacPorts = { 'exe': '/opt/local/bin/ruby3.3',
'inc': '/opt/local/include/ruby-3.3.9',
'lib': '/opt/local/lib/libruby.3.3.dylib'
# Bundled with Tahoe (26.x)
# [Key Type Name] = 'Sys'
TahoeXcSDK = "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk"
TahoeCLTSDK = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"
RubyTahoe = { 'exe': '/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/bin/ruby',
'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
@ -205,24 +341,30 @@ Ruby34Brew = { 'exe': '%s/bin/ruby' % HBRuby34Path,
'lib': '%s/lib/libruby.3.4.dylib' % HBRuby34Path
}
# Ruby 3.2 bundled with anaconda3 installed under /Applications/anaconda3/
# The standard installation deploys the tool under $HOME/opt/anaconda3/.
# If so, you need to make a symbolic link: /Applications/anaconda3 ---> $HOME/opt/anaconda3/
# Ruby 3.4 installed under [/opt|/Applications]/anaconda3/envs/klayout-qt[5|6]
# See the [Qt5] [Qt6] section above.
# [Key Type Name] = 'Ana3'
RubyAnaconda3 = { 'exe': '/Applications/anaconda3/bin/ruby',
'inc': '/Applications/anaconda3/include/ruby-3.2.0',
'lib': '/Applications/anaconda3/lib/libruby.3.2.dylib'
}
RubyAnaconda3V5 = { 'exe': '%s/bin/ruby' % Ana3VirEnv5,
'inc': '%s/include/ruby-3.4.0' % Ana3VirEnv5,
'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
RubyDictionary = { 'nil' : None,
'RubyMonterey' : RubyMonterey,
'RubyVentura' : RubyVentura,
'RubySonoma' : RubySonoma,
'RubySequoia' : RubySequoia,
'Ruby33MacPorts': Ruby33MacPorts,
'Ruby34Brew' : Ruby34Brew,
'RubyAnaconda3' : RubyAnaconda3
RubyDictionary = { 'nil' : None,
'RubyMonterey' : RubyMonterey,
'RubyVentura' : RubyVentura,
'RubySonoma' : RubySonoma,
'RubySequoia' : RubySequoia,
'RubyTahoe' : RubyTahoe,
'Ruby34MacPorts' : Ruby34MacPorts,
'Ruby34Brew' : Ruby34Brew,
'RubyAnaconda3V5' : RubyAnaconda3V5,
'RubyAnaconda3V6' : RubyAnaconda3V6
}
#-----------------------------------------------------
@ -234,9 +376,9 @@ RubyDictionary = { 'nil' : None,
# for the previous states.
#-----------------------------------------------------
PythonNil = [ 'nil' ]
PythonSys = [ 'PythonMonterey', 'PythonVentura', 'PythonSonoma', 'PythonSequoia' ]
PythonExt = [ 'Python311MacPorts', 'Python312MacPorts' ]
PythonExt += [ 'Python311Brew', 'Python312Brew', 'PythonAutoBrew' ]
PythonSys = [ 'PythonMonterey', 'PythonVentura', 'PythonSonoma', 'PythonSequoia', 'PythonTahoe' ]
PythonExt = [ 'Python311MacPorts', 'Python312MacPorts', 'Python313MacPorts' ]
PythonExt += [ 'Python311Brew', 'Python312Brew', 'Python313Brew', 'PythonAutoBrew' ]
PythonExt += [ 'PythonAnaconda3' ]
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
}
# 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/)
# install with 'sudo port install python311'
# [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'
}
# 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
# install with 'brew install python@3.11'
# [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
}
# Python 3.12 bundled with anaconda3 installed under /Applications/anaconda3/
# The standard installation deploys the tool under $HOME/opt/anaconda3/.
# If so, you need to make a symbolic link: /Applications/anaconda3 ---> $HOME/opt/anaconda3/
# Python 3.13 from Homebrew
# install with 'brew install python@3.13'
# [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'
PythonAnaconda3 = { 'exe': '/Applications/anaconda3/bin/python3.12',
'inc': '/Applications/anaconda3/include/python3.12',
'lib': '/Applications/anaconda3/lib/libpython3.12.dylib'
}
PythonAnaconda3V5 = { 'exe': '%s/bin/python3.13' % Ana3VirEnv5,
'inc': '%s/include/python3.13' % Ana3VirEnv5,
'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
# install with 'brew install python'
@ -373,16 +545,20 @@ else:
_have_Homebrew_Python = True
# Consolidated dictionary kit for Python
PythonDictionary = { 'nil' : None,
'PythonMonterey' : PythonMonterey,
'PythonVentura' : PythonVentura,
'PythonSonoma' : PythonSonoma,
'PythonSequoia' : PythonSequoia,
'Python312MacPorts': Python312MacPorts,
'Python312Brew' : Python312Brew,
'PythonAnaconda3' : PythonAnaconda3,
'Python311MacPorts': Python311MacPorts,
'Python311Brew' : Python311Brew
PythonDictionary = { 'nil' : None,
'PythonMonterey' : PythonMonterey,
'PythonVentura' : PythonVentura,
'PythonSonoma' : PythonSonoma,
'PythonSequoia' : PythonSequoia,
'PythonTahoe' : PythonTahoe,
'Python313MacPorts' : Python313MacPorts,
'Python313Brew' : Python313Brew,
'PythonAnaconda3V5' : PythonAnaconda3V5,
'PythonAnaconda3V6' : PythonAnaconda3V6,
'Python312MacPorts' : Python312MacPorts,
'Python312Brew' : Python312Brew,
'Python311MacPorts' : Python311MacPorts,
'Python311Brew' : Python311Brew
}
if _have_Homebrew_Python:
PythonDictionary['PythonAutoBrew'] = PythonAutoBrew

View File

@ -6,7 +6,7 @@
#
# Here are utility functions and classes ...
# 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.
#========================================================================================
@ -131,8 +131,10 @@ def SetChangeIdentificationNameOfDyLib( libdic, pathDic ):
# +-- Contents/+
# +-- Info.plist
# +-- PkgInfo
# +-- PlugIns/
# +-- Resources/+
# | +-- 'klayout.icns'
# | +-- 'qt.conf'
# +-- Frameworks/+
# | +-- '*.framework'
# | +-- '*.dylib'
@ -146,11 +148,15 @@ def SetChangeIdentificationNameOfDyLib( libdic, pathDic ):
# | +-- pymod/
# |
# +-- Buddy/+
# +-- 'strm2cif'
# +-- 'strm2dxf'
# :
# +-- 'strmxor'
# | +-- 'strm2cif'
# | +-- 'strm2dxf'
# | :
# | +-- '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
#----------------------------------------------------------------------------------------
def SetChangeLibIdentificationName( executable, relativedir ):
@ -870,6 +876,255 @@ def DumpDependencyDicPair( title, depDic, pathDic ):
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
#----------------

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 += "<< 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 += "$ [python] ./makeDMG4mac.py\n"
Usage += " option & argument : descriptions | default value\n"
Usage += " ----------------------------------------------------------------------------------+-----------------\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 += " <-m|--make> : make a compressed DMG file | disabled\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']
LatestOS = ""
if release == 24:
if release == 25:
GenOSName = "macOS"
Platform = "Tahoe"
LatestOS = Platform
elif release == 24:
GenOSName = "macOS"
Platform = "Sequoia"
LatestOS = Platform
@ -131,7 +135,7 @@ def SetGlobals():
if not Machine == "x86_64":
# 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( "### Your Mac equips an Apple Silicon Chip ###" )
print("")
@ -293,9 +297,15 @@ def CheckPkgDirectory():
PackagePrefix = pkgdirComponents[0]
QtIdentification = pkgdirComponents[2]
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:
BackgroundPNG = "KLayoutDMG-BackQt6.png"
if Machine == "x86_64":
BackgroundPNG = "KLayoutDMG-BackQt6-X86.png"
else: # arm64
BackgroundPNG = "KLayoutDMG-BackQt6-Mx.png"
else:
BackgroundPNG = None
raise Exception( "! neither qt5 nor qt6" )
@ -319,16 +329,16 @@ def CheckPkgDirectory():
LatestOSMacPorts = Platform == LatestOS
LatestOSMacPorts &= PackagePrefix == "LW"
LatestOSMacPorts &= QtIdentification in [ "qt5MP", "qt6MP" ]
LatestOSMacPorts &= RubyPythonID in [ "Rmp33Pmp312", "Rmp33Pmp311" ]
LatestOSMacPorts &= RubyPythonID in [ "Rmp34Pmp313", "Rmp34Pmp312", "Rmp34Pmp311" ]
LatestOSHomebrew = Platform == LatestOS
LatestOSHomebrew &= PackagePrefix == "LW"
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 &= PackagePrefix == "LW"
LatestOSAnaconda3 &= QtIdentification in [ "qt5Ana3" ]
LatestOSAnaconda3 &= QtIdentification in [ "qt5Ana3", "qt6Ana3" ]
LatestOSAnaconda3 &= RubyPythonID in [ "Rana3Pana3" ]
LatestOSHomebrewH = Platform == LatestOS

View File

@ -30,14 +30,16 @@ import pandas as pd
#
# @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()
if not System == "Darwin":
return ""
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"
elif release == 23:
Platform = "Sonoma"
@ -101,15 +103,15 @@ def Get_Build_Options( targetDic, platform ):
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")
elif target == "ports":
buildOp[(qtVer, "ports", "r")] = [ '-q', '%sMacPorts' % qtType, '-r', 'MP33', '-p', 'MP312' ]
logfile[(qtVer, "ports", "r")] = "%sMP.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "release", "Rmp33Pmp312")
buildOp[(qtVer, "ports", "d")] = [ '-q', '%sMacPorts' % qtType, '-r', 'MP33', '-p', 'MP312', '--debug' ]
logfile[(qtVer, "ports", "d")] = "%sMP.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "debug", "Rmp33Pmp312")
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", "Rmp34Pmp313")
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", "Rmp34Pmp313")
elif target == "brew":
buildOp[(qtVer, "brew", "r")] = [ '-q', '%sBrew' % qtType, '-r', 'HB34', '-p', 'HB312' ]
logfile[(qtVer, "brew", "r")] = "%sBrew.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "release", "Rhb34Phb312")
buildOp[(qtVer, "brew", "d")] = [ '-q', '%sBrew' % qtType, '-r', 'HB34', '-p', 'HB312', '--debug' ]
logfile[(qtVer, "brew", "d")] = "%sBrew.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "debug", "Rhb34Phb312")
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", "Rhb34Phb313")
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", "Rhb34Phb313")
elif target == "brewHW":
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")
@ -131,10 +133,10 @@ def Get_Build_Options( targetDic, platform ):
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")
elif target == "pbrew":
buildOp[(qtVer, "pbrew", "r")] = [ '-q', '%sMacPorts' % qtType, '-r', 'HB34', '-p', 'HB312' ]
logfile[(qtVer, "pbrew", "r")] = "%sMP.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "release", "Rhb34Phb312")
buildOp[(qtVer, "pbrew", "d")] = [ '-q', '%sMacPorts' % qtType, '-r', 'HB34', '-p', 'HB312', '--debug' ]
logfile[(qtVer, "pbrew", "d")] = "%sMP.build.macos-%s-%s-%s.log" % (qtType.lower(), platform, "debug", "Rhb34Phb312")
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", "Rhb34Phb313")
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", "Rhb34Phb313")
elif target == "pbrewHW":
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")
@ -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", "d")] = '%sMP.build.macos-%s-debug-RsysPsys.macQAT' % (qtType.lower(), platform)
elif target == "ports":
dirQAT[(qtVer, "ports", "r")] = '%sMP.build.macos-%s-release-Rmp33Pmp312.macQAT' % (qtType.lower(), platform)
dirQAT[(qtVer, "ports", "d")] = '%sMP.build.macos-%s-debug-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-Rmp34Pmp313.macQAT' % (qtType.lower(), platform)
elif target == "brew":
dirQAT[(qtVer, "brew", "r")] = '%sBrew.build.macos-%s-release-Rhb34Phb312.macQAT' % (qtType.lower(), platform)
dirQAT[(qtVer, "brew", "d")] = '%sBrew.build.macos-%s-debug-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-Rhb34Phb313.macQAT' % (qtType.lower(), platform)
elif target == "brewHW":
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)
@ -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", "d")] = '%sBrew.build.macos-%s-debug-RsysPhbauto.macQAT' % (qtType.lower(), platform)
elif target == "pbrew":
dirQAT[(qtVer, "pbrew", "r")] = '%sMP.build.macos-%s-release-Rhb34Phb312.macQAT' % (qtType.lower(), platform)
dirQAT[(qtVer, "pbrew", "d")] = '%sMP.build.macos-%s-debug-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-Rhb34Phb313.macQAT' % (qtType.lower(), platform)
elif target == "pbrewHW":
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)
@ -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),
'-s', '%d' % srlDMG, '%s' % flag ]
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 ]
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 ]
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 ]
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 ]
elif target == "brewHW":
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),
'-s', '%d' % srlDMG, '%s' % flag ]
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 ]
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 ]
elif target == "pbrewHW":
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 QtTarget # list of (Qt, target, bdType)-tuple
global Build # operation flag
global Deploy # operation flag
global WithPymod # operation flag
global QATest # operation flag
global QACheck # operation flag
@ -296,7 +299,7 @@ def Parse_CommandLine_Arguments():
global DryRun # True for dry-run
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"
else:
targetopt = ""
@ -320,10 +323,11 @@ def Parse_CommandLine_Arguments():
Usage += " + You can use this option multiple times. |\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 += " [--build] : build and deploy | disabled\n"
Usage += " [--pymod] : build and deploy Pymod, too (release build only) | disabled\n"
Usage += " [--test] : run the QA Test | disabled\n"
Usage += " [--check] : check the QA Test results | disabled\n"
Usage += " [--build] : build and deploy | disabled\n"
Usage += " [--deploy] : deploy only | disabled\n"
Usage += " [--pymod] : build and deploy Pymod, too (release build only) | 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 += " [--upload <dropbox>] : upload DMGs to $HOME/Dropbox/klayout/<dropbox> | disabled\n"
Usage += " [--dryrun] : dry-run for --build option | disabled\n"
@ -363,6 +367,12 @@ def Parse_CommandLine_Arguments():
default=False,
help='build and deploy' )
p.add_option( '--deploy',
action='store_true',
dest='deploy',
default=False,
help='deploy only' )
p.add_option( '--pymod',
action='store_true',
dest='with_pymod',
@ -409,6 +419,7 @@ def Parse_CommandLine_Arguments():
targets = "%s" % targetopt,
qt_target = list(),
build = False,
deploy = False,
with_pymod = False,
qa_test = False,
qa_check = False,
@ -421,24 +432,28 @@ def Parse_CommandLine_Arguments():
opt, args = p.parse_args()
if opt.checkusage:
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 == "":
print( "! Current platform is not [ 'Monterey', 'Ventura', 'Sonoma', 'Sequoia' ]" )
print( "! Current platform is not [ 'Monterey', 'Ventura', 'Sonoma', 'Sequoia', 'Tahoe' ]" )
print(Usage)
quit()
sys.exit(0)
QtType = int(opt.qt_type)
if not QtType in [5, 6]:
print( "! Invalid Qt type <%d>" % QtType )
print(Usage)
quit()
sys.exit(0)
targetIdx = list()
for target in [ int(item) for item in opt.targets.split(",") ]:
if not target in targetIdx:
targetIdx.append(target) # first appeared and non-duplicated index
raw = (opt.targets or "").strip()
targets = [int(item) for item in raw.split(",") if item.strip() != ""]
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()
Target = list()
@ -462,14 +477,14 @@ def Parse_CommandLine_Arguments():
if len(df) == 0:
print( "! --qttarget==nightlyBuild.csv is used but DataFrame is empty" )
print(Usage)
quit()
sys.exit(0)
for i in range(0, len(df)):
qt = df.iloc[i,0]
idx = df.iloc[i,1]
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 \
(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) )
elif len(opt.qt_target) > 0:
QtTarget = list()
@ -480,7 +495,7 @@ def Parse_CommandLine_Arguments():
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 \
(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) )
else:
withqttarget = False
@ -492,9 +507,10 @@ def Parse_CommandLine_Arguments():
else:
print( "! --qttarget is used but there is no valid (Qt, target, bdTye)-tuple" )
print(Usage)
quit()
sys.exit(0)
Build = opt.build
Deploy = opt.deploy
WithPymod = opt.with_pymod
QATest = opt.qa_test
QACheck = opt.qa_check
@ -514,27 +530,27 @@ def Parse_CommandLine_Arguments():
if MakeDMG and CleanDMG:
print( "! --makedmg and --cleandmg cannot be used simultaneously" )
print(Usage)
quit()
sys.exit(0)
if not opt.upload == "":
Upload = True
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(Usage)
quit()
sys.exit(0)
#------------------------------------------------------------------------------
## To build and deploy
#------------------------------------------------------------------------------
def Build_Deploy():
def Build_Deploy( deployonly=False ):
pyBuilder = "./build4mac.py"
myPlatform = Test_My_Platform()
buildOp, logfile = Get_Build_Options( Get_Build_Target_Dict(), myPlatform )
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
deplog = logfile[(qttype, key, bdType)].replace( ".log", ".dep.log" )
@ -565,19 +581,20 @@ def Build_Deploy():
print( "" )
continue
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 )
sys.exit(1)
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 )
if not deployonly:
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 )
sys.exit(1)
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 )
if subprocess.call( command2, shell=True ) != 0:
print( "", file=sys.stderr )
@ -604,7 +621,7 @@ def Run_QATest( excludeList ):
dirQAT = Get_QAT_Directory( Get_Build_Target_Dict(), myPlatform )
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
if key == "ana3":
@ -645,11 +662,19 @@ def Check_QATest_Results( lines ):
dirQAT = Get_QAT_Directory( Get_Build_Target_Dict(), myPlatform )
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
os.chdir( dirQAT[(qttype, key, bdType)] )
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
print( dirQAT[(qttype, key, bdType)], command1 )
#continue
@ -686,7 +711,7 @@ def DMG_Make( srlDMG ):
os.mkdir( stashDMG )
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
command1 = [ pyDMGmaker ] + packOp[(qttype, key, bdType)]
@ -726,7 +751,7 @@ def DMG_Clean( srlDMG ):
shutil.rmtree( stashDMG )
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
command1 = [ pyDMGmaker ] + packOp[(qttype, key, bdType)]
@ -770,7 +795,10 @@ def Main():
Parse_CommandLine_Arguments()
if Build:
Build_Deploy()
Build_Deploy(deployonly=False)
if Deploy:
Build_Deploy(deployonly=True)
sys.exit(0)
if QATest:
Run_QATest( [] ) # ex. ['pymod', 'pya']
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
# Qt6 comes with C++ 17 requirement.
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
}