diff --git a/macbuild/KLayoutNightlyBuild.Bash b/macbuild/KLayoutNightlyBuild.Bash new file mode 100755 index 000000000..89e5add92 --- /dev/null +++ b/macbuild/KLayoutNightlyBuild.Bash @@ -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 " +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 < 10 seconds + # 5m => 5 minutes +} + +####---------------------------------------------------------------------- +#### Main +####---------------------------------------------------------------------- +Initialize +Build +exit $? + +# EOF diff --git a/macbuild/ReadMe.md b/macbuild/ReadMe.md index 79f408a12..4bf2aeb5b 100644 --- a/macbuild/ReadMe.md +++ b/macbuild/ReadMe.md @@ -1,16 +1,18 @@ -Relevant KLayout version: 0.30.2
+Relevant KLayout version: 0.30.5
Author: Kazzz-S
-Last modified: 2025-05-30
+Last modified: 2025-11-10
# 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.
Pre-built DMG packages are also not provided.
-* 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.
* El Capitan (10.11) Throughout this document, the primary target machine is **Intel x86_64** with **macOS Sequoia**.
-All Apple (M1|M2|M3|M4) chips are still untested, as the author does not own an (M1|M2|M3|M4) Mac.
-However, some kind volunteers told me they successfully built on an Apple silicon machine.
+The author recently acquired an M4 Mac Mini and is attempting to build a native ARM64 version in the Tahoe environment **experimentally**.
+Therefore, this document does not include detailed build procedures for Apple Silicon environments.
+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.
+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.
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).
-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 ] : 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 ] : case-insensitive type=['nil', 'Sys', 'MP33', 'HB34', 'Ana3'] | sys + [-r|--ruby ] : 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 ] : case-insensitive type=['nil', 'Sys', 'MP312', 'HB312', 'Ana3', | sys + : Ana3: use Ruby 3.4 from Anaconda3 | + [-p|--python ] : 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.
@@ -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:
- **`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).
-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.
The buddy command-line tools (strm*) will also be deployed under **klayout.app/Contents/Buddy/** in this step.
``` -$ ./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:
- **`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.
@@ -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:
- **`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.
@@ -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:
-* **`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.
diff --git a/macbuild/Resources/KLayoutDMG-BackQt5-Mx.logoist b/macbuild/Resources/KLayoutDMG-BackQt5-Mx.logoist new file mode 100644 index 000000000..3c0a6aa8a Binary files /dev/null and b/macbuild/Resources/KLayoutDMG-BackQt5-Mx.logoist differ diff --git a/macbuild/Resources/KLayoutDMG-BackQt5-Mx.png b/macbuild/Resources/KLayoutDMG-BackQt5-Mx.png new file mode 100644 index 000000000..239bac023 Binary files /dev/null and b/macbuild/Resources/KLayoutDMG-BackQt5-Mx.png differ diff --git a/macbuild/Resources/KLayoutDMG-BackQt5-X86.logoist b/macbuild/Resources/KLayoutDMG-BackQt5-X86.logoist new file mode 100644 index 000000000..a2a6f5fd2 Binary files /dev/null and b/macbuild/Resources/KLayoutDMG-BackQt5-X86.logoist differ diff --git a/macbuild/Resources/KLayoutDMG-BackQt5-X86.png b/macbuild/Resources/KLayoutDMG-BackQt5-X86.png new file mode 100644 index 000000000..49c9d6365 Binary files /dev/null and b/macbuild/Resources/KLayoutDMG-BackQt5-X86.png differ diff --git a/macbuild/Resources/KLayoutDMG-BackQt5.logoist b/macbuild/Resources/KLayoutDMG-BackQt5.logoist deleted file mode 100644 index 7dfa0e137..000000000 Binary files a/macbuild/Resources/KLayoutDMG-BackQt5.logoist and /dev/null differ diff --git a/macbuild/Resources/KLayoutDMG-BackQt5.png b/macbuild/Resources/KLayoutDMG-BackQt5.png deleted file mode 100644 index 479459662..000000000 Binary files a/macbuild/Resources/KLayoutDMG-BackQt5.png and /dev/null differ diff --git a/macbuild/Resources/KLayoutDMG-BackQt6-Mx.logoist b/macbuild/Resources/KLayoutDMG-BackQt6-Mx.logoist new file mode 100644 index 000000000..fd54e99c2 Binary files /dev/null and b/macbuild/Resources/KLayoutDMG-BackQt6-Mx.logoist differ diff --git a/macbuild/Resources/KLayoutDMG-BackQt6-Mx.png b/macbuild/Resources/KLayoutDMG-BackQt6-Mx.png new file mode 100644 index 000000000..3c90f7394 Binary files /dev/null and b/macbuild/Resources/KLayoutDMG-BackQt6-Mx.png differ diff --git a/macbuild/Resources/KLayoutDMG-BackQt6-X86.logoist b/macbuild/Resources/KLayoutDMG-BackQt6-X86.logoist new file mode 100644 index 000000000..14ad62250 Binary files /dev/null and b/macbuild/Resources/KLayoutDMG-BackQt6-X86.logoist differ diff --git a/macbuild/Resources/KLayoutDMG-BackQt6-X86.png b/macbuild/Resources/KLayoutDMG-BackQt6-X86.png new file mode 100644 index 000000000..d266966e5 Binary files /dev/null and b/macbuild/Resources/KLayoutDMG-BackQt6-X86.png differ diff --git a/macbuild/Resources/KLayoutDMG-BackQt6.logoist b/macbuild/Resources/KLayoutDMG-BackQt6.logoist deleted file mode 100644 index 30e9a57d1..000000000 Binary files a/macbuild/Resources/KLayoutDMG-BackQt6.logoist and /dev/null differ diff --git a/macbuild/Resources/KLayoutDMG-BackQt6.png b/macbuild/Resources/KLayoutDMG-BackQt6.png deleted file mode 100644 index 556770b3b..000000000 Binary files a/macbuild/Resources/KLayoutDMG-BackQt6.png and /dev/null differ diff --git a/macbuild/Resources/icon-resources/M-SeriesNative.logoist b/macbuild/Resources/icon-resources/M-SeriesNative.logoist new file mode 100644 index 000000000..fb8f343ba Binary files /dev/null and b/macbuild/Resources/icon-resources/M-SeriesNative.logoist differ diff --git a/macbuild/Resources/icon-resources/M-SeriesNative.png b/macbuild/Resources/icon-resources/M-SeriesNative.png new file mode 100644 index 000000000..fbbe22acc Binary files /dev/null and b/macbuild/Resources/icon-resources/M-SeriesNative.png differ diff --git a/macbuild/Resources/icon-resources/x86_64.logoist b/macbuild/Resources/icon-resources/x86_64.logoist new file mode 100644 index 000000000..2780a477e Binary files /dev/null and b/macbuild/Resources/icon-resources/x86_64.logoist differ diff --git a/macbuild/Resources/icon-resources/x86_64.png b/macbuild/Resources/icon-resources/x86_64.png new file mode 100644 index 000000000..6ee8ffe43 Binary files /dev/null and b/macbuild/Resources/icon-resources/x86_64.png differ diff --git a/macbuild/Resources/qt.conf b/macbuild/Resources/qt.conf index df50e9dba..87849f625 100644 --- a/macbuild/Resources/qt.conf +++ b/macbuild/Resources/qt.conf @@ -1,4 +1,5 @@ [Paths] -Plugins = ../PlugIns -Imports = ../Resources/qml -Qml2Imports = ../Resources/qml +Prefix = .. +Plugins = PlugIns +Imports = Resources/qml +Qml2Imports = Resources/qml diff --git a/macbuild/Resources/script-bundle-A.zip b/macbuild/Resources/script-bundle-A.zip index 1260f8645..ec40f25b0 100644 Binary files a/macbuild/Resources/script-bundle-A.zip and b/macbuild/Resources/script-bundle-A.zip differ diff --git a/macbuild/Resources/script-bundle-B.zip b/macbuild/Resources/script-bundle-B.zip index 45ca4140b..4428a3f0e 100644 Binary files a/macbuild/Resources/script-bundle-B.zip and b/macbuild/Resources/script-bundle-B.zip differ diff --git a/macbuild/Resources/script-bundle-H.zip b/macbuild/Resources/script-bundle-H.zip index 9cd7530f7..b25794db4 100644 Binary files a/macbuild/Resources/script-bundle-H.zip and b/macbuild/Resources/script-bundle-H.zip differ diff --git a/macbuild/Resources/script-bundle-P.zip b/macbuild/Resources/script-bundle-P.zip index 4a29233d1..faa4c33e6 100644 Binary files a/macbuild/Resources/script-bundle-P.zip and b/macbuild/Resources/script-bundle-P.zip differ diff --git a/macbuild/Resources/script-bundle-S.zip b/macbuild/Resources/script-bundle-S.zip index 9addf53ae..c9f66a282 100644 Binary files a/macbuild/Resources/script-bundle-S.zip and b/macbuild/Resources/script-bundle-S.zip differ diff --git a/macbuild/build4mac.py b/macbuild/build4mac.py index fdcff9b45..abd8d87e0 100755 --- a/macbuild/build4mac.py +++ b/macbuild/build4mac.py @@ -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 ] : 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 ] : case-insensitive type=['nil', 'Sys', 'MP33', 'HB34', 'Ana3'] | %s\n" % myRuby + usage += " [-r|--ruby ] : 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 ] : case-insensitive type=['nil', 'Sys', 'MP312', 'HB312', 'Ana3', | %s\n" % myPython + usage += " : Ana3: use Ruby 3.4 from Anaconda3 |\n" + usage += " [-p|--python ] : 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 # 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] 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 #####" ) 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 #------------------------------------------------------------------------------ diff --git a/macbuild/build4mac_env.py b/macbuild/build4mac_env.py index 0faed651b..349b46669 100755 --- a/macbuild/build4mac_env.py +++ b/macbuild/build4mac_env.py @@ -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 diff --git a/macbuild/build4mac_util.py b/macbuild/build4mac_util.py index 7acc1fe47..545c0dc4a 100755 --- a/macbuild/build4mac_util.py +++ b/macbuild/build4mac_util.py @@ -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 #---------------- diff --git a/macbuild/bundle_qtconf.py b/macbuild/bundle_qtconf.py new file mode 100755 index 000000000..9fead5a10 --- /dev/null +++ b/macbuild/bundle_qtconf.py @@ -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=) + +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 /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() diff --git a/macbuild/cleanQAT.sh b/macbuild/cleanQAT.sh new file mode 100755 index 000000000..11918f64e --- /dev/null +++ b/macbuild/cleanQAT.sh @@ -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 diff --git a/macbuild/mac_no_agl.pri b/macbuild/mac_no_agl.pri new file mode 100644 index 000000000..028c39991 --- /dev/null +++ b/macbuild/mac_no_agl.pri @@ -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 it’s 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.") +} \ No newline at end of file diff --git a/macbuild/makeDMG4mac.py b/macbuild/makeDMG4mac.py index 7c62caab3..b0e567240 100755 --- a/macbuild/makeDMG4mac.py +++ b/macbuild/makeDMG4mac.py @@ -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 > : 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 diff --git a/macbuild/nightlyBuild.py b/macbuild/nightlyBuild.py index 8c3d51e27..994dbc941 100755 --- a/macbuild/nightlyBuild.py +++ b/macbuild/nightlyBuild.py @@ -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 ] : make or clean DMGs | disabled\n" Usage += " [--upload ] : upload DMGs to $HOME/Dropbox/klayout/ | 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: diff --git a/macbuild/pureARM64.py b/macbuild/pureARM64.py new file mode 100755 index 000000000..86557db9c --- /dev/null +++ b/macbuild/pureARM64.py @@ -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) diff --git a/src/plugins/streamers/lstream/lstream.pri b/src/plugins/streamers/lstream/lstream.pri index 05b84310e..a3a6b2991 100644 --- a/src/plugins/streamers/lstream/lstream.pri +++ b/src/plugins/streamers/lstream/lstream.pri @@ -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 }