There are probably multiple tools like that already but perhaps you like my try at it. UNTESTED ON MACOS (and AmigaOS) - feedback needed.
It checks the compilers in the preference file, and adds new discoveries to the list. You can also start it repeatedly because all old compilers (if not broken/gone) will be preserved. It also compares the installed version strings with the latest available updates. It uses default search paths when no parameters are given, it can be pretty fast too.
When launched from PureBasic IDE, or if the IDE is running in the background, the changes will be undone by the IDE overwriting the preference file on exit. So you can use that to TEST FIRST from IDE if you like (run with debugger on to have time to see the console output). This may help identify unnecessary installations, too. On my 64bit Windows machine I have 36 different working versions & backends as of now

Usage examples:
listcompilers
listcompilers /
listcompilers D: E: F: G:
Screenshot from virtual machine

Code: Select all
; ==============================================================
; === (c) 2024 by benubi ===
; ==============================================================
;
; Source: ListCompilers.pb
; Thread:
; Author: benubi
; Date: 3/Jan/2024
; OS: All
; License: Free
; Description:
; Scan directories for new, existing or deleted PureBasic versions, check for installation of latest updates/betas, update "MoreCompilers" in PureBasic.prefs.
; The tool can be launched with an arbitrary amount of drives/directories to scan as start parameters.
;
; THE TOOL WILL REWRITE THE "MORECOMPILERS" GROUP IN THE MAIN IDE PREFERENCES FILE (purebasic.pref)
;
; When ran without parameters the tool will use following paths to search for pbcompiler/pbcompilerc executables:
;
; Scan for compilers in (default paths): [ Main() ]
; =================================================
; Windows: %ProgramFiles% and %ProgramFiles(x86)% (latter only on 64bit machines)
; Linux/MacOS: /home/<UserName>/ aka ~/ (untested on MacOS)
; AmigaOS: SYS: - just for the fun of it
;
; Preferences files: [ OpenPureBasicPrefs(), LoadUpdateCheck() ]
; ==============================================================
; Windows: %APPDATA%\PureBasic\
; Linux/MacOS: ~/.purebasic/
; AmigaOS: -guessed-
;
;
; When a candidate executable is found it is started with RunProgram() and --standby parameter; then version number,
; version string and MD5 hash are gathered and added to lists (there are a few checks to make double entries and redirection loops improbable)
;
; The results are sorted descending so that the latest versions are on top of the list and oldest on the bottom.
;
#APP_VERSION = 095
; ------------------------------------------------------
;XIncludeFile "UpdateConsoleMetrics.pb"
; /////////////////////////////////////////////////////
; -8<---------------------------------------------------
CompilerIf Not Defined(UpdateConsoleSize, #PB_Procedure) ; lib place holders & replacements
Procedure InitConsoleMetrics()
EndProcedure
Procedure UpdateConsoleSize()
EndProcedure
Procedure ConsoleWidth()
ProcedureReturn 80 ; standart/preset terminal width on most systems
EndProcedure
CompilerEndIf
; --->8-------------------------------------------------
Procedure ConsoleBold(bool) ; bold font on/off (non-Windows only)
CompilerIf #PB_Compiler_OS <> #PB_OS_Windows
EnableGraphicalConsole(0)
If bool
Print(Chr(27) + "[1m")
Else
Print(Chr(27) + "[22m")
EndIf
EnableGraphicalConsole(1)
CompilerEndIf
EndProcedure
Structure CompilerInfo ; Structure to store gathered compilers information
Path.s
Flags.i
Name.s
OsName.s
CpuName.s
ExeMd5.s
VersionMajor.i
VersionMinor.i
EndStructure
Global NewMap PathChecked.i() ; to avoid checking same path twice, prevent redirection loops
Global NewMap CompilerInfo.i() ; pointer to CompilerInfo structure in List
Global OldCompilerCount
Global DeletedCompilers
Global NewCompilerCount
Global FinalCompilerCount
NewList DeleteCompilers.CompilerInfo() ; list of compilers to delete (uninstalled, lost, broken link installations)
Procedure VerbosePathScan(path$) ; display a path (sometimes)
Static ldate
Static last_update = -100
Static cw = 0
If ElapsedMilliseconds() - last_update >= 1000 / 15
last_update = ElapsedMilliseconds()
If cw = 0 Or Date() <> ldate
UpdateConsoleSize()
ldate = Date()
cw = ConsoleWidth() - 1
EndIf
Protected msg$ = "Searching: "
If Len(msg$ + path$) > cw
Protected fname$ = GetFilePart(path$)
Protected ppart$ = GetPathPart(path$)
Protected tf$ = ".." + #PS$ + fname$
If Len(msg$ + tf$) > cw
tf$ = msg$ + Left(tf$, cw - Len(msg$))
Else
tf$ = msg$ + Left(ppart$, cw - Len(tf$ + msg$)) + tf$
EndIf
Else
tf$ = msg$ + path$
EndIf
Print(#CR$ + LSet(tf$, cw))
EndIf
EndProcedure
Procedure ListCompilers(Root$, boolVerbose, List Result.CompilerInfo() ) ; Search for PureBasic compilers
Protected dh = ExamineDirectory( - 1, root$, "")
Protected type, name$, path$, prog, ID$, SHORT_VERSION$, LONG_NAME$, RDY$
Shared PathChecked()
Shared CompilerInfo()
Root$ = ReplaceString(Root$, #PS$ + #PS$, #PS$)
While Left(Root$, 2) = #PS$ + #PS$
Root$ = Mid(Root$, 2)
Wend
While Right(Root$, 1) = "/" Or Right(Root$, 1) = "\"
Root$ = Left(Root$, Len(Root$) - 1)
Wend
If FindMapElement(PathChecked(), Root$)
ProcedureReturn
EndIf
If dh
AddMapElement(PathChecked(), Root$)
PathChecked() = 1
While NextDirectoryEntry(dh)
If boolVerbose
VerbosePathScan(Root$)
EndIf
name$ = DirectoryEntryName(dh)
type = DirectoryEntryType(dh)
Select StringField(name$, 1, ".")
Case "pbcompiler", "pbcompilerc"
If type = 1
; candidate!
path$ = root$ + #PS$ + name$
If FindMapElement(CompilerInfo(), path$) = 0
SetEnvironmentVariable("PUREBASIC_HOME", GetPathPart(root$))
SetEnvironmentVariable("LD_LIBRARY_PATH", root$)
prog = RunProgram(path$, "--standby", root$ , #PB_Program_Connect | #PB_Program_Open | #PB_Program_Write | #PB_Program_Read | #PB_Program_Error | #PB_Program_Hide )
If prog
AddElement(Result())
Result()\Path = path$
ID$ = ReadProgramString(prog)
SHORT_VERSION$ = StringField(ID$, 2, #TAB$)
LONG_NAME$ = StringField(ID$, 3, #TAB$)
Repeat
RDY$ = StringField(ReadProgramString(prog), 1, #TAB$)
Until RDY$ = "ERROR" Or RDY$ = "READY" Or Not ProgramRunning(prog)
If rdy$ = "READY"
WriteProgramStringN(prog, "END")
EndIf
If IsProgram(prog)
CloseProgram(prog)
EndIf
If FindString(LONG_NAME$, "PureBasic")
result()\Name = LONG_NAME$
result()\OsName = StringField(StringField(LONG_NAME$, 2, "("), 1, " ")
result()\CpuName = StringField(StringField(LONG_NAME$, 2, "("), 2, " ")
result()\VersionMajor = Val(StringField(SHORT_VERSION$, 1, "."))
result()\VersionMinor = Val(StringField(SHORT_VERSION$, 2, "."))
result()\ExeMd5 = FileFingerprint(path$, #PB_Cipher_MD5)
result()\Flags = 1 ; is new
AddMapElement(CompilerInfo(), path$)
CompilerInfo() = Result()
NewCompilerCount = NewCompilerCount + 1
Else
DeleteElement(result())
EndIf
EndIf
EndIf
EndIf
Default
If type = 2 And name$ <> ".." And name$ <> "."
;Debug "list "+name$
ListCompilers(root$ + #PS$ + name$, boolVerbose, result())
EndIf
EndSelect
Wend
FinishDirectory(dh)
Else
PrintN(#CR$ + LSet("Access denied for '" + Root$ + "'", ConsoleWidth() - 1))
EndIf
EndProcedure
Procedure OpenPureBasicPrefs() ; Open PureBasic.prefs
CompilerSelect #PB_Compiler_OS
CompilerCase #PB_OS_Windows
ProcedureReturn OpenPreferences(GetEnvironmentVariable("APPDATA") + #PS$ + "PureBasic" + #PS$ + "PureBasic.prefs", #PB_Preference_GroupSeparator)
CompilerCase #PB_OS_Linux
ProcedureReturn OpenPreferences("/home/" + UserName() + "/.purebasic/purebasic.prefs", #PB_Preference_GroupSeparator)
CompilerCase #PB_OS_MacOS
ProcedureReturn OpenPreferences("/home/" + UserName() + "/.purebasic/purebasic.prefs", #PB_Preference_GroupSeparator)
CompilerCase #PB_OS_AmigaOS
ProcedureReturn OpenPreferences("ENV:purebasic/purebasic.prefs", #PB_Preference_GroupSeparator)
CompilerEndSelect
EndProcedure
Procedure LoadUpdateCheck() ; Load UpdateCheck.xml
CompilerSelect #PB_Compiler_OS
CompilerCase #PB_OS_Windows
ProcedureReturn LoadXML(1, GetEnvironmentVariable("APPDATA") + #PS$ + "PureBasic" + #PS$ + "UpdateCheck.xml")
CompilerCase #PB_OS_Linux
ProcedureReturn LoadXML(1, "/home/" + UserName() + "/.purebasic/UpdateCheck.xml")
CompilerCase #PB_OS_MacOS
ProcedureReturn LoadXML(1, "/home/" + UserName() + "/.purebasic/UpdateCheck.xml")
CompilerCase #PB_OS_AmigaOS
ProcedureReturn LoadXML(1, "ENV:purebasic/UpdateCheck.xml")
CompilerEndSelect
EndProcedure
Procedure CountCompilersLike(Name$) ; Check if the latest version is installed
Protected *CI.CompilerInfo
Protected result
ForEach CompilerInfo()
*CI = CompilerInfo()
If LCase(StringField(*CI\Name, 1, " (")) = LCase(Name$)
result + 1
EndIf
Next
ProcedureReturn result
EndProcedure
Procedure LoadAndAddCompilersFromIniFile(List result.CompilerInfo()) ; Check for broken links, lost, deleted/uninstalled compilers, add valid compiler file paths to result() list.
Protected i, c, path$, version$, md5$
Shared DeleteCompilers()
;
If OpenPureBasicPrefs()
PreferenceGroup("MoreCompilers")
c = ReadPreferenceLong("Count", 0)
OldCompilerCount = c
; For i = 1 To c Step 1
While i < c
i + 1
path$ = ReadPreferenceString("Compiler" + Str(i) + "_Exe", "")
version$ = ReadPreferenceString("Compiler" + Str(i) + "_Version", "")
md5$ = ReadPreferenceString("Compiler" + Str(i) + "_Md5", "")
If FindMapElement(CompilerInfo(), path$) = 0
If FileSize(path$) > 0
AddElement(result())
result()\ExeMd5 = md5$
result()\Path = path$
result()\Name = version$
AddMapElement(CompilerInfo(), path$)
CompilerInfo() = result()
Else
DeletedCompilers = DeletedCompilers + 1
AddElement(DeleteCompilers())
DeleteCompilers()\ExeMd5 = md5$
DeleteCompilers()\Name = version$
DeleteCompilers()\Path = path$
EndIf
EndIf
Wend
ProcedureReturn 1
EndIf
ProcedureReturn 0
EndProcedure
Procedure Main() ; Load prefs, test listed compilers for broken links, discovery scan for new/unlisted compilers.
Shared DeleteCompilers()
Protected NewList CI.CompilerInfo()
Protected i, c = CountProgramParameters()
; Init things
OpenConsole()
UseMD5Fingerprint()
InitConsoleMetrics()
; Display Title
ConsoleColor(15, 0): ConsoleBold(1) : PrintN("PureBasic Compiler Discovery & Cleanup Tool") : ConsoleBold(0)
ConsoleColor(7, 0): PrintN("Version " + StrF(#APP_VERSION / 100, 2) + " " + FormatDate("(%dd/%mm/%yyyy %hh:%ii)", #PB_Compiler_Date))
PrintN("")
; Load PureBasic.prefs
If OpenPureBasicPrefs() = 0
ConsoleError("ERROR! Could not open PureBasic preferences")
ProcedureReturn
EndIf
; Start making list with known compilers (and remove gone/broken installations)
LoadAndAddCompilersFromIniFile(ci())
If c > 0 ; Scan specified directories (CLI parameters)
For i = 0 To c - 1
ListCompilers(ProgramParameter(i), #True, ci())
Next
Else
; No parameters:
; Scan default installation paths
CompilerSelect #PB_Compiler_OS
CompilerCase #PB_OS_Windows
ListCompilers(GetEnvironmentVariable("ProgramFiles"), #True, ci())
CompilerIf #PB_Compiler_64Bit
ListCompilers(GetEnvironmentVariable("ProgramFiles(x86)"), #True, ci())
CompilerEndIf
CompilerCase #PB_OS_Linux
ListCompilers("/home/" + UserName(), #True, ci())
CompilerCase #PB_OS_MacOS
ListCompilers("/home/" + UserName(), #True, ci())
CompilerCase #PB_OS_AmigaOS
ListCompilers("SYS:", #True, ci())
CompilerDefault
ListCompilers("~/", #True, ci())
CompilerEndSelect
EndIf
; Erase last path line
Print(#CR$ + Space(ConsoleWidth()))
PrintN("")
; Sort results
SortStructuredList(ci(), #PB_Sort_Descending, OffsetOf(CompilerInfo\Name), #PB_String)
; Display PureBasic versions
ConsoleColor(15, 0):ConsoleBold(1):PrintN("PureBasic versions"):ConsoleBold(0)
; Display deleted (gone) compilers
ForEach DeleteCompilers()
ConsoleColor(12, 0)
Print(" Gone")
ConsoleColor(7, 0)
PrintN(": " + DeleteCompilers()\Name)
Next
EnableGraphicalConsole(1)
; Display new & old (known) compilers
ForEach ci()
If ci()\Flags ; new
ConsoleColor(10, 0) : Print(" New") : ConsoleColor(15, 0)
Else ; old
ConsoleColor(14, 0) : Print("Installed") : ConsoleColor(7, 0)
EndIf
; Display version string
PrintN(": " + ci()\Name) : ConsoleColor(7, 0)
Next
PrintN("")
; Display latest releases & betas (using CheckUpdate.xml)
ConsoleColor(15, 0):ConsoleBold(1):PrintN("Latest releases and betas"):ConsoleBold(0)
If LoadUpdateCheck()
; Parse CheckUpdate.xml
Protected main = MainXMLNode(1)
c = XMLChildCount(main)
Protected cc
Protected node, name$, cat$, missing_beta, missing_release
i = 0
While i < c
i + 1
node = ChildXMLNode(main, i)
name$ = GetXMLAttribute(node, "name")
cat$ = GetXMLAttribute(node, "category")
; Check for beta + LTS versions only
If cat$ = "beta" Or (cat$ = "release" And FindString(name$, "LTS"))
cc = CountCompilersLike(name$)
If cc = 0
ConsoleColor(12, 0)
Print("Not installed")
ConsoleColor(7, 0)
PrintN(": " + name$ + " (" + cat$ + ")")
If cat$ = "beta"
missing_beta + 1
Else
missing_release + 1
EndIf
Else
ConsoleColor(10, 0)
Print(" Installed")
ConsoleColor(7, 0)
PrintN(": " + name$ + " (" + cat$ + ")")
EndIf
EndIf
Wend
Else
ConsoleColor(12, 0)
PrintN("Could not load updatecheck.xml")
ConsoleColor(7, 0)
EndIf
; Fill/Erase last line
Print(LSet("", ConsoleWidth() , "-"))
; Display final statistics
PrintN("")
PrintN(" Old compiler count in prefs: " + FormatNumber(OldCompilerCount, 0))
PrintN(" Missing compilers removed: " + FormatNumber(DeletedCompilers, 0))
PrintN(" Compilers discovered: " + FormatNumber(NewCompilerCount, 0))
PrintN(" Final compiler count in prefs: " + FormatNumber(ListSize(ci()), 0))
PrintN("")
Print(" Write preferences file: ")
; Update & store preferences (PureBasic.prefs)
; Remove old Settings
PreferenceGroup("MoreCompilers")
i = 0
c = OldCompilerCount
While i < c
i + 1
RemovePreferenceKey("Compiler" + Str(i) + "_Exe")
RemovePreferenceKey("Compiler" + Str(i) + "_Md5")
RemovePreferenceKey("Compiler" + Str(i) + "_Version")
Wend
; New settings
i = 0
c = ListSize(ci())
PreferenceGroup("MoreCompilers")
WritePreferenceLong("Count", c)
ForEach ci()
i + 1
WritePreferenceString("Compiler" + Str(i) + "_Exe", ci()\Path)
WritePreferenceString("Compiler" + Str(i) + "_Md5", ci()\ExeMd5)
WritePreferenceString("Compiler" + Str(i) + "_Version", ci()\Name)
Next
; Finish
ClosePreferences()
PrintN("done")
; Display time elapsed
PrintN(" Time elapsed: " + FormatDate("%hh:%ii:%ss", ElapsedMilliseconds() / 1000))
PrintN("")
; Exit
ProcedureReturn
EndProcedure
; Call main()
main()
CompilerIf #PB_Compiler_Debugger
; Wait for user to hit return to give time to read console output
PrintN("(Debugger ON) Press ENTER to exit.")
Input()
CompilerEndIf
; THE
End