-
-
Save joevt/e3cd4ff08aae06279134969c98ca3ab7 to your computer and use it in GitHub Desktop.
#!/bin/bash | |
# by joevt Feb 6, 2024 | |
# Jan 22, 2023 - Updated to work in Mac OS X 10.4. | |
# Jan 30, 2023 - Fixed the method options. | |
# Apr 7, 2023 - Fixed parse of unknown prog-if and move temp files to separate tmp directory. | |
# Feb 6, 2024 - Fix column alignment. | |
#=================== | |
if [[ $EUID -ne 0 ]]; then | |
echo "This script must be run as root like this:" | |
echo "sudo $0" | |
exit 1 | |
fi | |
#=================== | |
style="wide" | |
style="narrow" | |
#encoding="ascii" | |
encoding="utf8" | |
method="" # default method which for macOS on Intel is -Adarwin | |
#method="-Adarwin" # uses AppleACPIPlatformExpert user client | |
#method="-Adarwin2" # uses IOPCIBridge user client | |
#method="-Adarwin3" # uses IOPCIBridge through DirectHW user client | |
#method="-Aintel-conf1" # uses I/O to CF8h/CFCh (requires Intel DirectHW.kext on macOS) | |
#method="-H1" # same as -Aintel-conf1 | |
#=================== | |
if [ $encoding == "ascii" ]; then | |
box0='\' | |
box1='-' | |
box2='\' | |
box3='+' | |
box4='|' | |
box5='.' | |
else | |
box0='┐' | |
box1='─' | |
box2='└' | |
box3='├' | |
box4='│' | |
box5='┬' | |
fi | |
if [ $style == "wide" ]; then | |
if [ $encoding == "ascii" ]; then | |
# since we're using \, we want to indent one extra space | |
rootindent=' ' | |
subindent=' ' | |
else | |
rootindent=' ' | |
subindent=' ' | |
fi | |
domainprefix="\\${box1}\\[" | |
domainsuffix="\\]\\${box1}\\${box0}" | |
bridgeprefix="\\${box3}\\${box1}" | |
bridgesuffixlong="\\${box1}\\${box1}\\${box1}\\${box1}\\${box0}" | |
bridgesuffix="\\${box1}\\${box0}" | |
deviceprefix="\\${box3}\\${box1}" | |
devicesuffix="" | |
else | |
rootindent='' | |
subindent=' ' | |
domainprefix="\\${box5}\\[" | |
domainsuffix="\\]" | |
bridgeprefix="\\${box3}\\${box5}" | |
bridgesuffixlong="" | |
bridgesuffix="" | |
deviceprefix="\\${box3}\\${box1}" | |
devicesuffix="" | |
fulladdress=1 | |
fi | |
#=================== | |
#tmp=/tmp/pcitree | |
tmp="$(mktemp -d "/tmp/pcitree.XXXX")" | |
mkdir -p "$tmp" | |
chmod 777 "$tmp" | |
# Get header type and bus information for all pci devices - reverse sort so last bus is first (required for device path substitution step below). | |
# Format is "domain:bus%device.function domain:primary domain:secondary domain:subordinate". | |
# Keep the domain number for device path substitution step below. | |
# -0777 allows perl to process all lines at once - here we process two lines at a time. | |
setpci $method -v -s '*:*:*' HEADER_TYPE.b 0x18.l 2> /dev/null | perl -0777 -pe ' | |
s/((.{4}):(..):(..\..)) \@0e = ([08][12])\n\1 \@18 = ..(.{2})(.{2})(.{2})\n/$2:$3%$4 $2:$8 $2:$7 $2:$6\n/g; # bridge(01) or card bus(02) header type, multifunction(80) or not (00). | |
s/((.{4}):(..):(..\..)) \@0e = (..)\n\1 \@18 = ..(.{2})(.{2})(.{2})\n/$2:$3%$4\n/g # endpoint (or anything else) header type | |
' \ | |
| sort -r > "$tmp"/pcilist.txt | |
# If primary = secondary = subordinate then that is a problem - change them to XX. | |
sed '/^\(....:..%..\..\) \(\(.\{4\}:\)..\) \2 \2$/s//\1 \3XX \3XX \3XX/' "$tmp"/pcilist.txt > "$tmp"/pcilist2.txt | |
echo '/ ....:/s// /g' > "$tmp"/pcipaths.txt # Add a command that will remove domain from primary, secondary, and subordinate bus numbers. | |
# Create device path substitution commands that will add parent bus to each secondary bus. | |
# When applied to the list of pci devices, the commands will create a full device path for | |
# each pci device. Multiple commands will be applied to each device to build the device | |
# path from right to left: end point (greatest bus number) to domain (lowest bus number). | |
# The commands are sorted from highest bus number to lowest bus number to make that possible. | |
sed -nE '/^(....:..%..\..) .{4}:.. (.{4}:..) .*/s//\/(\2)\/s\/\/\1%\\1\/g/p' "$tmp"/pcilist2.txt >> "$tmp"/pcipaths.txt | |
sed -E -f "$tmp"/pcipaths.txt "$tmp"/pcilist2.txt > "$tmp"/pcitree1.txt # Apply the device path substitution commands. | |
sed -E '/^(....:..).*/s//\1/g' "$tmp"/pcitree1.txt | sort -u > "$tmp"/pcitree2.txt # Get all root complexes (usually just 0000:00). | |
cat "$tmp"/pcitree1.txt "$tmp"/pcitree2.txt | LC_ALL=C sort > "$tmp"/pcitree3.txt # Concatenate the devices and root complexes and sort. | |
# Use perl to do some formatting. | |
perl -CSDA -Mutf8 -pe ' | |
s/^(.*(\w\w\w\w:\w\w%\w\w\.\w).*)/\1 # \2/; # save domain/bus/device/function after a " # " - this will be used to append name and ids | |
s/^\w\w\w\w:\w\w%/'"$rootindent"'/; # remove domain/bus number from left side and indent | |
s/\w\w\.\w%\w\w\w\w:\w\w%/'"$subindent"'/g; # use the paths to do more indenting | |
s/(\w\w\.\w) \w\w (\w\w) \2/'"${bridgeprefix}"'\1-[\2]'"${bridgesuffixlong}"'/; # format the secondary and subordinate bus numbers for single bus bridge | |
s/(\w\w\.\w) \w\w (\w\w) (\w\w)/'"${bridgeprefix}"'\1-[\2-\3]'"${bridgesuffix}"'/; # format the secondary and subordinate bus numbers for multiple bus bridge | |
s/%/:/; # replace % bus/device delimiter with standard ":" | |
s/(\w\w\.\w )/'"${deviceprefix}"'\1'"${devicesuffix}"'/; # add the "+-" formating for devices | |
s/^(\w\w\w\w:\w\w)$/'"${domainprefix}"'\1'"${domainsuffix}"'/ # add the -[ ]\" for domain/bus | |
' \ | |
"$tmp"/pcitree3.txt > "$tmp"/pcitree4.txt | |
# https://perldoc.perl.org/perlrun.html | |
# The -C flag controls some of the Perl Unicode features. | |
# S: STDIN, STDOUT and STDERR are assumed to be in UTF-8. | |
# D: UTF-8 is the default PerlIO layer for both input and output streams. | |
# A: @ARGV elements are expected to be strings encoded in UTF-8. | |
# -0[octal/hexadecimal] specifies the input record separator ($/ ) as an octal or hexadecimal number. | |
# Any value 0400 or above will cause Perl to slurp files whole, but by convention the value 0777 is the one normally used for this purpose. | |
# -Mmodule executes use module ; before executing your program. This loads the module and calls its import method, causing the module to have its default effect, typically importing subroutines or giving effect to a pragma. | |
# -p causes Perl to assume a loop around your program, which makes it iterate over filename arguments somewhat like sed. | |
# -e commandline May be used to enter one line of program. | |
perl -CSDA -Mutf8 -0777 -pe ' | |
# add | between devices of the same bus | |
do { | |
$good=0; | |
while ( | |
/ # begin regular expression | |
( # begin g1 | |
\n # \n | |
([\ '"\\${box4}"']*) # [ |]* -> g2 | |
['"\\${box3}\\${box4}"'] # [+|] | |
[^\n]* # rest of the line up to \n, (change + to *+ to give nothing back) | |
(?{$X=pos()}) # mark position | |
\n # \n | |
) # end g1 | |
(?= # begin lookahead | |
( # begin group | |
\2\ [^\n]* # following line(s) start with g2 also (change + to *+ to give nothing back) | |
\n # | |
)+ # end group, match one or more, (change + to ++ to give nothing back) | |
\2 # the line after those start with g2 | |
'"\\${box3}"' # and "+" meaning that another device follows g1 on the same bus | |
) # end lookahead | |
( | |
\2 # | |
(?{$C=pos()}) # $C is the position where we want to insert the | | |
\ # the character at position $C to be modified is a space character | |
) # | |
/gx # end regular expression (g=global; x=ignore white space in RE) | |
) { | |
$good = 1; | |
substr($_,$C,1) = "'"\\${box4}"'"; # replace the space character at position $C with a | | |
pos() = $X; # start next search at $X which is the end of the previous line | |
} | |
} until ($good == 0); | |
# replace + with \ for last device of a bus | |
s/(\n[\ '"\\${box4}"']*)'"\\${box3}"'(?![^\n]*\1['"\\${box3}\\${box4}"'])/\1'"\\${box2}"'/g; | |
' "$tmp"/pcitree4.txt > "$tmp"/pcitree5.txt | |
# Create the substitutions for the detail column | |
lspci $method -nnv -D 2> /dev/null > "$tmp"/pcidevices0_lspci.txt | |
perl -pe ' | |
s/^\n//g; | |
s/^[ \t].*\n//g; | |
s|/|\\/|g; | |
s|^(....:..:..\..) ([^[]*) (\[....\])(.*) (\[....:....\])( \(rev ..\))?(?: \(prog-if ..(( \[.*?\])?)\))?|/# (\1)\$/s//'" "'# \\1 \5 \3:\6 : \2\7 \4/|; | |
s/\]:([^:]{9}.*?) *:([^:]{37}.*?) *:/]\1\2 :/ # rev column is 9 characters and type column is 36 characters | |
' \ | |
"$tmp"/pcidevices0_lspci.txt > "$tmp"/pcidevices1_lspci_sed.txt | |
# Get link width and speed max/current. | |
pat="(....:..:..\..) @09 = (..)" | |
IFS=$'\n' | |
echo "" > "$tmp"/pcidevices2_link_sed.txt | |
echo "" > "$tmp"/pcidevices1_progintf_sed.txt | |
for thedevice in $(sudo setpci $method -v -s '*:*:*' CLASS_PROG.b 2> /dev/null); do | |
pcidevice="${thedevice% @*}" | |
proginterface="${thedevice#* = }" | |
# We have to do this in a loop because setpci stops if express capability doesn't exist | |
LinkRegisters=$(sudo setpci $method -s $pcidevice CAP_EXP+12.w CAP_EXP+c.l 2> /dev/null) | |
if [[ -n $LinkRegisters ]]; then | |
LinkStatusRegister=${LinkRegisters:0:4} | |
LinkWidth=$((0x$LinkStatusRegister >> 4 & 31)) | |
if [[ $LinkWidth -ge 0 ]]; then | |
LinkSpeed=$((0x$LinkStatusRegister & 15)) | |
LinkCapabilitiesRegister=${LinkRegisters:5:8} | |
MaxLinkWidth=$((0x$LinkCapabilitiesRegister >> 4 & 31)) | |
MaxLinkSpeed=$((0x$LinkCapabilitiesRegister & 15)) | |
echo "/# ($pcidevice .*$)/s//# g${MaxLinkSpeed}x${MaxLinkWidth} > g${LinkSpeed}x${LinkWidth} \1/" >> "$tmp"/pcidevices2_link_sed.txt | |
fi | |
fi | |
echo "/(# +$pcidevice \[....:....\] \[....)/s//\1${proginterface}/" >> "$tmp"/pcidevices1_progintf_sed.txt | |
done | |
perl -CSDA -Mutf8 -pe ' | |
s/# (g.x..?) > \1 /# \1 /; # if the links are the same then output it only once | |
s|(/s//#.{16}) *|\1|; # make the link column 16 characters wide | |
' "$tmp"/pcidevices2_link_sed.txt > "$tmp"/pcidevices3_link_sed.txt | |
# If there's only one domain/segment then remove them from the output. | |
if [[ $(sed -E '/(....).*/s//\1/' "$tmp"/pcitree2.txt | sort -u | wc -l) -eq 1 ]]; then | |
echo "/(#.{16}).{4}:/s//\\1/" >> "$tmp"/pcidevices3_link_sed.txt | |
fi | |
if [[ $fulladdress -eq 1 ]]; then | |
echo "/..\..([^#]*#.{16})([0-9a-f.:]+) /s//\\2\\1/" >> "$tmp"/pcidevices3_link_sed.txt | |
fi | |
# Apply the substitutions | |
sed -E -f "$tmp"/pcidevices1_lspci_sed.txt -f "$tmp"/pcidevices1_progintf_sed.txt -f "$tmp"/pcidevices3_link_sed.txt "$tmp"/pcitree5.txt > "$tmp"/pcitree6.txt | |
# Set width of the first column to the minimum required | |
spaces=$(perl -CSDA -Mutf8 -e '$spaces=0; while (<>) { if (/^([^#]*?) *#/) { $s = length($1); if ($s > $spaces) { $spaces = $s }}} print $spaces' "$tmp"/pcitree6.txt) | |
perl -CSDA -Mutf8 -pe 's/^(.{'"$((spaces+2))"'}) */$1/' "$tmp"/pcitree6.txt > "$tmp"/pcitree7.txt | |
echo '#=========================================================================================' | |
cat "$tmp"/pcitree7.txt # final output |
Setup
The script should work unmodified for Linux.
DirectHW.kext is required for PowerPC Macs running Mac OS X 10.4 or 10.5 (or any Mac OS X version).
DirectHW.kext may be required for Intel Macs running macOS 10.11 El Capitan or 10.12 Sierra.
Sierra might panic if you try the darwin2 access method.
It might work for Apple Silicon Macs without DirectHW.kext using the darwin2 access method but this is untested. DirectHW.kext on Apple Silicon Macs is also untested.
The darwin and darwin2 access methods of pciutils require nvram boot-args
to include debug=0x144
. If it does not, then add it:
sudo nvram boot-args="debug=0x144"
This may require SIP to be disabled, otherwise execute the command without sudo
in Recovery's Terminal.app.
Don't forget to include the existing contents of boot-args inside the quotes.
The value used for debug=
might not matter.
Download the script
cd ~
git clone https://gist.github.com/e3cd4ff08aae06279134969c98ca3ab7.git pcitree
cd pcitree
chmod +x pcitree.sh
(Optional) Install pciutils (this example uses my fork)
If the lspci
command doesn't exist already (likely for macOS but not Linux), then you need to install pciutils.
cd ~
git clone https://github.com/joevt/pciutils.git
cd pciutils
make
sudo make install
grep -q /usr/local/sbin /etc/paths || sudo sed -e $'1i\\\n/usr/local/sbin\\\n' -i "" /etc/paths
cd ~
Create a new Terminal window to start using the new commands.
Occasionally, update the pciids database:
sudo update-pciids
Test the pciutils commands (-A detect
is new option in my fork):
sudo lspci -A detect
sudo lspci
Finally, try the pcitree script:
cd ~/pcitree
sudo ./pcitree.sh
I created a EFI driver called FixPCIeLinkRate.efi which also produces similar output to pcitree.sh. https://forums.macrumors.com/threads/opencore-and-the-2008-mac-pro-3-1.2287044/post-30087837
https://github.com/joevt/joevtApps
Updates
Feb 6, 2024
- Fix column alignment.
Apr 7, 2023
- Fixed parse of unknown prog-if
- Moved temporary files to separate tmp directory.
Jan 30, 2023
- Fixed list of alternate access methods.
Jan 22, 2023
- Updated to work in Mac OS X 10.4. Now it can be used for PowerPC Macs.
Jan 3, 2023
- Remove domain numbers if they're all the same.
Dec 14, 2022
- Updated for latest pciutils.
- Includes program interface in the class code columns (the numeric column and the text column).
Tested in Ubuntu and macOS. Use it like this:
sudo pcitree.sh
Output should look like this: