From 012e8e1e334b330c9cd11b5b5496918655f729b2 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Mon, 22 Jun 2026 21:53:47 -0500 Subject: [PATCH 01/20] chore: add sync sentinel markers; simplify unsupported-version fallback --- README.md | 2 ++ install-openmp.sh | 19 +++++-------------- tests/test-markers.sh | 21 +++++++++++++++++++++ 3 files changed, 28 insertions(+), 14 deletions(-) create mode 100644 tests/test-markers.sh diff --git a/README.md b/README.md index 18d115a..8beea6f 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ chmod +x uninstall-openmp.sh | Xcode Version | Apple Clang | OpenMP Version | Download | |---------------|-------------|----------------|----------| + | 16.3+ | 1700.x | 19.1.0 | [openmp-19.1.0-darwin20-Release.tar.gz](https://mac.r-project.org/openmp/openmp-19.1.0-darwin20-Release.tar.gz) | | 16.0-16.2 | 1600.x | 17.0.6 | [openmp-17.0.6-darwin20-Release.tar.gz](https://mac.r-project.org/openmp/openmp-17.0.6-darwin20-Release.tar.gz) | | 15.x | 1500.x | 16.0.4 | [openmp-16.0.4-darwin20-Release.tar.gz](https://mac.r-project.org/openmp/openmp-16.0.4-darwin20-Release.tar.gz) | @@ -49,6 +50,7 @@ chmod +x uninstall-openmp.sh | 11.4-11.7 | 1103.x | 9.0.1 | [openmp-9.0.1-darwin17-Release.tar.gz](https://mac.r-project.org/openmp/openmp-9.0.1-darwin17-Release.tar.gz) | | 11.0-11.3.1 | 1100.x | 8.0.1 | [openmp-8.0.1-darwin17-Release.tar.gz](https://mac.r-project.org/openmp/openmp-8.0.1-darwin17-Release.tar.gz) | | 10.2-10.3 | 1001.x | 7.1.0 | [openmp-7.1.0-darwin17-Release.tar.gz](https://mac.r-project.org/openmp/openmp-7.1.0-darwin17-Release.tar.gz) | + > [!NOTE] > diff --git a/install-openmp.sh b/install-openmp.sh index d215bcf..a53015c 100755 --- a/install-openmp.sh +++ b/install-openmp.sh @@ -63,6 +63,7 @@ show_help() { echo " - sudo privileges for installation to /usr/local/" echo "" echo -e "${YELLOW}SUPPORTED VERSIONS:${NC}" + # >>> BEGIN GENERATED HELP VERSIONS (managed by sync-openmp.sh; do not edit) >>> echo " Xcode 16.3+ → OpenMP 19.1.0" echo " Xcode 16.0-16.2 → OpenMP 17.0.6" echo " Xcode 15.x → OpenMP 16.0.4" @@ -75,6 +76,7 @@ show_help() { echo " Xcode 11.4-11.7 → OpenMP 9.0.1" echo " Xcode 11.0-11.3.1 → OpenMP 8.0.1" echo " Xcode 10.2-10.3 → OpenMP 7.1.0" + # <<< END GENERATED HELP VERSIONS <<< echo "" echo "MORE INFO:" echo " https://mac.r-project.org/openmp/" @@ -140,6 +142,7 @@ EXPECTED_SHA1="" BASE_URL="https://mac.r-project.org/openmp" case $CLANG_VERSION in + # >>> BEGIN GENERATED VERSION CASES (managed by sync-openmp.sh; do not edit) >>> 1700) OPENMP_VERSION="19.1.0" DARWIN_TARGET="darwin20" @@ -200,22 +203,10 @@ case $CLANG_VERSION in DARWIN_TARGET="darwin17" EXPECTED_SHA1="6891ff6f83f2ed83eeed42160de819b50cf643cd" ;; + # <<< END GENERATED VERSION CASES <<< *) echo -e "${RED}Error: Unsupported clang version $CLANG_VERSION${NC}" - echo "Supported versions and their corresponding OpenMP builds:" - echo " 1700 (Xcode 16.3+) → OpenMP 19.1.0" - echo " 1600 (Xcode 16.0-16.2) → OpenMP 17.0.6" - echo " 1500 (Xcode 15.x) → OpenMP 16.0.4" - echo " 1403 (Xcode 14.3.x) → OpenMP 15.0.7" - echo " 1400 (Xcode 14.0-14.2) → OpenMP 14.0.6" - echo " 1316 (Xcode 13.3-13.4.1) → OpenMP 13.0.0" - echo " 1300 (Xcode 13.0-13.2.1) → OpenMP 12.0.1" - echo " 1205 (Xcode 12.5) → OpenMP 11.0.1" - echo " 1200 (Xcode 12.0-12.4) → OpenMP 10.0.0" - echo " 1103 (Xcode 11.4-11.7) → OpenMP 9.0.1" - echo " 1100 (Xcode 11.0-11.3.1) → OpenMP 8.0.1" - echo " 1001 (Xcode 10.2-10.3) → OpenMP 7.1.0" - echo "" + echo "Run '$0 --help' to see the supported Xcode/clang/OpenMP versions." echo "Please check https://mac.r-project.org/openmp/ for updates." exit 1 ;; diff --git a/tests/test-markers.sh b/tests/test-markers.sh new file mode 100644 index 0000000..788e0ec --- /dev/null +++ b/tests/test-markers.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail +DIR="$(cd "$(dirname "$0")/.." && pwd)" +fail() { echo "FAIL: $1" >&2; exit 1; } + +# Every BEGIN marker has a matching END marker in each file. +for f in install-openmp.sh README.md; do + b=$(grep -c 'BEGIN GENERATED' "$DIR/$f" || true) + e=$(grep -c 'END GENERATED' "$DIR/$f" || true) + [ "$b" -ge 1 ] || fail "$f: no BEGIN markers" + [ "$b" = "$e" ] || fail "$f: $b BEGIN vs $e END markers" +done + +# install-openmp.sh must still be valid bash. +bash -n "$DIR/install-openmp.sh" || fail "install-openmp.sh has a syntax error" + +# Required generated-block names are present. +grep -q 'BEGIN GENERATED VERSION CASES' "$DIR/install-openmp.sh" || fail "missing VERSION CASES block" +grep -q 'BEGIN GENERATED HELP VERSIONS' "$DIR/install-openmp.sh" || fail "missing HELP VERSIONS block" +grep -q 'BEGIN GENERATED README TABLE' "$DIR/README.md" || fail "missing README TABLE block" +echo "PASS: test-markers" From 2132baa06c8e973037e2085f7883fe96e6bb8fc5 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Mon, 22 Jun 2026 21:57:13 -0500 Subject: [PATCH 02/20] test: add bash test harness and upstream HTML fixture --- tests/fixtures/golden-records.tsv | 12 ++ tests/fixtures/page.html | 239 ++++++++++++++++++++++++++++++ tests/helpers.sh | 9 ++ tests/run-tests.sh | 9 ++ 4 files changed, 269 insertions(+) create mode 100644 tests/fixtures/golden-records.tsv create mode 100644 tests/fixtures/page.html create mode 100644 tests/helpers.sh create mode 100755 tests/run-tests.sh diff --git a/tests/fixtures/golden-records.tsv b/tests/fixtures/golden-records.tsv new file mode 100644 index 0000000..9b01a3d --- /dev/null +++ b/tests/fixtures/golden-records.tsv @@ -0,0 +1,12 @@ +1700 19.1.5 darwin20 5b44175bcbaa334b0c57391482e068ea185c95a2 Xcode 16.3-26.3 +1600 17.0.6 darwin20 a89cab4e763025f03a5d12a93a609ff771ad209c Xcode 16.0-16.2 +1500 16.0.4 darwin20 591136d3c1cc26f3a21f1202a652be911bf1a2ad Xcode 15.x +1403 15.0.7 darwin20 31f0be747101b2bdce3c01b4d1c9041959bb3b27 Xcode 14.3.x +1400 14.0.6 darwin20 19912991431ecf032f037b6e8aea19dbd490f1ba Xcode 14.0-14.2 +1316 13.0.0 darwin21 47af4cb0d1f3554969f2ec9dee450d728ea30024 Xcode 13.3-13.4.1 +1300 12.0.1 darwin20 4fab53ccc420ab882119256470af15c210d19e5e Xcode 13.0-13.2.1 +1205 11.0.1 darwin20 0dcd19042f01c4f552914e2cf7a53186de397aa1 Xcode 12.5 +1200 10.0.0 darwin17 9bf16a64ab747528c5de7005a1ea1a9e318b3cf0 Xcode 12.0-12.4 +1103 9.0.1 darwin17 e5bd8501a3f957b4babe27b0a266d4fa15dbc23f Xcode 11.4-11.7 +1100 8.0.1 darwin17 e4612bfcb1bf520bf22844f7db764cadb7577c28 Xcode 11.0-11.3.1 +1001 7.1.0 darwin17 6891ff6f83f2ed83eeed42160de819b50cf643cd Xcode 10.2-10.3 diff --git a/tests/fixtures/page.html b/tests/fixtures/page.html new file mode 100644 index 0000000..3fc01cc --- /dev/null +++ b/tests/fixtures/page.html @@ -0,0 +1,239 @@ + + + OpenMP on macOS with Xcode tools + + + +

OpenMP on macOS with Xcode tools

+

+

+ Warning! Everything described on this page is strictly + experimental and not officially supported by Apple. + It may break at any time. The information is + provided in the hope of being useful to some tech-savvy + people. It is not intended for the regular R user. +
+

+ For those impatient, skip to how to enable OpenMP in + packages. +

+

OpenMP support in Xcode

+

+ Apple has explicitly disabled OpenMP support in compilers that + they ship in Xcode: + +

   $ clang -c omp.c -fopenmp
+   clang: error: unsupported option '-fopenmp'
+ + even though clang had OpenMP support for quite a long + time now (great thanks to the folks at Intel providing their + library as open source!). In fact, the clang compiler in Xcode + can generate all the necessary code for OpenMP. It can be tricked + into performing its designed function by using + -Xclang -fopenmp flags. +

+

+ The unfortunate part about this is that Apple is not shipping the + necesssary libomp.dylib run-time library needed for + OpenMP support. Fortunately, some clever folks were able to + + match the versions so we can build the binaries + that correspond to the clang version used. It is + sometimes possible to use a more recent version of the runtime + than the version of Apple clang. A notable exception is Xcode 16.3 + (Apple clang 1700+) which is incompatible with previous versions. + +

OpenMP run-time downloads

+ The following are links to libomp OpenMP run-time built + from official LLVM release sources using Xcode compilers. They + are signed and support macOS 10.13 (High Sierra) and higher + (x86_64) or macOS 11 (Big Sur) or higher (arm64). All + tar-balls contain the system tree usr/local/lib + and usr/local/include so the recommended installation is to type in Terminal: +
+    curl -O https://mac.r-project.org/openmp/openmp-17.0.6-darwin20-Release.tar.gz
+    sudo tar fvxz openmp-17.0.6-darwin20-Release.tar.gz -C /
+

NOTE: Do NOT use a browser to download the tar balls, because it will quarantine the downloaded file and its contents. Modern macOS security doesn't allow the use of quarantined libraries so you'd have to remove the quarantine first with xattr -c if you do so.

+

The contained set of files is the same in all tar balls + (except 19.1.5 which adds ompx.h) +

+    usr/local/lib/libomp.dylib
+    usr/local/include/ompt.h
+    usr/local/include/omp.h
+    usr/local/include/omp-tools.h
+ so you can simply remove those to uninstall. Note that any package + you compile against libomp.dylib will need that run-time + so you have to ship it with your package or have users install it. + Note, however, that CRAN R ships with libomp.dylib + from here in $R_HOME/lib (corresponding to the Xcode + version used on CRAN) so if you link against that location (recommended) + you don't have to ship it if users are using CRAN binaries of R + as long as you make sure you use a compatible run-time. +

+ You can verify the signature in the library via codetool (see below). +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BuildDownloadSHA1 checksum
LLVM 19.1.5
Xcode 16.3-26.3 (Apple clang 1700.x)
openmp-19.1.5-darwin20-Release.tar.gz (Release)
+ openmp-19.1.5-darwin20-Debug.tar.gz (Debug)
5b44175bcbaa334b0c57391482e068ea185c95a2
759bc6348431c793b22438f6bd714af59c071036
LLVM 18.1.8
 
openmp-18.1.8-darwin20-Release.tar.gz (Release)
+ openmp-18.1.8-darwin20-Debug.tar.gz (Debug)
b8e7b79d265310ba12672e117df48ccfd9ce0366
115629b4ca79af7155a0bbe76932e7a2c9f3c313
LLVM 17.0.6
Xcode 16.0-16.2 (Apple clang 1600.x)
openmp-17.0.6-darwin20-Release.tar.gz (Release)
+ openmp-17.0.6-darwin20-Debug.tar.gz (Debug)
a89cab4e763025f03a5d12a93a609ff771ad209c
d74afdd50cd1fb4d17bdb45c2467ffeea530b1a2
LLVM 16.0.4
Xcode 15.x (Apple clang 1500.x)
openmp-16.0.4-darwin20-Release.tar.gz (Release)
+ openmp-16.0.4-darwin20-Debug.tar.gz (Debug)
591136d3c1cc26f3a21f1202a652be911bf1a2ad
1253f7157f590804031095440ffd80aac016b101
+
LLVM 15.0.7
Xcode 14.3.x (Apple clang 1403.x)
openmp-15.0.7-darwin20-Release.tar.gz (Release)
+ openmp-15.0.7-darwin20-Debug.tar.gz (Debug)
31f0be747101b2bdce3c01b4d1c9041959bb3b27
17728c6592d341400bb6076add9c2524a95305b7
LLVM 14.0.6
Xcode 14.0-14.2 (Apple clang 1400.x)
openmp-14.0.6-darwin20-Release.tar.gz (Release)
+ openmp-14.0.6-darwin20-Debug.tar.gz (Debug)
19912991431ecf032f037b6e8aea19dbd490f1ba
6b96a15db9329ea6f605449a630575036fa20aae
LLVM 13.0.0
Xcode 13.3-13.4.1 (Apple clang 1316.x)
openmp-13.0.0-darwin21-Release.tar.gz (Release)
+ openmp-13.0.0-darwin21-Debug.tar.gz (Debug)
47af4cb0d1f3554969f2ec9dee450d728ea30024
f6f8f1f49c02d5ec0729b56ddc7eaf51e7f04714
LLVM 12.0.1
Xcode 13.0-13.2.1 (Apple clang 1300.x)
openmp-12.0.1-darwin20-Release.tar.gz (Release)
+ openmp-12.0.1-darwin20-Debug.tar.gz (Debug)
4fab53ccc420ab882119256470af15c210d19e5e
58b4323e7933e12cba5c2996b4c9ef27567c41d9
LLVM 11.0.1 (+M1 patch)
Xcode 12.5 (Apple clang 1205.x)
openmp-11.0.1-darwin20-Release.tar.gz (Release)
+ openmp-11.0.1-darwin20-Debug.tar.gz (Debug)
0dcd19042f01c4f552914e2cf7a53186de397aa1
65e83ea667c72bbe44fea699776564d2f03a080f
(All binaries above include both arm64 and x86_64 and require macOS 11 or higher. Binaries below require macOS 10.13 or higher and are Intel-only)
LLVM 10.0.0
Xcode 12.0-12.4 (Apple clang 1200.x)
openmp-10.0.0-darwin17-Release.tar.gz (Release)
+ openmp-10.0.0-darwin17-Debug.tar.gz (Debug)
9bf16a64ab747528c5de7005a1ea1a9e318b3cf0
d4508d3f0c2952c3f984393b088e0b4beab33b58
LLVM 9.0.1
Xcode 11.4-11.7 (Apple clang 1103.x)
openmp-9.0.1-darwin17-Release.tar.gz (Release)
+ openmp-9.0.1-darwin17-Debug.tar.gz (Debug)
e5bd8501a3f957b4babe27b0a266d4fa15dbc23f
c4c8491631504fb060f7c25ec14324d02d617d5b
LLVM 8.0.1
Xcode 11.0-11.3.1 (Apple clang 1100.x)
openmp-8.0.1-darwin17-Release.tar.gz (Release)
+ openmp-8.0.1-darwin17-Debug.tar.gz (Debug)
e4612bfcb1bf520bf22844f7db764cadb7577c28
d6c83918b28405d43950d4b864ca8d1687eed4d1
LLVM 7.1.0
Xcode 10.2-10.3 (Apple clang 1001.x)
openmp-7.1.0-darwin17-Release.tar.gz (Release)
+ openmp-7.1.0-darwin17-Debug.tar.gz (Debug)
6891ff6f83f2ed83eeed42160de819b50cf643cd
34456adde62b9a1047f906e1d7f54990a1c15a34
+ +

How to enable OpenMP in packages

+ + How you do the latter depends on the package, but if the package does + not set these environment variables itself, you can try +
+    PKG_CPPFLAGS='-Xclang -fopenmp' PKG_LIBS=-lomp R CMD INSTALL myPackage
+ + If that doesn't work, please consult the package's documentation, and + liaise with its maintainer. + + It is also possible to add those flags globally by + adding the following to ~/.R/Makevars: +
+    CPPFLAGS += -Xclang -fopenmp
+    LDFLAGS += -lomp
+ + but be very careful when doing this, always check + your ~/.R/Makevars whenever you upgrade R, macOS or Xcode! + +

Note: If you are using CRAN R binary and there is a potential + conflict between its run-time and your run-time (which is generally + a bad idea!) then you can force the use of your binary by + replacing -lomp above with /usr/local/lib/libomp.dylib + (note that lack of -l!).

+ +

Side notes

+ It may be possible in principle to build static version of the run-time. + That can be done via -DLIBOMP_ENABLE_SHARED=OFF, but has not been + tested. There is a potential for chaos when more OMP run-times get + loaded into one process as they may clash, in fact it is strongly + discouraged to use static run-time, because only one version the + run-time may be loaded into a process. That is why R package are + encouraged to use the common run-time supplied with CRAN R + binaries instead of shipping their own (see above). +

+ Another interesting test would be to + try -DLIBOMP_FORTRAN_MODULES=ON and see which versions + are compatible with the GNU Fortran used in R. We are + intentionally removing the gomp symlink from the binary + so that iomp and gomp don't conflict. Future R + releases may use LLVM-based Fortran complier with OpenMP support. + +

License and sources

+ All sources were obtained directly from the + LLVM releases + and copies are also available in the src + directory (including the build script). See LICENSE.txt + in the sources for the corresponding license and + https://openmp.llvm.org/ + for details on the OpenMP run-time. + +

Verifying code signatures

+ You can use codetools to verify the signature, for example you should see: +
+$ codesign -d -vv /usr/local/lib/libomp.dylib 
+Executable=/usr/local/lib/libomp.dylib
+Identifier=libomp
+Format=Mach-O universal (x86_64 arm64)
+CodeDirectory v=20400 size=5514 flags=0x0(none) hashes=167+2 location=embedded
+Signature size=8927
+Authority=Developer ID Application: Simon Urbanek (VZLD955F6P)
+Authority=Developer ID Certification Authority
+Authority=Apple Root CA
+Timestamp=11/11/2021 at 1:29:04 PM
+Info.plist=not bound
+TeamIdentifier=VZLD955F6P
+Sealed Resources=none
+Internal requirements count=1 size=168
+
+ +

Acknowledgements

+ Thanks to John Clayden, Kevin Ushey and others who contributed to the discussion and testing. + +

Trademark notices

+ +

+ Last modified on 2025/12/11 by Simon Urbanek + + diff --git a/tests/helpers.sh b/tests/helpers.sh new file mode 100644 index 0000000..89c35bc --- /dev/null +++ b/tests/helpers.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +fail() { echo "FAIL: $1" >&2; exit 1; } +assert_eq() { + # assert_eq actual expected message + if [ "$1" != "$2" ]; then + printf 'FAIL: %s\n expected: %s\n actual: %s\n' "$3" "$2" "$1" >&2 + exit 1 + fi +} diff --git a/tests/run-tests.sh b/tests/run-tests.sh new file mode 100755 index 0000000..1d78a86 --- /dev/null +++ b/tests/run-tests.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail +DIR="$(cd "$(dirname "$0")" && pwd)" +rc=0 +for t in "$DIR"/test-*.sh; do + echo "== $(basename "$t") ==" + bash "$t" || rc=1 +done +exit $rc From ed6847332dcee4ed0ea580f899483de9e6e9eed8 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Mon, 22 Jun 2026 22:04:41 -0500 Subject: [PATCH 03/20] feat: add sync-openmp.sh parse_page (upstream HTML -> records) --- sync-openmp.sh | 57 +++++++++++++++++++++++++++++++++++++++++++++ tests/test-parse.sh | 10 ++++++++ 2 files changed, 67 insertions(+) create mode 100755 sync-openmp.sh create mode 100644 tests/test-parse.sh diff --git a/sync-openmp.sh b/sync-openmp.sh new file mode 100755 index 0000000..5e43f07 --- /dev/null +++ b/sync-openmp.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +# OpenMP Setup - Automatic OpenMP installer for macOS +# Copyright (C) 2025: James J Balamuta +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +set -euo pipefail + +BASE_URL="https://mac.r-project.org/openmp" + +# parse_page +# Emits one record per *primary* tier: clangversiondarwinsha1xcode +parse_page() { + local html_file="$1" + # Strip HTML comments (defensive: drops the commented git build), then put one per line. + perl -0pe 's///gs' "$html_file" \ + | tr '\n' ' ' | sed 's/&2; continue; fi + ver=$(printf '%s' "$rel" | sed -E 's/^openmp-([0-9.]+)-darwin.*/\1/') + darwin=$(printf '%s' "$rel" | grep -oE 'darwin[0-9]+') + sha=$(printf '%s' "$row" | grep -oiE '[0-9a-f]{40}' | head -1) + if [ -z "$sha" ]; then echo "WARN: clang ${clang:-?} has no SHA1; skipping" >&2; continue; fi + xcode=$(printf '%s' "$row" | grep -oE 'Xcode [^(<]+' | head -1 | sed 's/[[:space:]]*$//') + if [ "$ver" != "$llvm" ]; then echo "WARN: clang $clang Release version ($ver) != LLVM heading ($llvm)" >&2; fi + printf '%s\t%s\t%s\t%s\t%s\n' "$clang" "$ver" "$darwin" "$sha" "$xcode" + done | sort -t"$(printf '\t')" -k1,1nr +} + +main() { + : # implemented in Task 6 +} + +if [ "${OPENMP_SYNC_LIB:-}" != "1" ]; then + main "$@" +fi diff --git a/tests/test-parse.sh b/tests/test-parse.sh new file mode 100644 index 0000000..588028e --- /dev/null +++ b/tests/test-parse.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail +DIR="$(cd "$(dirname "$0")/.." && pwd)" +. "$DIR/tests/helpers.sh" +OPENMP_SYNC_LIB=1 . "$DIR/sync-openmp.sh" + +got=$(parse_page "$DIR/tests/fixtures/page.html") +want=$(cat "$DIR/tests/fixtures/golden-records.tsv") +assert_eq "$got" "$want" "parse_page output matches golden records" +echo "PASS: test-parse" From cb87be20150d03b3516fcfc93a73a2844a683c41 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Mon, 22 Jun 2026 22:10:35 -0500 Subject: [PATCH 04/20] fix: skip HTML preamble in parse_page to silence false WARN The sed split on '') is silently skipped. --- sync-openmp.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sync-openmp.sh b/sync-openmp.sh index 5e43f07..4ce8797 100755 --- a/sync-openmp.sh +++ b/sync-openmp.sh @@ -30,7 +30,7 @@ parse_page() { | tr '\n' ' ' | sed 's/ Date: Mon, 22 Jun 2026 22:13:16 -0500 Subject: [PATCH 05/20] feat: add case/help/readme block renderers --- sync-openmp.sh | 22 ++++++++++++++++++++++ tests/test-render.sh | 19 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 tests/test-render.sh diff --git a/sync-openmp.sh b/sync-openmp.sh index 4ce8797..79026e7 100755 --- a/sync-openmp.sh +++ b/sync-openmp.sh @@ -48,6 +48,28 @@ parse_page() { done | sort -t"$(printf '\t')" -k1,1nr } +# All renderers read records (clang ver darwin sha xcode) from stdin. +render_case() { + while IFS=$'\t' read -r clang ver darwin sha xcode; do + printf ' %s)\n OPENMP_VERSION="%s"\n DARWIN_TARGET="%s"\n EXPECTED_SHA1="%s"\n ;;\n' \ + "$clang" "$ver" "$darwin" "$sha" + done +} + +render_help() { + while IFS=$'\t' read -r clang ver darwin sha xcode; do + printf ' echo " %s → OpenMP %s"\n' "$xcode" "$ver" + done +} + +render_readme() { + while IFS=$'\t' read -r clang ver darwin sha xcode; do + local short="${xcode#Xcode }" + local file="openmp-${ver}-${darwin}-Release.tar.gz" + printf '| %s | %s.x | %s | [%s](%s/%s) |\n' "$short" "$clang" "$ver" "$file" "$BASE_URL" "$file" + done +} + main() { : # implemented in Task 6 } diff --git a/tests/test-render.sh b/tests/test-render.sh new file mode 100644 index 0000000..f1501b8 --- /dev/null +++ b/tests/test-render.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail +DIR="$(cd "$(dirname "$0")/.." && pwd)" +. "$DIR/tests/helpers.sh" +OPENMP_SYNC_LIB=1 . "$DIR/sync-openmp.sh" + +rec=$'1700\t19.1.5\tdarwin20\t5b44175bcbaa334b0c57391482e068ea185c95a2\tXcode 16.3-26.3' + +case_out=$(printf '%s\n' "$rec" | render_case) +want_case=$' 1700)\n OPENMP_VERSION="19.1.5"\n DARWIN_TARGET="darwin20"\n EXPECTED_SHA1="5b44175bcbaa334b0c57391482e068ea185c95a2"\n ;;' +assert_eq "$case_out" "$want_case" "render_case single tier" + +help_out=$(printf '%s\n' "$rec" | render_help) +assert_eq "$help_out" ' echo " Xcode 16.3-26.3 → OpenMP 19.1.5"' "render_help single tier" + +readme_out=$(printf '%s\n' "$rec" | render_readme) +want_readme='| 16.3-26.3 | 1700.x | 19.1.5 | [openmp-19.1.5-darwin20-Release.tar.gz](https://mac.r-project.org/openmp/openmp-19.1.5-darwin20-Release.tar.gz) |' +assert_eq "$readme_out" "$want_readme" "render_readme single tier" +echo "PASS: test-render" From 6f40479a3c4ebed50f7af0f765cfd35f2a9279d0 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Mon, 22 Jun 2026 22:16:36 -0500 Subject: [PATCH 06/20] feat: add current_pin, resolve_sha (compute-on-change), replace_block --- sync-openmp.sh | 41 +++++++++++++++++++++++++++++++++++++++++ tests/test-edit.sh | 23 +++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 tests/test-edit.sh diff --git a/sync-openmp.sh b/sync-openmp.sh index 79026e7..52f197a 100755 --- a/sync-openmp.sh +++ b/sync-openmp.sh @@ -70,6 +70,47 @@ render_readme() { done } +# current_pin -> "versionsha1" or empty +current_pin() { + local clang="$1" file="$2" + awk -v key=" ${clang})" ' + $0==key {inblock=1; next} + inblock && /OPENMP_VERSION=/ {v=$0; sub(/.*OPENMP_VERSION="/,"",v); sub(/".*/,"",v)} + inblock && /EXPECTED_SHA1=/ {s=$0; sub(/.*EXPECTED_SHA1="/,"",s); sub(/".*/,"",s)} + inblock && /;;/ {print v"\t"s; exit} + ' "$file" +} + +sha1_of() { + if command -v shasum >/dev/null 2>&1; then shasum -a 1 "$1" | cut -d' ' -f1 + else sha1sum "$1" | cut -d' ' -f1; fi +} + +# resolve_sha +resolve_sha() { + local ver="$1" darwin="$2" page_sha="$3" + if [ "${OPENMP_SYNC_TRUST_PAGE:-0}" = "1" ]; then printf '%s' "$page_sha"; return 0; fi + local url="$BASE_URL/openmp-${ver}-${darwin}-Release.tar.gz" tmp got + tmp=$(mktemp) + if ! curl -fsS -o "$tmp" "$url"; then echo "ERROR: download failed: $url" >&2; rm -f "$tmp"; return 1; fi + got=$(sha1_of "$tmp"); rm -f "$tmp" + if [ -n "$page_sha" ] && [ "$got" != "$page_sha" ]; then + echo "WARN: computed SHA1 ($got) != page SHA1 ($page_sha) for $ver" >&2 + fi + printf '%s' "$got" +} + +# replace_block +replace_block() { + local file="$1" begin="$2" end="$3" content="$4" tmp + tmp=$(mktemp) + awk -v b="$begin" -v e="$end" -v cf="$content" ' + index($0,b){print; while((getline line < cf)>0) print line; close(cf); skip=1; next} + index($0,e){skip=0; print; next} + !skip{print} + ' "$file" > "$tmp" && mv "$tmp" "$file" +} + main() { : # implemented in Task 6 } diff --git a/tests/test-edit.sh b/tests/test-edit.sh new file mode 100644 index 0000000..098314e --- /dev/null +++ b/tests/test-edit.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +DIR="$(cd "$(dirname "$0")/.." && pwd)" +. "$DIR/tests/helpers.sh" +OPENMP_SYNC_LIB=1 . "$DIR/sync-openmp.sh" + +# current_pin reads the live install-openmp.sh +pin=$(current_pin 1600 "$DIR/install-openmp.sh") +assert_eq "$pin" $'17.0.6\ta89cab4e763025f03a5d12a93a609ff771ad209c' "current_pin 1600" +assert_eq "$(current_pin 9999 "$DIR/install-openmp.sh")" "" "current_pin unknown -> empty" + +# resolve_sha trust-page path is offline + deterministic +assert_eq "$(OPENMP_SYNC_TRUST_PAGE=1 resolve_sha 19.1.5 darwin20 deadbeef)" "deadbeef" "resolve_sha trust-page" + +# replace_block swaps content between markers, atomically +tmp=$(mktemp); content=$(mktemp) +printf 'head\n# >>> BEGIN GENERATED X (managed by sync-openmp.sh; do not edit) >>>\nOLD\n# <<< END GENERATED X <<<\ntail\n' > "$tmp" +printf 'NEW1\nNEW2\n' > "$content" +replace_block "$tmp" "BEGIN GENERATED X" "END GENERATED X" "$content" +got=$(cat "$tmp"); rm -f "$tmp" "$content" +want=$'head\n# >>> BEGIN GENERATED X (managed by sync-openmp.sh; do not edit) >>>\nNEW1\nNEW2\n# <<< END GENERATED X <<<\ntail' +assert_eq "$got" "$want" "replace_block swaps between markers" +echo "PASS: test-edit" From c8bc335a3b33e6eea339b3c42ff7bd3945892378 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 06:09:29 -0500 Subject: [PATCH 07/20] feat: implement sync-openmp.sh main with --check and compute-on-change --- sync-openmp.sh | 69 ++++++++++++++++++++++++++++++++++++++++++++++- tests/test-e2e.sh | 24 +++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 tests/test-e2e.sh diff --git a/sync-openmp.sh b/sync-openmp.sh index 52f197a..ba92aaf 100755 --- a/sync-openmp.sh +++ b/sync-openmp.sh @@ -111,8 +111,75 @@ replace_block() { ' "$file" > "$tmp" && mv "$tmp" "$file" } +# Build resolved records (compute-on-change) and the per-tier summary. +# Writes resolved records to stdout; appends summary lines to the file named by $1. +resolve_records() { + local summary_file="$1" install="$2" page_records="$3" + while IFS=$'\t' read -r clang ver darwin sha_page xcode; do + local pin cur_ver cur_sha sha + pin=$(current_pin "$clang" "$install") + cur_ver=$(printf '%s' "$pin" | cut -f1); cur_sha=$(printf '%s' "$pin" | cut -f2) + if [ "$cur_ver" = "$ver" ] && [ -n "$cur_sha" ]; then + sha="$cur_sha" + else + sha=$(resolve_sha "$ver" "$darwin" "$sha_page") || return 1 + if [ -z "$cur_ver" ]; then echo "NEW $clang: $ver" >> "$summary_file" + else echo "UPDATED $clang: $cur_ver -> $ver" >> "$summary_file"; fi + fi + printf '%s\t%s\t%s\t%s\t%s\n' "$clang" "$ver" "$darwin" "$sha" "$xcode" + done <<< "$page_records" +} + main() { - : # implemented in Task 6 + local check=0 + [ "${1:-}" = "--check" ] && check=1 + + local script_dir install readme page records summary + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + install="$script_dir/install-openmp.sh" + readme="$script_dir/README.md" + + page=$(mktemp) + if [ -n "${OPENMP_SYNC_PAGE:-}" ]; then cp "$OPENMP_SYNC_PAGE" "$page" + else curl -fsS "$BASE_URL/" -o "$page" || { echo "ERROR: cannot fetch $BASE_URL/" >&2; exit 1; }; fi + + records=$(parse_page "$page") + rm -f "$page" + [ -n "$records" ] || { echo "ERROR: parsed zero records (page structure changed?)" >&2; exit 1; } + + summary=$(mktemp) + local resolved + resolved=$(resolve_records "$summary" "$install" "$records") || { echo "ERROR: SHA resolution failed" >&2; exit 1; } + + # Render blocks into temp files. + local f_case f_help f_readme + f_case=$(mktemp); f_help=$(mktemp); f_readme=$(mktemp) + printf '%s\n' "$resolved" | render_case > "$f_case" + printf '%s\n' "$resolved" | render_help > "$f_help" + printf '%s\n' "$resolved" | render_readme > "$f_readme" + + if [ "$check" = 1 ]; then + # Apply to copies and diff; write nothing to the real files. + local ci cr; ci=$(mktemp); cr=$(mktemp); cp "$install" "$ci"; cp "$readme" "$cr" + replace_block "$ci" "BEGIN GENERATED VERSION CASES" "END GENERATED VERSION CASES" "$f_case" + replace_block "$ci" "BEGIN GENERATED HELP VERSIONS" "END GENERATED HELP VERSIONS" "$f_help" + replace_block "$cr" "BEGIN GENERATED README TABLE" "END GENERATED README TABLE" "$f_readme" + local rc=0 + diff -q "$ci" "$install" >/dev/null || rc=1 + diff -q "$cr" "$readme" >/dev/null || rc=1 + rm -f "$f_case" "$f_help" "$f_readme" "$summary" "$ci" "$cr" + [ "$rc" = 0 ] && echo "up to date" || echo "DRIFT: run sync-openmp.sh" + exit "$rc" + fi + + replace_block "$install" "BEGIN GENERATED VERSION CASES" "END GENERATED VERSION CASES" "$f_case" + replace_block "$install" "BEGIN GENERATED HELP VERSIONS" "END GENERATED HELP VERSIONS" "$f_help" + replace_block "$readme" "BEGIN GENERATED README TABLE" "END GENERATED README TABLE" "$f_readme" + rm -f "$f_case" "$f_help" "$f_readme" + + echo "Sync complete." + if [ -s "$summary" ]; then cat "$summary"; else echo "No version changes."; fi + rm -f "$summary" } if [ "${OPENMP_SYNC_LIB:-}" != "1" ]; then diff --git a/tests/test-e2e.sh b/tests/test-e2e.sh new file mode 100644 index 0000000..d62948c --- /dev/null +++ b/tests/test-e2e.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail +DIR="$(cd "$(dirname "$0")/.." && pwd)" +. "$DIR/tests/helpers.sh" + +work=$(mktemp -d) +cp "$DIR/install-openmp.sh" "$DIR/README.md" "$work/" +cp "$DIR/sync-openmp.sh" "$work/" +cp -r "$DIR/tests" "$work/tests" + +# Run sync against the fixture, trusting page SHA (offline), writing into the temp copies. +( cd "$work" && OPENMP_SYNC_PAGE="tests/fixtures/page.html" OPENMP_SYNC_TRUST_PAGE=1 \ + bash sync-openmp.sh > summary.txt ) + +grep -q '19.1.5' "$work/install-openmp.sh" || fail "install case not updated to 19.1.5" +grep -q '5b44175bcbaa334b0c57391482e068ea185c95a2' "$work/install-openmp.sh" || fail "install SHA not updated" +grep -q '| 16.3-26.3 | 1700.x | 19.1.5 |' "$work/README.md" || fail "README row not updated" +grep -q 'UPDATED 1700' "$work/summary.txt" || fail "summary missing UPDATED 1700" + +# Running --check on the freshly-synced tree reports no drift (exit 0). +( cd "$work" && OPENMP_SYNC_PAGE="tests/fixtures/page.html" OPENMP_SYNC_TRUST_PAGE=1 \ + bash sync-openmp.sh --check ) || fail "--check reported drift after sync" +rm -rf "$work" +echo "PASS: test-e2e" From 237dd0aac6ab673f3af2d348aa292d284dd70b6b Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 06:14:21 -0500 Subject: [PATCH 08/20] ci: weekly OpenMP upstream sync that opens a PR on change --- .github/workflows/check-upstream.yml | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/check-upstream.yml diff --git a/.github/workflows/check-upstream.yml b/.github/workflows/check-upstream.yml new file mode 100644 index 0000000..c9493c0 --- /dev/null +++ b/.github/workflows/check-upstream.yml @@ -0,0 +1,36 @@ +name: Check OpenMP upstream + +on: + schedule: + - cron: "17 6 * * 1" # Mondays 06:17 UTC + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run sync + id: sync + run: | + ./sync-openmp.sh | tee sync-summary.txt + + - name: Run tests + run: bash tests/run-tests.sh + + - name: Open PR on change + uses: peter-evans/create-pull-request@v6 + with: + branch: chore/sync-openmp-upstream + title: "chore: sync OpenMP versions from upstream" + body-path: sync-summary.txt + commit-message: "chore: sync OpenMP versions from upstream" + delete-branch: true + add-paths: | + install-openmp.sh + README.md From 5a88ad267c1b8489ef038b84f02c09b114b3e449 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 06:16:15 -0500 Subject: [PATCH 09/20] fix: enforce pipefail so a failed sync fails the CI step --- .github/workflows/check-upstream.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/check-upstream.yml b/.github/workflows/check-upstream.yml index c9493c0..45e548a 100644 --- a/.github/workflows/check-upstream.yml +++ b/.github/workflows/check-upstream.yml @@ -17,7 +17,9 @@ jobs: - name: Run sync id: sync + shell: bash run: | + set -euo pipefail ./sync-openmp.sh | tee sync-summary.txt - name: Run tests From 035a4e5bd1dbd1312bf23dc031400c20116d9103 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 06:19:05 -0500 Subject: [PATCH 10/20] fix: make test-e2e set a deterministic pre-state, decoupling from repo pin --- tests/test-e2e.sh | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test-e2e.sh b/tests/test-e2e.sh index d62948c..4f6c0b0 100644 --- a/tests/test-e2e.sh +++ b/tests/test-e2e.sh @@ -8,6 +8,22 @@ cp "$DIR/install-openmp.sh" "$DIR/README.md" "$work/" cp "$DIR/sync-openmp.sh" "$work/" cp -r "$DIR/tests" "$work/tests" +# Force a known OLD pre-state in the temp copy so the test is deterministic +# regardless of what the repo's install-openmp.sh currently pins for clang 1700. +# We rewrite only the 1700 case block in the temp copy (never the repo file). +awk ' + /^ 1700\)/ { in1700=1 } + in1700 && /OPENMP_VERSION=/ && !ver_done { + sub(/OPENMP_VERSION="[^"]*"/, "OPENMP_VERSION=\"19.1.0\""); ver_done=1 + } + in1700 && /EXPECTED_SHA1=/ && !sha_done { + sub(/EXPECTED_SHA1="[^"]*"/, "EXPECTED_SHA1=\"42a22fa5852bafc23ab31241d064f9be9aab8a0d\""); sha_done=1 + } + in1700 && /^ ;;/ { in1700=0 } + { print } +' "$work/install-openmp.sh" > "$work/install-openmp.sh.tmp" \ + && mv "$work/install-openmp.sh.tmp" "$work/install-openmp.sh" + # Run sync against the fixture, trusting page SHA (offline), writing into the temp copies. ( cd "$work" && OPENMP_SYNC_PAGE="tests/fixtures/page.html" OPENMP_SYNC_TRUST_PAGE=1 \ bash sync-openmp.sh > summary.txt ) From 76ee4a0f61a9c5370a013c49c5076a258957f069 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 06:19:42 -0500 Subject: [PATCH 11/20] chore: sync OpenMP 19.1.0 -> 19.1.5 from upstream --- README.md | 2 +- install-openmp.sh | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) mode change 100755 => 100644 install-openmp.sh diff --git a/README.md b/README.md index 8beea6f..1b7fe2e 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ chmod +x uninstall-openmp.sh | Xcode Version | Apple Clang | OpenMP Version | Download | |---------------|-------------|----------------|----------| -| 16.3+ | 1700.x | 19.1.0 | [openmp-19.1.0-darwin20-Release.tar.gz](https://mac.r-project.org/openmp/openmp-19.1.0-darwin20-Release.tar.gz) | +| 16.3-26.3 | 1700.x | 19.1.5 | [openmp-19.1.5-darwin20-Release.tar.gz](https://mac.r-project.org/openmp/openmp-19.1.5-darwin20-Release.tar.gz) | | 16.0-16.2 | 1600.x | 17.0.6 | [openmp-17.0.6-darwin20-Release.tar.gz](https://mac.r-project.org/openmp/openmp-17.0.6-darwin20-Release.tar.gz) | | 15.x | 1500.x | 16.0.4 | [openmp-16.0.4-darwin20-Release.tar.gz](https://mac.r-project.org/openmp/openmp-16.0.4-darwin20-Release.tar.gz) | | 14.3.x | 1403.x | 15.0.7 | [openmp-15.0.7-darwin20-Release.tar.gz](https://mac.r-project.org/openmp/openmp-15.0.7-darwin20-Release.tar.gz) | diff --git a/install-openmp.sh b/install-openmp.sh old mode 100755 new mode 100644 index a53015c..4cc1eb9 --- a/install-openmp.sh +++ b/install-openmp.sh @@ -64,18 +64,18 @@ show_help() { echo "" echo -e "${YELLOW}SUPPORTED VERSIONS:${NC}" # >>> BEGIN GENERATED HELP VERSIONS (managed by sync-openmp.sh; do not edit) >>> - echo " Xcode 16.3+ → OpenMP 19.1.0" - echo " Xcode 16.0-16.2 → OpenMP 17.0.6" - echo " Xcode 15.x → OpenMP 16.0.4" - echo " Xcode 14.3.x → OpenMP 15.0.7" - echo " Xcode 14.0-14.2 → OpenMP 14.0.6" - echo " Xcode 13.3-13.4.1 → OpenMP 13.0.0" - echo " Xcode 13.0-13.2.1 → OpenMP 12.0.1" - echo " Xcode 12.5 → OpenMP 11.0.1" - echo " Xcode 12.0-12.4 → OpenMP 10.0.0" - echo " Xcode 11.4-11.7 → OpenMP 9.0.1" - echo " Xcode 11.0-11.3.1 → OpenMP 8.0.1" - echo " Xcode 10.2-10.3 → OpenMP 7.1.0" + echo " Xcode 16.3-26.3 → OpenMP 19.1.5" + echo " Xcode 16.0-16.2 → OpenMP 17.0.6" + echo " Xcode 15.x → OpenMP 16.0.4" + echo " Xcode 14.3.x → OpenMP 15.0.7" + echo " Xcode 14.0-14.2 → OpenMP 14.0.6" + echo " Xcode 13.3-13.4.1 → OpenMP 13.0.0" + echo " Xcode 13.0-13.2.1 → OpenMP 12.0.1" + echo " Xcode 12.5 → OpenMP 11.0.1" + echo " Xcode 12.0-12.4 → OpenMP 10.0.0" + echo " Xcode 11.4-11.7 → OpenMP 9.0.1" + echo " Xcode 11.0-11.3.1 → OpenMP 8.0.1" + echo " Xcode 10.2-10.3 → OpenMP 7.1.0" # <<< END GENERATED HELP VERSIONS <<< echo "" echo "MORE INFO:" @@ -144,9 +144,9 @@ BASE_URL="https://mac.r-project.org/openmp" case $CLANG_VERSION in # >>> BEGIN GENERATED VERSION CASES (managed by sync-openmp.sh; do not edit) >>> 1700) - OPENMP_VERSION="19.1.0" + OPENMP_VERSION="19.1.5" DARWIN_TARGET="darwin20" - EXPECTED_SHA1="42a22fa5852bafc23ab31241d064f9be9aab8a0d" + EXPECTED_SHA1="5b44175bcbaa334b0c57391482e068ea185c95a2" ;; 1600) OPENMP_VERSION="17.0.6" From 5f3ede8486a13f062197f562a500a8d77af5a48d Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 06:25:58 -0500 Subject: [PATCH 12/20] fix: guard replace_block against missing markers and preserve file mode - Add begin/end marker count check (grep -cF) in replace_block; errors and returns non-zero without touching the file if either marker is absent or appears more than once (fixes latent truncation bug). - Capture original file mode portably (stat -f on macOS, stat -c on Linux) and chmod the mktemp before mv, so exec bits survive rewrites. - Clean up temp file on awk failure to avoid leaking it. - Restore 100755 on install-openmp.sh (was wrongly stripped to 100644). - Add regression test in tests/test-edit.sh: replace_block must return non-zero and leave file unchanged when END marker is missing. --- install-openmp.sh | 0 sync-openmp.sh | 12 +++++++++++- tests/test-edit.sh | 12 ++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) mode change 100644 => 100755 install-openmp.sh diff --git a/install-openmp.sh b/install-openmp.sh old mode 100644 new mode 100755 diff --git a/sync-openmp.sh b/sync-openmp.sh index ba92aaf..fffb3cf 100755 --- a/sync-openmp.sh +++ b/sync-openmp.sh @@ -103,12 +103,22 @@ resolve_sha() { # replace_block replace_block() { local file="$1" begin="$2" end="$3" content="$4" tmp + local nb ne + nb=$(grep -cF -- "$begin" "$file") || true + ne=$(grep -cF -- "$end" "$file") || true + if [ "$nb" != 1 ] || [ "$ne" != 1 ]; then + echo "ERROR: replace_block: markers not found exactly once in $file (begin=$nb end=$ne)" >&2 + return 1 + fi + local mode + mode=$(stat -f '%Lp' "$file" 2>/dev/null || stat -c '%a' "$file") tmp=$(mktemp) awk -v b="$begin" -v e="$end" -v cf="$content" ' index($0,b){print; while((getline line < cf)>0) print line; close(cf); skip=1; next} index($0,e){skip=0; print; next} !skip{print} - ' "$file" > "$tmp" && mv "$tmp" "$file" + ' "$file" > "$tmp" || { rm -f "$tmp"; return 1; } + chmod "$mode" "$tmp" && mv "$tmp" "$file" } # Build resolved records (compute-on-change) and the per-tier summary. diff --git a/tests/test-edit.sh b/tests/test-edit.sh index 098314e..79872cd 100644 --- a/tests/test-edit.sh +++ b/tests/test-edit.sh @@ -20,4 +20,16 @@ replace_block "$tmp" "BEGIN GENERATED X" "END GENERATED X" "$content" got=$(cat "$tmp"); rm -f "$tmp" "$content" want=$'head\n# >>> BEGIN GENERATED X (managed by sync-openmp.sh; do not edit) >>>\nNEW1\nNEW2\n# <<< END GENERATED X <<<\ntail' assert_eq "$got" "$want" "replace_block swaps between markers" +# replace_block must refuse (non-zero, file unchanged) when END marker is absent +tmp2=$(mktemp); content2=$(mktemp) +printf 'head\n# >>> BEGIN GENERATED X (managed by sync-openmp.sh; do not edit) >>>\nOLD\ntail\n' > "$tmp2" +printf 'NEW\n' > "$content2" +orig2=$(cat "$tmp2") +if OPENMP_SYNC_LIB=1 replace_block "$tmp2" "BEGIN GENERATED X" "END GENERATED X" "$content2" 2>/dev/null; then + fail "replace_block should return non-zero when END marker is missing" +fi +got2=$(cat "$tmp2") +rm -f "$tmp2" "$content2" +assert_eq "$got2" "$orig2" "replace_block leaves file unchanged when END marker is missing" + echo "PASS: test-edit" From 87fa869df0cc6be2e40e7dc3dc8680f42409d6c7 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 09:55:58 -0500 Subject: [PATCH 13/20] feat: add test-openmp.sh C toolchain correctness check --- test-openmp.sh | 121 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100755 test-openmp.sh diff --git a/test-openmp.sh b/test-openmp.sh new file mode 100755 index 0000000..a2b9a57 --- /dev/null +++ b/test-openmp.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +# OpenMP Setup - Automatic OpenMP installer for macOS +# Copyright (C) 2025: James J Balamuta +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +set -euo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' + +show_help() { + echo -e "${GREEN}OpenMP correctness test${NC}" + echo "Usage: $0 [--c-only] [-h|--help]" + echo "" + echo " Compiles and runs OpenMP programs (C, and an R package) against the" + echo " installed mac.r-project.org runtime and verifies real multithreading" + echo " AND a deterministic parallel-reduction result." + echo "" + echo " --c-only Run only the C toolchain check (skip the R package check)" + echo " -h, --help Show this help" + echo "" + echo "Exit codes: 0 pass, 1 single-thread, 2 _OPENMP undefined," + echo " 3 wrong C result, 4 R unavailable/install failed, 5 wrong R result," + echo " 6 C compile failed." + exit 0 +} + +C_ONLY=false +for arg in "$@"; do + case $arg in + -h|--help) show_help ;; + --c-only) C_ONLY=true ;; + *) echo -e "${RED}Error: Unknown option '$arg'${NC}"; echo "Use --help"; exit 1 ;; + esac +done + +if [[ "$OSTYPE" != darwin* ]]; then + echo -e "${YELLOW}SKIP: test-openmp.sh validates the macOS OpenMP runtime; current OS is not macOS.${NC}" + exit 0 +fi + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT + +run_c_test() { + cat > "$TMP/omp_test.c" <<'EOF' +#include +#include +int main(void) { +#ifndef _OPENMP + fprintf(stderr, "FAIL: _OPENMP not defined\n"); + return 2; +#endif + int observed = 0; + #pragma omp parallel + { + #pragma omp critical + { int t = omp_get_num_threads(); if (t > observed) observed = t; } + } + const long N = 1000000L; + long long expected = (long long)N * (N + 1) / 2; /* 500000500000 */ + long long sum = 0; + #pragma omp parallel for reduction(+:sum) + for (long i = 1; i <= N; ++i) sum += i; + printf("observed_threads=%d reduction_sum=%lld expected=%lld\n", observed, sum, expected); + if (sum != expected) return 3; + if (observed <= 1) return 1; + return 0; +} +EOF + echo "Compiling C OpenMP test..." + if ! clang -Xclang -fopenmp -I/usr/local/include -L/usr/local/lib -lomp \ + "$TMP/omp_test.c" -o "$TMP/omp_test"; then + echo -e "${RED}FAIL: C OpenMP program did not compile.${NC}" + return 6 + fi + + local out rc + out=$("$TMP/omp_test"); rc=$? + echo " default run: $out" + case $rc in + 0) ;; + 1) echo -e "${RED}FAIL: OpenMP linked but ran single-threaded.${NC}"; return 1 ;; + 2) echo -e "${RED}FAIL: _OPENMP not defined (OpenMP not enabled).${NC}"; return 2 ;; + 3) echo -e "${RED}FAIL: parallel reduction produced the wrong result.${NC}"; return 3 ;; + *) echo -e "${RED}FAIL: C test exited with unexpected code $rc.${NC}"; return $rc ;; + esac + + local out4 obs4 + out4=$(OMP_NUM_THREADS=4 "$TMP/omp_test") + echo " OMP_NUM_THREADS=4 run: $out4" + obs4=$(printf '%s\n' "$out4" | sed -n 's/.*observed_threads=\([0-9]*\).*/\1/p') + if [ "$obs4" != "4" ]; then + echo -e "${RED}FAIL: runtime did not honor OMP_NUM_THREADS=4 (observed=$obs4).${NC}" + return 1 + fi + echo -e "${GREEN}C toolchain OpenMP check passed.${NC}" +} + +run_c_test + +if [ "$C_ONLY" = true ]; then + echo -e "${GREEN}All requested OpenMP correctness checks passed (C only).${NC}" + exit 0 +fi + +# Part B (R package) is appended in Task 2, before the final summary line below. +echo -e "${GREEN}All OpenMP correctness checks passed.${NC}" From b80e3a592f3df6b58338aa966a0a3389068244ce Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 09:58:50 -0500 Subject: [PATCH 14/20] fix: tolerate nonzero OMP_NUM_THREADS run so the diagnostic prints --- test-openmp.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-openmp.sh b/test-openmp.sh index a2b9a57..4d616f1 100755 --- a/test-openmp.sh +++ b/test-openmp.sh @@ -100,7 +100,7 @@ EOF esac local out4 obs4 - out4=$(OMP_NUM_THREADS=4 "$TMP/omp_test") + out4=$(OMP_NUM_THREADS=4 "$TMP/omp_test") || true echo " OMP_NUM_THREADS=4 run: $out4" obs4=$(printf '%s\n' "$out4" | sed -n 's/.*observed_threads=\([0-9]*\).*/\1/p') if [ "$obs4" != "4" ]; then From aa3d05af1c55f61fff205f7a77501e6f99d175f2 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:00:10 -0500 Subject: [PATCH 15/20] feat: add R package OpenMP correctness check to test-openmp.sh --- test-openmp.sh | 80 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/test-openmp.sh b/test-openmp.sh index 4d616f1..a015b9e 100755 --- a/test-openmp.sh +++ b/test-openmp.sh @@ -117,5 +117,83 @@ if [ "$C_ONLY" = true ]; then exit 0 fi -# Part B (R package) is appended in Task 2, before the final summary line below. +run_r_test() { + if ! command -v Rscript >/dev/null 2>&1; then + echo -e "${RED}FAIL: Rscript not found; R package OpenMP check cannot run.${NC}" + return 4 + fi + + local pkg="$TMP/ompcheck" lib="$TMP/lib" + mkdir -p "$pkg/R" "$pkg/src" "$lib" + + cat > "$pkg/DESCRIPTION" <<'EOF' +Package: ompcheck +Version: 0.0.1 +Title: OpenMP CI Correctness Probe +Description: Minimal package running a parallel reduction to verify OpenMP. +Authors@R: person("CI", "Probe", email = "ci@example.com", role = c("aut", "cre")) +License: AGPL (>= 3) +EOF + + printf 'useDynLib(ompcheck, .registration = TRUE)\nexport(omp_sum)\n' > "$pkg/NAMESPACE" + printf 'omp_sum <- function(n) .Call(C_omp_sum, as.numeric(n))\n' > "$pkg/R/omp_sum.R" + + # NOTE: MUST be included before the R headers. R's headers before + # omp.h break omp.h's `declare variant` parsing on recent Apple clang. + cat > "$pkg/src/omp_sum.c" <<'EOF' +#include +#include +#include +#include + +SEXP C_omp_sum(SEXP nSEXP) { + long N = (long) Rf_asReal(nSEXP); + int observed = 0; + #pragma omp parallel + { + #pragma omp critical + { int t = omp_get_num_threads(); if (t > observed) observed = t; } + } + double sum = 0.0; + #pragma omp parallel for reduction(+:sum) + for (long i = 1; i <= N; ++i) sum += (double) i; + SEXP out = PROTECT(Rf_allocVector(REALSXP, 2)); + REAL(out)[0] = sum; + REAL(out)[1] = (double) observed; + UNPROTECT(1); + return out; +} + +static const R_CallMethodDef CallEntries[] = { + {"C_omp_sum", (DL_FUNC) &C_omp_sum, 1}, + {NULL, NULL, 0} +}; +void R_init_ompcheck(DllInfo *dll) { + R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); + R_useDynamicSymbols(dll, FALSE); +} +EOF + + echo "Building R package 'ompcheck' with OpenMP (documented flags)..." + if ! PKG_CPPFLAGS='-Xclang -fopenmp -I/usr/local/include' \ + PKG_LIBS='-L/usr/local/lib -lomp' \ + R CMD INSTALL -l "$lib" "$pkg" >/dev/null 2>"$TMP/rinstall.log"; then + echo -e "${RED}FAIL: R CMD INSTALL of the OpenMP package failed.${NC}" + sed 's/^/ /' "$TMP/rinstall.log" | tail -15 + return 4 + fi + + echo "Running R package OpenMP check..." + if ! Rscript -e '.libPaths(c("'"$lib"'", .libPaths())) +v <- ompcheck::omp_sum(1e6) +cat(sprintf(" R result: sum=%.0f observed_threads=%d\n", v[1], as.integer(v[2]))) +stopifnot(v[1] == 1e6 * (1e6 + 1) / 2, v[2] > 1) +cat(" R parallel reduction correct and multithreaded.\n")'; then + echo -e "${RED}FAIL: R OpenMP package produced a wrong or single-threaded result.${NC}" + return 5 + fi + echo -e "${GREEN}R package OpenMP check passed.${NC}" +} + +run_r_test echo -e "${GREEN}All OpenMP correctness checks passed.${NC}" From 0767159f0a0afacca6c446c22b44150d0dac230b Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:02:59 -0500 Subject: [PATCH 16/20] test: syntax-guard test-openmp.sh in the cross-platform suite --- tests/test-openmp-syntax.sh | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100755 tests/test-openmp-syntax.sh diff --git a/tests/test-openmp-syntax.sh b/tests/test-openmp-syntax.sh new file mode 100755 index 0000000..d46c67d --- /dev/null +++ b/tests/test-openmp-syntax.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail +DIR="$(cd "$(dirname "$0")/.." && pwd)" +fail() { echo "FAIL: $1" >&2; exit 1; } + +[ -f "$DIR/test-openmp.sh" ] || fail "test-openmp.sh missing" +bash -n "$DIR/test-openmp.sh" || fail "test-openmp.sh has a syntax error" +grep -q 'omp_get_num_threads' "$DIR/test-openmp.sh" || fail "test-openmp.sh missing the OpenMP probe" +echo "PASS: test-openmp-syntax" From e188b83811b57509cc66fc02b51797e4e7555268 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:05:30 -0500 Subject: [PATCH 17/20] ci: run OpenMP correctness test on macOS for runtime-affecting changes --- .github/workflows/test-openmp.yml | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/test-openmp.yml diff --git a/.github/workflows/test-openmp.yml b/.github/workflows/test-openmp.yml new file mode 100644 index 0000000..f39a370 --- /dev/null +++ b/.github/workflows/test-openmp.yml @@ -0,0 +1,34 @@ +name: OpenMP correctness + +on: + push: + branches: [main] + paths: + - install-openmp.sh + - test-openmp.sh + - .github/workflows/test-openmp.yml + pull_request: + branches: [main] + paths: + - install-openmp.sh + - test-openmp.sh + - .github/workflows/test-openmp.yml + workflow_dispatch: + +jobs: + openmp-correctness: + strategy: + fail-fast: false + matrix: + os: [macos-14, macos-15] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - name: Install OpenMP runtime + run: ./install-openmp.sh --yes + + - uses: r-lib/actions/setup-r@v2 + + - name: Run OpenMP correctness test + run: ./test-openmp.sh From d0e622e4b5ddf6439f15d9da62e6e6d4da10d678 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:12:07 -0500 Subject: [PATCH 18/20] fix: capture C-test exit code without aborting so FAIL diagnostics print --- test-openmp.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-openmp.sh b/test-openmp.sh index a015b9e..e644104 100755 --- a/test-openmp.sh +++ b/test-openmp.sh @@ -89,7 +89,7 @@ EOF fi local out rc - out=$("$TMP/omp_test"); rc=$? + out=$("$TMP/omp_test") && rc=0 || rc=$? echo " default run: $out" case $rc in 0) ;; From 8ec2f615f493ca5e1a2076715b27b98755184170 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:20:54 -0500 Subject: [PATCH 19/20] ci: bump actions/checkout to v7 --- .github/workflows/check-upstream.yml | 2 +- .github/workflows/test-openmp.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-upstream.yml b/.github/workflows/check-upstream.yml index 45e548a..9357871 100644 --- a/.github/workflows/check-upstream.yml +++ b/.github/workflows/check-upstream.yml @@ -13,7 +13,7 @@ jobs: sync: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Run sync id: sync diff --git a/.github/workflows/test-openmp.yml b/.github/workflows/test-openmp.yml index f39a370..a409011 100644 --- a/.github/workflows/test-openmp.yml +++ b/.github/workflows/test-openmp.yml @@ -23,7 +23,7 @@ jobs: os: [macos-14, macos-15] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Install OpenMP runtime run: ./install-openmp.sh --yes From 2aa588b0267d7d4c3ea3ae105fe27e5b44d9c02e Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:23:25 -0500 Subject: [PATCH 20/20] ci: bump peter-evans/create-pull-request to v8 --- .github/workflows/check-upstream.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-upstream.yml b/.github/workflows/check-upstream.yml index 9357871..1f37246 100644 --- a/.github/workflows/check-upstream.yml +++ b/.github/workflows/check-upstream.yml @@ -26,7 +26,7 @@ jobs: run: bash tests/run-tests.sh - name: Open PR on change - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@v8 with: branch: chore/sync-openmp-upstream title: "chore: sync OpenMP versions from upstream"