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)
|
|
|
|
}
|
|
|
|
}
|