Compiler discovery & cleanup tool

Everything else that doesn't fall into one of the other PB categories.
benubi
Enthusiast
Enthusiast
Posts: 220
Joined: Tue Mar 29, 2005 4:01 pm

Compiler discovery & cleanup tool

Post by benubi »

Hello

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
Image

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