Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export(assert_macos_supported)
export(assert_r_version_supported)
export(assert_x86_64)
export(binary_download)
export(developer_dir)
export(gfortran_install)
export(gfortran_uninstall)
export(gfortran_update)
Expand All @@ -21,6 +22,7 @@ export(is_gfortran_installed)
export(is_openmp_installed)
export(is_xcode_app_installed)
export(is_xcode_cli_installed)
export(is_xcode_toolchain_usable)
export(macos_rtools_install)
export(macos_rtools_uninstall)
export(openmp_install)
Expand All @@ -34,8 +36,10 @@ export(xcode_cli_path)
export(xcode_cli_reset)
export(xcode_cli_switch)
export(xcode_cli_uninstall)
export(xcode_cli_version)
export(xcode_select)
export(xcode_select_path)
export(xcode_select_version)
export(xcode_toolchains)
export(xcodebuild)
export(xcodebuild_version)
109 changes: 59 additions & 50 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,60 +1,69 @@
# macrtools 0.0.8

## Features
## Breaking changes

- Added support for macOS Golden Gate (27.x).
- `is_xcode_cli_installed()` now reports whether the Command Line Tools package
is installed, rather than whether it is the selected developer directory. Use
`developer_dir()` or `is_xcode_toolchain_usable()` to ask about the selection.

## Bug Fixes
- `is_xcode_app_installed()` now reports whether any Xcode is installed
anywhere, rather than whether `/Applications/Xcode.app` is selected.

- Fixed the macOS version check so that macOS 27 is recognized as supported.
`is_macos_r_supported()` compares against an *exclusive* upper bound, so the
previous value of `"27.0"` rejected every macOS 27 release, including 27.0
itself. On macOS 27 this aborted `macos_rtools_install()`, `gfortran_install()`
and `openmp_install()`, and printed an "unsupported" warning on attach.
This is the same off-by-one that affected Tahoe
([#28](https://github.com/coatless-mac/macrtools/issues/28)).

- Fixed macOS version comparisons for `.0` releases. `sw_vers -productVersion`
reports only two components for a `.0` release (macOS 27.0 reports `"27.0"`,
never `"27.0.0"`), and `utils::compareVersion()` ranks a shorter string below
an otherwise-equal longer one. Comparing those against three-component bounds
placed every `.0` release in the *previous* release's range: `"12.0"` was
reported as Big Sur rather than Monterey, `"11.0"` matched nothing at all, and
`"10.13"` was rejected as unsupported by an error message that named 10.13 as
supported. Versions and bounds are now padded to three components before
comparison.
- `xcode_cli_switch()` takes `path` as its first argument, where `password`
used to be.

## Internal
## New features

- Added support for macOS 27 (Golden Gate).

- `xcode_toolchains()` lists every developer directory `xcode-select` can be
pointed at, with its version and whether it is active.

- `is_xcode_toolchain_usable()` reports whether a compiler is reachable through
the active developer directory, which is what decides whether R can build a
package.

- `developer_dir()` resolves the active developer directory, honoring
`DEVELOPER_DIR`.

- `xcode_cli_version()` reports the installed Command Line Tools version.

- `xcode_cli_switch()` gained a `path` argument, so a toolchain from
`xcode_toolchains()` can be selected instead of always forcing the Command
Line Tools.

## Bug fixes

- Xcode is detected outside its default location. Both predicates compared the
active developer directory against a hardcoded path, so a versioned bundle
such as `/Applications/Xcode_16.4.app` reported nothing installed and
triggered a needless `softwareupdate` run.

- macOS 27 is recognized as supported. The version ceiling is exclusive, so the
previous bound rejected every 27.x release and aborted
`macos_rtools_install()`, `gfortran_install()` and `openmp_install()`.

- macOS versions ending in `.0` compare correctly. `sw_vers` reports only two
components for those releases, which placed each one in the previous
release's range: `"12.0"` read as Big Sur, `"11.0"` matched nothing, and
`"10.13"` was rejected as unsupported.

- `xcode_cli_install()` no longer skips installation when an unselected Xcode
exists elsewhere on disk, since it provides no compiler.

- `macos_rtools_install()` reports which toolchain is active instead of assuming
a full Xcode, and reports a failure rather than success when nothing can
compile.

- `macos_rtools_uninstall()` no longer reports a component as removed when it
was not installed.

- The Command Line Tools version no longer comes from `xcode-select --version`,
which reports the version of `xcode-select` itself.

- Centralized the macOS support window behind `minimum_supported_macos_version()`
and `first_unsupported_macos_version()`, mirroring the R version window added
in 0.0.7. The ceiling is now named for what it is, the first *rejected*
version, which is the distinction both previous off-by-one bugs got wrong.
- The supported range shown by `assert_macos_supported()` and the startup
message is now rendered by a single `macos_support_range()` helper that
derives the version number from the bound. Previously the range was written
out as a literal in two files, with different wording, and neither was tested.
- Added upper-bound tests for `is_macos_r_supported()`. The suite previously
exercised only the lower bound, which is why both off-by-one bugs shipped
without a test failure. Added a test asserting that exactly one named
`is_macos_*()` predicate matches any given release, so a new release cannot be
swallowed by its predecessor's range.
- Added `is_macos_golden_gate()` for 27.x.
- Added `pad_version()` to normalize version strings before comparison, and
extended the predicate test to cover the two-component `.0` form that
`sw_vers` actually emits.
- Corrected the `recipes_binary_install()` documentation, which stated that the
`darwin` repository is chosen by comparing against the macOS version. It is
chosen by comparing against the Darwin kernel version, and the two are no
longer a fixed offset apart: Darwin stayed on 25 for macOS 26, then realigned
to 27 for macOS 27, skipping 26.
- Noted in `R/openmp.R` that Apple renumbered Apple clang from 17.0.0
(`clang-1700.x`) to 21.0.0 (`clang-2100.x`) at Xcode 26.4, so Xcode 26.4 and
newer extrapolate onto the LLVM OpenMP 19.1.5 row. Upstream documents that
runtime only through Xcode 26.3, but 19.1.5 remains the newest build published
on mac.r-project.org, so the selection is unchanged and still correct.
- Bumped the declared `testthat` minimum to 3.1.7, the version that introduced
`local_mocked_bindings()`, which the suite already relies on throughout.
- `recipes_binary_install()` documentation now states that the `darwin`
repository is chosen by Darwin kernel version, not macOS version. The two are
no longer a fixed offset apart.

# macrtools 0.0.7

Expand Down
143 changes: 99 additions & 44 deletions R/toolchain.R
Original file line number Diff line number Diff line change
Expand Up @@ -134,61 +134,112 @@ rtools_install_xcode_cli <- function(entered_password, verbose, describe_steps,
cli::cli_text("") # Add spacing

result_xcode <- TRUE
if(!is_xcode_app_installed()) {
if(!is_xcode_cli_installed()) {
if (verbose) {
cli::cli_progress_update(id = pb_id, set = 10)
cli::cli_alert_info("{.pkg macrtools}: Need to install Xcode Command Line Tools.")
cli::cli_bullets(c(
"Source: Apple Software Update",
"Installation method: softwareupdate command",
"Status: Not installed",
"Estimated time: 10-15 minutes"
))
cli::cli_text("") # Add spacing
}
# Gate on whether a compiler is actually REACHABLE. The previous check
# compared the active developer directory against two hardcoded paths and
# so misread every non-standard layout, including the versioned Xcode that
# every GitHub runner selects, as "no toolchain installed".
if(is_xcode_toolchain_usable()) {
if(describe_steps) {
active_dir <- developer_dir()

if (verbose) cli::cli_progress_update(id = pb_id, set = 20)
result_xcode <- xcode_cli_install(password = entered_password, verbose = describe_steps)
if(developer_dir_is_app(active_dir)) {
bundle <- base::sub("/Contents/Developer$", "", active_dir)
active_version <- xcode_app_version(bundle)

if(!result_xcode) {
cli::cli_abort(c(
"{.pkg macrtools}: Failed to install Xcode Command Line Tools.",
"This is a required component for R package development.",
"Installation status: Failed",
"i" = "Try installing manually by running 'sudo xcode-select --install' in Terminal."
cli::cli_alert_info("{.pkg macrtools}: Full Xcode.app IDE is active.")
cli::cli_bullets(c(
"Location: {.path {bundle}}",
"Version: {.val {active_version}}",
"Status: Pre-installed, skipping Command Line Tools installation"
))
}

if (verbose) cli::cli_progress_update(id = pb_id, set = 30)
} else {
if(describe_steps) {
# Get Xcode CLI version information
xcode_version <- exec_text('xcode-select', '--version')
} else {
active_location <- if(base::is.na(active_dir)) {
install_directory_xcode_cli()
} else {
active_dir
}
# xcode-select --version reports the version of xcode-select
# itself and never changes with the toolchain, so read the
# package receipt instead.
active_version <- xcode_cli_version()

cli::cli_alert_info("{.pkg macrtools}: Xcode Command Line Tools already installed.")
cli::cli_bullets(c(
"Location: {.path {xcode_cli_path()}}",
"Version: {.val {xcode_version}}",
"Location: {.path {active_location}}",
"Version: {.val {active_version}}",
"Status: Pre-installed, no action needed"
))
cli::cli_text("") # Add spacing
}
if (verbose) cli::cli_progress_update(id = pb_id, set = 30)
cli::cli_text("") # Add spacing
}
if (verbose) cli::cli_progress_update(id = pb_id, set = 30)
} else if(!is_xcode_cli_installed()) {
if (verbose) {
cli::cli_progress_update(id = pb_id, set = 10)
cli::cli_alert_info("{.pkg macrtools}: Need to install Xcode Command Line Tools.")
cli::cli_bullets(c(
"Source: Apple Software Update",
"Installation method: softwareupdate command",
"Status: Not installed",
"Estimated time: 10-15 minutes"
))
cli::cli_text("") # Add spacing
}
} else {
if(describe_steps) {
# Get full Xcode app version information
xcode_app_info <- exec_text('xcodebuild', '-version')

cli::cli_alert_info("{.pkg macrtools}: Full Xcode.app IDE is installed.")
if (verbose) cli::cli_progress_update(id = pb_id, set = 20)
result_xcode <- xcode_cli_install(password = entered_password, verbose = describe_steps)

if(!result_xcode) {
cli::cli_abort(c(
"{.pkg macrtools}: Failed to install Xcode Command Line Tools.",
"This is a required component for R package development.",
"Installation status: Failed",
"i" = "Try installing manually by running 'sudo xcode-select --install' in Terminal."
))
}

# Installing does not guarantee the tools became REACHABLE: a stale
# selection or a DEVELOPER_DIR override still shadows them. Verify
# rather than trusting the installer's return value, so the summary
# cannot claim a working toolchain on a machine that cannot compile.
if(!is_xcode_toolchain_usable()) {
active_dir <- developer_dir()
selected <- if(base::is.na(active_dir)) "none selected" else active_dir

cli::cli_alert_warning(base::paste(
"{.pkg macrtools}: Xcode Command Line Tools were installed, but",
"no compiler is reachable yet."
))
cli::cli_bullets(c(
"Location: {.path {'/Applications/Xcode.app'}}",
"Version information: {.val {xcode_app_info}}",
"Status: Pre-installed, skipping Command Line Tools installation"
"Active developer directory: {.val {selected}}",
"i" = "Run {.code macrtools::xcode_cli_switch()} to select them, then retry."
))
cli::cli_text("") # Add spacing

result_xcode <- FALSE
}

if (verbose) cli::cli_progress_update(id = pb_id, set = 30)
} else {
# The Command Line Tools are on disk but no compiler is reachable, so
# the active developer directory is stale or broken. Reinstalling does
# not fix a bad selection, and reporting success here would claim a
# working toolchain on a machine that cannot compile.
active_dir <- developer_dir()
selected <- if(base::is.na(active_dir)) "none selected" else active_dir

cli::cli_alert_warning(base::paste(
"{.pkg macrtools}: Xcode Command Line Tools are installed, but no",
"compiler is reachable."
))
cli::cli_bullets(c(
"Command Line Tools: {.path {install_directory_xcode_cli()}}",
"Active developer directory: {.val {selected}}",
"i" = "Run {.code macrtools::xcode_cli_switch()} to select them, then retry."
))
cli::cli_text("") # Add spacing

result_xcode <- FALSE
if (verbose) cli::cli_progress_update(id = pb_id, set = 30)
}

Expand Down Expand Up @@ -359,13 +410,15 @@ macos_rtools_uninstall <- function(

# Step 1: Uninstall Xcode CLI
result_xcode <- TRUE
removed_xcode <- FALSE
if(is_xcode_cli_installed()) {
if (verbose) {
cli::cli_progress_update(id = pb_id, set = 30)
cli::cli_alert_info("{.pkg macrtools}: Uninstalling Xcode CLI...")
cli::cli_text("") # Add spacing
}
result_xcode <- xcode_cli_uninstall(password = password, verbose = verbose)
removed_xcode <- xcode_cli_uninstall(password = password, verbose = verbose)
result_xcode <- removed_xcode
if(!result_xcode) {
cli::cli_abort("{.pkg macrtools}: Failed to uninstall Xcode CLI. Please see manual steps.")
}
Expand All @@ -379,12 +432,14 @@ macos_rtools_uninstall <- function(
# Step 2: Uninstall gfortran
if (verbose) cli::cli_progress_update(id = pb_id, set = 70)
result_gfortran <- TRUE
removed_gfortran <- FALSE
if(is_gfortran_installed()) {
if (verbose) {
cli::cli_alert_info("{.pkg macrtools}: Uninstalling gfortran...")
cli::cli_text("") # Add spacing
}
result_gfortran <- gfortran_uninstall(password = password, verbose = verbose)
removed_gfortran <- gfortran_uninstall(password = password, verbose = verbose)
result_gfortran <- removed_gfortran
if(!result_gfortran) {
cli::cli_abort("{.pkg macrtools}: Failed to uninstall gfortran. Please see manual steps.")
}
Expand All @@ -405,8 +460,8 @@ macos_rtools_uninstall <- function(
if(clean) {
cli::cli_alert_success("{.pkg macrtools}: Uninstallation complete.")
cli::cli_bullets(c(
"Xcode CLI: Successfully removed",
"Gfortran: Successfully removed",
base::paste0("Xcode CLI: ", if (removed_xcode) "Successfully removed" else "Was not installed"),
base::paste0("Gfortran: ", if (removed_gfortran) "Successfully removed" else "Was not installed"),
"Note: This did not uninstall any binaries from the recipes project"
))
}
Expand Down
Loading