Publisher/update_appindex.go

366 lines
12 KiB
Go
Raw Normal View History

2025-02-12 12:18:16 +00:00
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...)
}
}
// 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...)
}
}
2025-02-26 19:22:24 +00:00
// Join and collapse multiple newlines into one
2025-02-12 12:18:16 +00:00
finalContent := strings.Join(newLines, "\n")
2025-02-26 19:22:24 +00:00
re := regexp.MustCompile(`\n+`)
finalContent = re.ReplaceAllString(finalContent, "\n")
2025-02-12 12:18:16 +00:00
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/<remotePath>
// 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/<arch>/<release>/<version>/<origin>-<arch>-<release>-<platform>.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)
}
}