package main import ( "crypto/sha1" "flag" "fmt" "io" "log" "os" "runtime" "strings" "time" ) // PackageAPPINDEX updates the local APPINDEX file by removing any old // entry matching (P, R, A, o, p) and appending a new entry. // // It includes: // - Compressed/uncompressed file measurement (size + checksums). // - A user-defined remotePath for the final download URL. // - Additional flags for icon, screenshots, tags, and notes. func PackageAPPINDEX( name string, // P: Package name, e.g., "spitfire-browser" release string, // R: Release type, e.g., "nightly" version string, // V: Version, e.g., "2025.02.07" arch string, // A: Architecture, e.g., "amd64" description string, // X: Short description, e.g., "Short summary" url string, // U: Project URL, e.g., "https://spitfirebrowser.xyz/" license string, // L: License, e.g., "AGPL-3.0" origin string, // o: Origin, e.g., "browser" maintainer string, // m: Maintainer, e.g., "Internet Addict" dependencies string, // D: Dependencies, e.g., "default-theme, browser" platform string, // p: Platform, e.g., "linux" remotePath string, // d: Remote file path, e.g., "browser/amd64/nightly/2025.02.07/browser-amd64-nightly-windows.tar.gz" icon string, // I: Icon URL, e.g., "https://weforge.xyz/Spitfire/Branding/raw/branch/main/active/browser/icon.svg" screenshots string, // S: Screenshots URL, e.g., "https://spitfirebrowser.xyz/static/images/screenshots/1.png" tags string, // T: Tags, e.g., "browser,experimental,testing" notes string, // r: Notes, e.g., "Automated build of Spitfire" compressedFile string, // Path to the compressed file, used for size & checksum uncompressedFile string, // Path to the uncompressed file, used for size & checksum ) error { //---------------------------------------------------------------------- // 1) Measure compressed file //---------------------------------------------------------------------- compSizeBytes := measureFileSize(compressedFile) compSize := fmt.Sprintf("%d", compSizeBytes) compChecksum := calcFileChecksum(compressedFile) //---------------------------------------------------------------------- // 2) Measure uncompressed file //---------------------------------------------------------------------- uncompSizeBytes := measureFileSize(uncompressedFile) uncompSize := fmt.Sprintf("%d", uncompSizeBytes) uncompChecksum := calcFileChecksum(uncompressedFile) //---------------------------------------------------------------------- // 3) Remove old entry from APPINDEX (based on P,R,A,o,p). //---------------------------------------------------------------------- removeExistingEntry(name, release, arch, origin, platform) //---------------------------------------------------------------------- // 4) Check for existence of APPINDEX //---------------------------------------------------------------------- appIndexPath := "./APPINDEX" if _, err := os.Stat(appIndexPath); os.IsNotExist(err) { fmt.Println("APPINDEX does not exist. Creating a new one...") } //---------------------------------------------------------------------- // 5) Open (or create) the APPINDEX file for appending. //---------------------------------------------------------------------- file, err := os.OpenFile(appIndexPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return fmt.Errorf("failed to open APPINDEX file: %v", err) } defer file.Close() //---------------------------------------------------------------------- // 6) Build the final download URL from `remotePath` //---------------------------------------------------------------------- downloadURL := fmt.Sprintf("https://downloads.sourceforge.net/project/spitfire-browser/%s", remotePath) //---------------------------------------------------------------------- // 7) Current Unix timestamp for t: //---------------------------------------------------------------------- timestamp := time.Now().Unix() //---------------------------------------------------------------------- // 8) Format final entry // // We'll store: // C:%s => compressed checksum // S:%s => compressed size // I:%s => uncompressed size // c:%s => uncompressed checksum // X:%s => short text/description (custom field, formerly "T:") //---------------------------------------------------------------------- entry := fmt.Sprintf(` C:%s P:%s R:%s V:%s A:%s S:%s I:%s X:%s U:%s L:%s o:%s m:%s t:%d D:%s p:%s q: d:%s I:%s S:%s T:%s r:%s c:%s `, compChecksum, // C: compressed checksum name, // P: release, // R: version, // V: arch, // A: compSize, // S: compressed size uncompSize, // I: uncompressed size description, // X: short text description url, // U: license, // L: origin, // o: maintainer, // m: timestamp, // t: dependencies, // D: platform, // p: downloadURL, // d: icon, // I: icon URL screenshots, // S: screenshots URL tags, // T: tags notes, // r: notes uncompChecksum, // c: uncompressed checksum ) // Trim leading newline for neatness entry = strings.TrimPrefix(entry, "\n") //---------------------------------------------------------------------- // 9) Write the new entry to APPINDEX //---------------------------------------------------------------------- if _, err := file.WriteString(entry + "\n"); err != nil { return fmt.Errorf("failed to write to APPINDEX: %v", err) } fmt.Println("APPINDEX has been updated successfully.") return nil } // calcFileChecksum calculates SHA-1 of an entire file. Returns "" if file not found or error. func calcFileChecksum(path string) string { f, err := os.Open(path) if err != nil { return "" } defer f.Close() h := sha1.New() if _, err := io.Copy(h, f); err != nil { return "" } return fmt.Sprintf("%x", h.Sum(nil)) } // measureFileSize returns file size in bytes. Returns 0 if file not found or error. func measureFileSize(path string) int64 { info, err := os.Stat(path) if err != nil { return 0 } return info.Size() } // removeExistingEntry removes an existing entry from APPINDEX if it matches // P (name) = "P:" // R (release) = "R:" // A (arch) = "A:" // o (origin) = "o:" // p (platform) = "p:" func removeExistingEntry(name, release, arch, origin, platform string) { content, err := os.ReadFile("./APPINDEX") if err != nil { if os.IsNotExist(err) { // No file to remove from return } log.Fatalf("Failed to read APPINDEX: %v", err) } lines := strings.Split(string(content), "\n") var newLines []string var currentEntry []string inEntry := false // We'll track P:, R:, A:, o:, p: fields for each entry var pVal, rVal, aVal, oVal, plVal string for _, line := range lines { trimmed := strings.TrimSpace(line) // Each entry *starts* with "C:..." if strings.HasPrefix(trimmed, "C:") { // We've hit the start of a new entry. Decide whether // we keep the old one we were collecting. if inEntry && len(currentEntry) > 0 { if !(pVal == name && rVal == release && aVal == arch && oVal == origin && plVal == platform) { newLines = append(newLines, currentEntry...) newLines = append(newLines, "") // optional blank line } } // Start a new entry currentEntry = []string{trimmed} inEntry = true pVal, rVal, aVal, oVal, plVal = "", "", "", "", "" continue } if inEntry { currentEntry = append(currentEntry, trimmed) // Check if it's one of the fields we track switch { case strings.HasPrefix(trimmed, "P:"): pVal = strings.TrimPrefix(trimmed, "P:") case strings.HasPrefix(trimmed, "R:"): rVal = strings.TrimPrefix(trimmed, "R:") case strings.HasPrefix(trimmed, "A:"): aVal = strings.TrimPrefix(trimmed, "A:") case strings.HasPrefix(trimmed, "o:"): oVal = strings.TrimPrefix(trimmed, "o:") case strings.HasPrefix(trimmed, "p:"): plVal = strings.TrimPrefix(trimmed, "p:") } } else if trimmed != "" { // Lines outside an entry newLines = append(newLines, trimmed) } } // After the loop, handle any last entry we collected if inEntry && len(currentEntry) > 0 { if !(pVal == name && rVal == release && aVal == arch && oVal == origin && plVal == platform) { newLines = append(newLines, currentEntry...) } } finalContent := strings.Join(newLines, "\n") if !strings.HasSuffix(finalContent, "\n") { finalContent += "\n" } if err := os.WriteFile("./APPINDEX", []byte(finalContent), 0644); err != nil { log.Fatalf("Failed to update APPINDEX: %v", err) } } func main() { //---------------------------------------------------------------------- // Default flags for basic fields //---------------------------------------------------------------------- defaultRelease := "nightly" defaultVersion := time.Now().Format("2006.01.02") // e.g. "2025.02.07" defaultArch := runtime.GOARCH // e.g. "amd64" //---------------------------------------------------------------------- // Primary fields: P, R, V, A, X (desc), U, L, o, m, D, p //---------------------------------------------------------------------- name := flag.String("name", "spitfire-browser", "P: Package name") release := flag.String("release", defaultRelease, "R: Release (nightly, stable, etc.)") version := flag.String("version", defaultVersion, "V: Version (date or semantic)") arch := flag.String("arch", defaultArch, "A: Architecture (default=GOARCH)") description := flag.String("description", "Spitfire build", "X: Short text description") url := flag.String("url", "https://spitfirebrowser.xyz/", "U: Project URL") license := flag.String("license", "AGPL-3.0", "L: License") origin := flag.String("origin", "browser", "o: Origin name") maintainer := flag.String("maintainer", "Internet Addict", "m: Maintainer name") dependencies := flag.String("dependencies", "", "D: Dependencies (comma-separated)") platform := flag.String("platform", "windows", "p: Platform (linux, windows, etc.)") //---------------------------------------------------------------------- // Additional flags for icon, screenshots, tags, notes //---------------------------------------------------------------------- icon := flag.String("icon", "https://weforge.xyz/Spitfire/Branding/raw/branch/main/active/browser/icon.svg", "I: Icon URL") screenshots := flag.String("screenshots", "https://spitfirebrowser.xyz/static/images/screenshots/1.png", "S: Screenshot(s) URL(s)") tags := flag.String("tags", "browser,experimental,testing", "T: Comma-separated tags") notes := flag.String("notes", "Automated build of Spitfire", "r: Additional notes") //---------------------------------------------------------------------- // remotePath is the path under: // https://downloads.sourceforge.net/project/spitfire-browser/ // Example: "browser/amd64/nightly/2025.02.07/browser-amd64-nightly-windows.tar.gz" //---------------------------------------------------------------------- remotePath := flag.String("remotePath", "", "Path under https://downloads.sourceforge.net/project/spitfire-browser/...") //---------------------------------------------------------------------- // Compressed & uncompressed artifact paths //---------------------------------------------------------------------- compressedFile := flag.String("compressedFile", "browser-amd64-nightly-windows.tar.gz", "Local path to compressed artifact (for size/checksum)") uncompressedFile := flag.String("uncompressedFile", "browser-amd64-nightly-windows", "Local path to uncompressed artifact (for size/checksum)") flag.Parse() //---------------------------------------------------------------------- // If user didn't specify remotePath, we build a naive default: // "browser////---.tar.gz" //---------------------------------------------------------------------- if *remotePath == "" { *remotePath = fmt.Sprintf( "browser/%s/%s/%s/%s-%s-%s-%s.tar.gz", *arch, *release, *version, *origin, *arch, *release, *platform, ) } //---------------------------------------------------------------------- // Execute the logic //---------------------------------------------------------------------- err := PackageAPPINDEX( *name, *release, *version, *arch, *description, *url, *license, *origin, *maintainer, *dependencies, *platform, *remotePath, *icon, *screenshots, *tags, *notes, *compressedFile, *uncompressedFile, ) if err != nil { log.Fatalf("APPINDEX update failed: %v", err) } }