macOS Catalina introduces "firmlinks" to the APFS filesystem. Is there a way via a CLI tool to determine whether /Users is a firmlink and where it is firmlinking to? I'm looking for something similar to the readlink
command for symlinks.
3 Answers
A list of the system firmlinks can be found in the file /usr/share/firmlinks.
On macOS Catalina version 10.15.1, the contents of the firmlinks file appear as follows:
/AppleInternal AppleInternal
/Applications Applications
/Library Library
/System/Library/Caches System/Library/Caches
/System/Library/Assets System/Library/Assets
/System/Library/PreinstalledAssets System/Library/PreinstalledAssets
/System/Library/AssetsV2 System/Library/AssetsV2
/System/Library/PreinstalledAssetsV2 System/Library/PreinstalledAssetsV2
/System/Library/CoreServices/CoreTypes.bundle/Contents/Library System/Library/CoreServices/CoreTypes.bundle/Contents/Library
/System/Library/Speech System/Library/Speech
/Users Users
/Volumes Volumes
/cores cores
/opt opt
/private private
/usr/local usr/local
/usr/libexec/cups usr/libexec/cups
/usr/share/snmp usr/share/snmp
I'm also looking for this. In the meantime, I've resorted to using comparing the results of pwd -P
(show physical path of process working directory) with that of df .
, e.g.:
# cd /Users ; pwd -P ; df .; cd - >/dev/null
/Users
Filesystem 512-blocks Used Available Capacity iused ifree %iused Mounted on
/dev/disk1s1 1953595632 1384119832 545065168 72% 1287392 9766690768 0% /System/Volumes/Data
(The final cd -
switches back to the original directory, and prints out the directory path, hence the redirection to /dev/null
.)
Note how the output of pwd -P
says /Users
, while df
says it is mounted on /System/Volumes/Data
.
Note: the answer below is a partial non-answer, because there is something wrong with the firmlink flag in macOS or in stat
. (An explanation or solution would be most welcome.)
XNU's /sys/stat.h has a flag for firmlinks (SF_FIRMLINK
). Normally you would check for a flag by reading the decimal output of all user flags with the stat command, converting it to hexadecimal, and testing bitwise against the hexadecimal value of the respective flag.
Example for Applications
, testing for SF_NOUNLINK
(0x00100000
):
filepath="/Applications"
flag_hex="0x00100000"
st_flags=$(printf '0x%x' $(stat -f %f "$filepath"))
[[ $((( $(echo "$st_flags") & $flag_hex ))) -eq 0 ]] && echo "Flag $flag_hex is *not* set" || echo "Flag $flag_hex is set"
This will print that Flag 0x00100000 is set
.
Conversely, testing for an unset flag, e.g. UF_HIDDEN
(0x00008000
), would result in a negative, because in our case /Applications
is not a hidden file.
filepath="/Applications"
flag_hex="0x00008000"
st_flags=$(printf '0x%x' $(stat -f %f "$filepath"))
[[ $((( $(echo "$st_flags") & $flag_hex ))) -eq 0 ]] && echo "Flag $flag_hex is *not* set" || echo "Flag $flag_hex is set"
The problems, however, begin with firmlinks. Let's test for the firmlink flag of /Applications
:
filepath="/Applications"
flag_hex="0x00800000"
st_flags=$(printf '0x%x' $(stat -f %f "$filepath"))
[[ $((( $(echo "$st_flags") & $flag_hex ))) -eq 0 ]] && echo "Flag $flag_hex is *not* set" || echo "Flag $flag_hex is set"
This should output Flag 0x00800000 is set
, but it does not. Instead it tells us that the SF_FIRMLINK
flag is not set, even though /Applications
is objectively a firmlink.
When you just look at the variable $st_flags
…
filepath="/Applications"
st_flags=$(printf '0x%x' $(stat -f %f "$filepath"))
echo "$st_flags"
…then the output is just 0x100000
(SF_NOUNLINK
only), but not 0x900000
(combined SF_NOUNLINK
and SF_FIRMLINK
).
This means that either macOS is not writing this flag at all, even though it's defined in /sys/stat.h
, or (more likely) Apple have deliberately kept this flag out of stat
, lstat
etc.
Either way, for the command-line it currently seems that we can only base a solution on Hambly's answer, unless someone knows or can create a tool that is somehow able to read these file flags without using stat/lstat. You would probably have to apply methods like getattrlist(), which according to its man page is like a "seriously enhanced version of stat(2)".
For the command-line you probably should also take synthetic firmlinks into account. So a quick solution (firmlink yes|no) would look e.g. like this:
firmlinks=$(awk '{print $1}' < /usr/share/firmlinks)
synthetics=$(grep -v -e "^$" -e "\#" < "/etc/synthetic.conf" | awk '{print "/"$1}')
firmlinks=$(echo -e "$firmlinks\n$synthetics" | grep -v "^$" | sort)
for filepath in "$@"
do
filepath=$(echo "$filepath" | sed 's-/$--')
echo "$firmlinks" | grep -q "^$filepath$" &>/dev/null && echo "Firmlink: $filepath"
done
The following would be a proper solution to also print the actual link information e.g. Firmlink: /Users -> Users
firmlinks=$(cat /usr/share/firmlinks 2>/dev/null)
synthetics=$(grep -v -e "^$" -e "\#" < "/etc/synthetic.conf" 2>/dev/null | sed 's-^-/-g')
firmlinks=$(echo -e "$firmlinks\n$synthetics" | grep -v "^$" | sort)
for filepath in "$@"
do
filepath=$(echo "$filepath/" | sed 's-/*$--')
firmlink_raw=$(echo "$firmlinks" | grep "^$filepath\t")
if [[ $firmlink_raw ]] ; then
firmlink1=$(echo "$firmlink_raw" | awk -F\t '{print $1}')
firmlink2=$(echo "$firmlink_raw" | awk -F\t '{print $2}')
echo "Firmlink: $firmlink1 -> $firmlink2"
fi
done
-
2Use getattrlistbulk() instead of getattrlist() to get
ATTR_CMN_FLAGS
, which will return au_int32_t
withSF_FIRMLINK
set Commented Jun 16 at 13:03