Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow non-admin user installs on macOS if prefix is user-writable #948

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

willmcginnis
Copy link

Allow macOS installation without sudo if prefix is user-writable

Previously, the installer always aborted if sudo was unavailable on macOS.

This change adds a check to determine if the Homebrew prefix is already writable by the current user.

If writable, the script proceeds without sudo.

Otherwise, it aborts with a clear message instructing users how to create and adjust permissions on the Homebrew prefix, enabling standard (non-admin) users to install Homebrew in that directory.

Developed and tested on 15.3.1

Allow macOS installation without sudo if prefix is user-writable

Previously, the installer always aborted if sudo was unavailable on macOS.

This change adds a check to determine if the Homebrew prefix is already writable by the current user.

If writable, the script proceeds without sudo.

Otherwise, it aborts with a clear message instructing users how to create and adjust permissions on the Homebrew prefix, enabling standard (non-admin) users to install Homebrew in that directory.

Developed and tested on 15.3.1
Used

brew style --fix install.sh
@@ -247,7 +252,18 @@ have_sudo_access() {

if [[ -n "${HOMEBREW_ON_MACOS-}" ]] && [[ "${HAVE_SUDO_ACCESS}" -ne 0 ]]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if [[ -n "${HOMEBREW_ON_MACOS-}" ]] && [[ "${HAVE_SUDO_ACCESS}" -ne 0 ]]
if [[ -n "${HOMEBREW_ON_MACOS-}" ]] && [[ "${HAVE_SUDO_ACCESS}" -ne 0 && ! -w "${HOMEBREW_PREFIX}" ]]

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured that if it's not writable then that's handled by the earlier part, but this extra check would be okay if you think it is good to have.

install.sh Outdated
Comment on lines 228 to 232
if [[ -n "${HOMEBREW_ON_MACOS-}" ]] && [[ -w "${HOMEBREW_PREFIX}" ]]
then
return 1
fi

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should put this in the have_sudo_access() function.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to go for total minimal changes. Everything seems to work smoothly in the script if:

  1. the homebrew prefix exists and is writable and
  2. have_sudo_access returns false and
  3. the script doesn't abort

My goal was minimally invasive but handles the above.
Other issues like permissions and the CLT are handled in other parts of the script.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't do a 'return 1' early, or something similar, then the script will prompt for sudo when it really doesn't actually need it to function and end up with a "This user is not in the sudoers file".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A better solution is to add a new function:

have_sudo_access_or_prefix_write_permission() {
    return [[ -n "${HOMEBREW_ON_MACOS-}" && -w "${HOMEBREW_PREFIX}" ]] || have_sudo_access
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure that's good

@SMillerDev
Copy link
Member

What if the directories under it are not writable?

@MikeMcQuaid
Copy link
Member

Thanks for the PR!

What if the directories under it are not writable?

This is my question, too. Can CLT install also be done without sudo?

Stepping back: can you explain the motivation for this?

@willmcginnis
Copy link
Author

What if the directories under it are not writable?

I debated trying to do some kind of 'make sure it's empty' but then I did some testing and the script itself actually does a lot of chmod-ing so that's actually handled unless a setup has gone out of its way to be like broken on purpose imo.

@willmcginnis
Copy link
Author

Thanks for the PR!

What if the directories under it are not writable?

This is my question, too. Can CLT install also be done without sudo?

Stepping back: can you explain the motivation for this?

You are welcome! Motivation:
I made a "standard" account, so not admin. It's a shared computer and I want to keep what security on it that I can, but also not deviate from the recommended homebrew install, so I tested how far I could get with the install script without sudo and found I only needed to make the prefix and chmod it with sudo, everything else worked fine without sudo.

CLT can be installed without sudo, strangely enough. I ran "python3" in a terminal, got a GUI prompt that said it needed developer tools and had a blue install button and clicking the blue install button worked fine to install the CLT tools.
(To be clear, this is on an account that was created as a "standard" account, is not in the "admin" group and never was an "Administrator" at the macOS system level. I don't know why this worked but somehow CLT can install without admin. It might be due to 'full disk access' allowed for the terminal app, full disclosure).

@MikeMcQuaid
Copy link
Member

I debated trying to do some kind of 'make sure it's empty' but then I did some testing and the script itself actually does a lot of chmod-ing so that's actually handled unless a setup has gone out of its way to be like broken on purpose imo.

I think having a "make sure it's empty" or other checks on permissions here is necessary to merge this, sorry.

CLT can be installed without sudo, strangely enough. I ran "python3" in a terminal, got a GUI prompt that said it needed developer tools and had a blue install button and clicking the blue install button worked fine to install the CLT tools.

This won't install it using the installer, though, which we want to preserve if not using sudo.

See

install/install.sh

Lines 837 to 856 in 839ae50

if should_install_command_line_tools && version_ge "${macos_version}" "10.13"
then
ohai "Searching online for the Command Line Tools"
# This temporary file prompts the 'softwareupdate' utility to list the Command Line Tools
clt_placeholder="/tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress"
execute_sudo "${TOUCH[@]}" "${clt_placeholder}"
clt_label_command="/usr/sbin/softwareupdate -l |
grep -B 1 -E 'Command Line Tools' |
awk -F'*' '/^ *\\*/ {print \$2}' |
sed -e 's/^ *Label: //' -e 's/^ *//' |
sort -V |
tail -n1"
clt_label="$(chomp "$(/bin/bash -c "${clt_label_command}")")"
if [[ -n "${clt_label}" ]]
then
ohai "Installing ${clt_label}"
execute_sudo "/usr/sbin/softwareupdate" "-i" "${clt_label}"
execute_sudo "/usr/bin/xcode-select" "--switch" "/Library/Developer/CommandLineTools"

Every other use of execute_sudo needs similarly audited.

@willmcginnis
Copy link
Author

I debated trying to do some kind of 'make sure it's empty' but then I did some testing and the script itself actually does a lot of chmod-ing so that's actually handled unless a setup has gone out of its way to be like broken on purpose imo.

I think having a "make sure it's empty" or other checks on permissions here is necessary to merge this, sorry.

CLT can be installed without sudo, strangely enough. I ran "python3" in a terminal, got a GUI prompt that said it needed developer tools and had a blue install button and clicking the blue install button worked fine to install the CLT tools.

This won't install it using the installer, though, which we want to preserve if not using sudo.

See

install/install.sh

Lines 837 to 856 in 839ae50

if should_install_command_line_tools && version_ge "${macos_version}" "10.13"
then
ohai "Searching online for the Command Line Tools"
# This temporary file prompts the 'softwareupdate' utility to list the Command Line Tools
clt_placeholder="/tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress"
execute_sudo "${TOUCH[@]}" "${clt_placeholder}"
clt_label_command="/usr/sbin/softwareupdate -l |
grep -B 1 -E 'Command Line Tools' |
awk -F'*' '/^ *\\*/ {print \$2}' |
sed -e 's/^ *Label: //' -e 's/^ *//' |
sort -V |
tail -n1"
clt_label="$(chomp "$(/bin/bash -c "${clt_label_command}")")"
if [[ -n "${clt_label}" ]]
then
ohai "Installing ${clt_label}"
execute_sudo "/usr/sbin/softwareupdate" "-i" "${clt_label}"
execute_sudo "/usr/bin/xcode-select" "--switch" "/Library/Developer/CommandLineTools"

Every other use of execute_sudo needs similarly audited.

Would it be worth having this with a check of && ! clt tools need to be installed?
Then the manual commands to set up beforehand would be the mkdir, chmod, and tools install.

The -w is more just a condensed way of going “the mkdir and chmod were followed”, it could be a -d and keep basically the same functionality.

I did want the change to be minimally invasive so if auditing execute sudo lines is required a separate function maybe is better?
(The script already is quite robust and clear about errors it encounters and tells the user how to fix them)

@MikeMcQuaid
Copy link
Member

Would it be worth having this with a check of && ! clt tools need to be installed?

I guess should_install_command_line_tools should handle this. I just want to ensure the e.g. error output in the case where you don't have sudo isn't degraded by this.

This is a larger change but it:
1. Has a check to make sure all files in the Homebrew prefix are writable
2. Specifically handles CLT installation
3. Cleanly separates it out into its own function

Uses running `chmod` as a check. If it fails then something isn't writable.
Bonus is it also is a permissions repair.

For the CLT install with macOS sudoless mode, the first method will fail, so within the fallback we check for macOS_sudoless_mode and if we're using it then `abort` with the message about manual installation, instead of running the GUI pop up manual installation.

This still retains the original goal of installing in the official prefixes without sudo, so it doesn't result in an "Alternative (unsupported) installation".
@willmcginnis
Copy link
Author

This is a larger change but it:

  1. Has a check to make sure all files in the Homebrew prefix are writable
  2. Specifically handles CLT installation
  3. Cleanly separates it out into its own function

Uses running chmod as a check. If it fails then something isn't writable.
Bonus is it also is a permissions repair.

For the CLT install with macOS sudoless mode, the first method will fail, so within the fallback we check for macOS_sudoless_mode and if we're using it then abort with the message about manual installation, instead of running the GUI pop up manual installation.

This still retains the original goal of installing in the official prefixes without sudo, so it doesn't result in an "Alternative (unsupported) installation".

Every use of execute_sudo and why it is safe with Using macOS Sudoless Mode

Line Number Command Usage (via execute_sudo) Check in macos_sudoless_check() handles checking write access Already world-writable by default Likely needs sudo
800 execute_sudo "${CHMOD[@]}" "u+rwx" "${chmods[@]}"
804 execute_sudo "${CHMOD[@]}" "g+rwx" "${group_chmods[@]}"
808 execute_sudo "${CHMOD[@]}" "go-w" "${user_chmods[@]}"
812 execute_sudo "${CHOWN[@]}" "${USER}" "${chowns[@]}"
816 execute_sudo "${CHGRP[@]}" "${GROUP}" "${chgrps[@]}"
819 execute_sudo "${INSTALL[@]}" "${HOMEBREW_PREFIX}"
824 execute_sudo "${MKDIR[@]}" "${mkdirs[@]}"
825 execute_sudo "${CHMOD[@]}" "ug=rwx" "${mkdirs[@]}"
828 execute_sudo "${CHMOD[@]}" "go-w" "${mkdirs_user_only[@]}"
830 execute_sudo "${CHOWN[@]}" "${USER}" "${mkdirs[@]}"
831 execute_sudo "${CHGRP[@]}" "${GROUP}" "${mkdirs[@]}"
836 execute_sudo "${MKDIR[@]}" "${HOMEBREW_REPOSITORY}"
838 execute_sudo "${CHOWN[@]}" "-R" "${USER}:${GROUP}" "${HOMEBREW_REPOSITORY}"
844 execute_sudo "${MKDIR[@]}" "${HOMEBREW_CACHE}"
851 execute_sudo "${CHMOD[@]}" "g+rwx" "${HOMEBREW_CACHE}"
855 execute_sudo "${CHOWN[@]}" "-R" "${USER}" "${HOMEBREW_CACHE}"
859 execute_sudo "${CHGRP[@]}" "-R" "${GROUP}" "${HOMEBREW_CACHE}"
871 execute_sudo "${TOUCH[@]}" "${clt_placeholder}"
884 execute_sudo "/usr/sbin/softwareupdate" "-i" "${clt_label}"
885 execute_sudo "/usr/bin/xcode-select" "--switch" "/Library/Developer/CommandLineTools"
887 execute_sudo "/bin/rm" "-f" "${clt_placeholder}"
909 execute_sudo "/usr/bin/xcode-select" "--switch" "/Library/Developer/CommandLineTools"

return "${MACOS_SUDOLESS_SATISFIED}"
fi

[[ -d "${HOMEBREW_PREFIX}" ]] && "${CHMOD[@]}" -R "ug+w" "${HOMEBREW_PREFIX}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should test (read) the file mode rather than change it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing would be a somewhat convoluted find and if there is an issue and you can fix it with a chmod, it does not make sense to do a find then a chmod if just the initial chmod would work and handle both cases.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the user does have sudo access and there are many many files under the prefix?

Copy link
Author

@willmcginnis willmcginnis Mar 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. Doing a check then bailing out at the first sign of not writable is a way to make it less bad.

If there are a lot of files, and the user does have sudo, then yes, as it is now and also with find it would do a large check over all of them.

There is a chicken and egg of what comes first of running without sudo and making sure it can run without sudo.

My thoughts go to a begin/rescue or try/except pattern fitting the issue well but that’s a larger change.


sudo mkdir -p "${HOMEBREW_PREFIX}"
sudo chown -R "${USER}:${GROUP}" "${HOMEBREW_PREFIX}"
sudo chmod -R "ug+w" "${HOMEBREW_PREFIX}"
Copy link
Contributor

@XuehaiPan XuehaiPan Mar 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not a good idea to add write permission to the group for all files/dirs. For example, you should remove the write access after installation:

chmod -R go-w "${HOMEBREW_PREFIX}/share/zsh"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An earlier objection was "What if the directories under it are not writable?"
so then if we only want some to be writable then
What is a clean way to determine what should have write and what shouldn't?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the permission for the user is enough, right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just matching other chmods in the script. But yes just the user is enough, agreed.

@willmcginnis
Copy link
Author

The check, either a direct chmod or find at least only has to run once due to the memoization.

One thought is to try to craft the most sane xargs to utilize cores for the single write check.

@willmcginnis
Copy link
Author

Replacing this:
[[ -d "${HOMEBREW_PREFIX}" ]] && "${CHMOD[@]}" -R "ug+w" "${HOMEBREW_PREFIX}" MACOS_SUDOLESS_SATISFIED=$?

With this:
find "${HOMEBREW_PREFIX}" -print0 | xargs -0 -P "$(sysctl -n hw.ncpu)" "${CHMOD[@]}" "u+w" MACOS_SUDOLESS_SATISFIED=$?

Would be more efficient if that's acceptable.

@willmcginnis
Copy link
Author

I took a new homebrew install prefix directory and copied it so I had 31 copies, for a total of:

will@macmini chmod_test % find /opt/homebrew/padding_area/ -type d | wc -l                            
   30567
will@macmini chmod_test % find /opt/homebrew/padding_area/ -type f | wc -l
  220720
will@macmini chmod_test % du -hcs /opt/homebrew/padding_area/
7.9G	/opt/homebrew/padding_area/
7.9G	total

And did chmod -R and compared it with parallel xargs with various args per cmd (-n).
Between runs I reset permissions with chmod -R u-w and this reset was not included in the times.

Commands Tested in Benchmark

Standard recursive chmod

chmod -R u+w /opt/homebrew/padding_area

Parallel xargs with optimal configuration (n=32)

find "/opt/homebrew/padding_area" -print0 | xargs -0 -P 14 -n 32 chmod u+w

The benchmark tested this parallel approach with different values for the -n parameter (8, 16, 32, 64, 128, 256) while keeping the processor count at 14 (from sysctl -n hw.ncpu).

Results Summary

Method Parallelism Args per cmd (-n) Run 1 (s) Run 2 (s) Run 3 (s) Avg Time (s) Speedup
Standard chmod -R N/A N/A 5.765 5.680 5.804 5.749 1.00x
Parallel xargs P=14 Default 6.034 5.315 4.704 5.351 1.07x
Parallel xargs P=14 n=8 18.030 18.266 18.302 18.199 0.32x
Parallel xargs P=14 n=16 9.190 9.201 9.193 9.194 0.63x
Parallel xargs P=14 n=32 4.992 4.999 5.032 5.007 1.15x
Parallel xargs P=14 n=64 5.328 5.544 5.524 5.465 1.05x
Parallel xargs P=14 n=128 5.845 6.029 6.111 5.995 0.96x
Parallel xargs P=14 n=256 6.098 5.809 6.008 5.971 0.96x

So for 250k files it took 5 to 6 seconds.

@MikeMcQuaid
Copy link
Member

I'm increasingly tempted to pass on this, sorry @willmcginnis. The complexity required to make this work does not seem to match the number of people who need this functionality (as far as I can tell: perhaps only @willmcginnis).

For this to get merged it needs to:

  • provide all the functionality possible in non-sudo "mode"
  • not change any behaviour/permissions/etc. for users with sudo, even in theory
  • add/change as few lines as possible

@willmcginnis
Copy link
Author

Thank you the reasons Mike. I agree I haven’t seen it discussed but maybe because was assumed to not be possible.
Since the overall homebrew program can run fine without sudo, the whole flow is most of the way there for an almost entirely sudoless experience.

One option that you might be okay with that could satisfy those is through the use of a SUDOLESS environment variable, like the NONINTERACTIVE environment variable, to make its usage explicit, and self-contained.

Placing just below

have_sudo_access() {
  if [[ ! -x "/usr/bin/sudo" ]]
  then
    return 1
  fi

The shortest would be:

  if [[ -n "${SUDOLESS-}" ]]
  then
    ohai 'Running in sudoless mode because `$SUDOLESS` is set.
    return 1
  fi

The other option I see is a function with more checks and an abort if SUDOLESS is set but fails for how to fix it, but since this is explicit usage then that might not be necessary.

@MikeMcQuaid
Copy link
Member

One option that you might be okay with that could satisfy those is through the use of a SUDOLESS environment variable, like the NONINTERACTIVE environment variable, to make its usage explicit, and self-contained.

If this seems like less code that the current version: this seems like a good idea to both implement and document in the README 👍🏻

@Monivali2020

This comment was marked as off-topic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants