diff options
| -rw-r--r-- | .envrc | 1 | ||||
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | README.org | 1 | ||||
| -rw-r--r-- | cmd/git-leaderboard/main.go | 128 | ||||
| -rw-r--r-- | cmd/goget/main.go | 165 | ||||
| -rw-r--r-- | cmd/pviz/main.go | 266 | ||||
| -rw-r--r-- | cmd/seq-stat/main.go | 115 | ||||
| -rw-r--r-- | flake.lock | 142 | ||||
| -rw-r--r-- | flake.nix | 135 | ||||
| -rw-r--r-- | go.mod | 20 | ||||
| -rw-r--r-- | go.sum | 38 | ||||
| -rw-r--r-- | nix/modules/goget.nix | 66 | ||||
| -rw-r--r-- | nix/overlay.nix | 5 | ||||
| -rw-r--r-- | nix/packages/default.nix | 5 | ||||
| -rw-r--r-- | nix/packages/goget.nix | 31 | ||||
| -rw-r--r-- | treefmt.nix | 30 |
16 files changed, 1150 insertions, 0 deletions
@@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d69087 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.pre-commit-config.yaml +/result diff --git a/README.org b/README.org new file mode 100644 index 0000000..19764b6 --- /dev/null +++ b/README.org @@ -0,0 +1 @@ +My experimental monorepo for Go tools and packages. diff --git a/cmd/git-leaderboard/main.go b/cmd/git-leaderboard/main.go new file mode 100644 index 0000000..7c846a5 --- /dev/null +++ b/cmd/git-leaderboard/main.go @@ -0,0 +1,128 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "sort" + "strings" +) + +type Author struct { + Name string + Count int +} + +func main() { + lineFlag := flag.Bool("line", false, "Count lines changed instead of commits") + flag.Parse() + + if !isGitRepository() { + fmt.Println("Error: Not in a git repository") + os.Exit(1) + } + + authors, err := getAuthors(*lineFlag) + if err != nil { + fmt.Printf("Error getting authors: %v\n", err) + os.Exit(1) + } + + if len(authors) == 0 { + fmt.Println("No authors found. The repository might be empty or have no commits.") + os.Exit(0) + } + + sort.Slice(authors, func(i, j int) bool { + return authors[i].Count > authors[j].Count + }) + + maxCount := authors[0].Count + + fmt.Printf("%-30s %-10s %s\n", "Author", "Count", "Contribution") + fmt.Println(strings.Repeat("-", 80)) + + for _, author := range authors { + bar := generateBar(author.Count, maxCount) + fmt.Printf("%-30s %-10d %s\n", author.Name, author.Count, bar) + } +} + +func isGitRepository() bool { + cmd := exec.Command("git", "rev-parse", "--is-inside-work-tree") + err := cmd.Run() + return err == nil +} + +func getAuthors(countLines bool) ([]Author, error) { + var cmd *exec.Cmd + + if countLines { + cmd = exec.Command("git", "log", "--pretty=format:%aN", "--numstat") + } else { + cmd = exec.Command("git", "shortlog", "-sn", "--no-merges", "HEAD") + } + + output, err := cmd.Output() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + return nil, fmt.Errorf("git command failed: %s. Error output: %s", err, exitErr.Stderr) + } + return nil, fmt.Errorf("error executing git command: %v", err) + } + + lines := strings.Split(string(output), "\n") + authors := make(map[string]int) + + if countLines { + currentAuthor := "" + for _, line := range lines { + if line == "" { + currentAuthor = "" + continue + } + if currentAuthor == "" { + currentAuthor = line + continue + } else { + fields := strings.Fields(line) + if len(fields) >= 2 { + added, _ := parseInt(fields[0]) + deleted, _ := parseInt(fields[1]) + authors[currentAuthor] += added + deleted + } + } + } + } else { + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) >= 2 { + count, _ := parseInt(fields[0]) + name := strings.Join(fields[1:], " ") + authors[name] = count + } + } + } + + var result []Author + for name, count := range authors { + result = append(result, Author{Name: name, Count: count}) + } + return result, nil +} + +func parseInt(s string) (int, error) { + if s == "-" { + return 0, nil + } + var result int + _, err := fmt.Sscanf(s, "%d", &result) + return result, err +} + +func generateBar(count, maxCount int) string { + maxWidth := 38 + width := int(float64(count) / float64(maxCount) * float64(maxWidth)) + return strings.Repeat("▆", width) +} diff --git a/cmd/goget/main.go b/cmd/goget/main.go new file mode 100644 index 0000000..3f17448 --- /dev/null +++ b/cmd/goget/main.go @@ -0,0 +1,165 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "slices" + "strings" + "syscall" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" + "golang.org/x/mod/semver" +) + +const ( + forgeDomain = "code.fcuny.net" + forgeUser = "fcuny" + goPkgDomain = "go.fcuny.net" +) + +var ( + goGetReqs = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "goget_requests_total", + Help: "go get requests processed, by repository name.", + }, []string{"name"}) + + modules = []string{"x"} +) + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + + metricsMux := http.NewServeMux() + metricsMux.Handle("/metrics", promhttp.Handler()) + metricsServer := &http.Server{ + Addr: ":9091", Handler: metricsMux, + ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, + } + + go func() { + log.Println("Starting metrics server on :9091") + if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Printf("Metrics server error: %v", err) + } + }() + + s := &http.Server{ + Addr: ":8070", + Handler: handler(), + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 1 * time.Minute, + } + + go func() { + log.Println("Starting main server on :8080") + if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Printf("Main server error: %v", err) + } + }() + + <-ctx.Done() + log.Println("Shutdown signal received, starting graceful shutdown...") + + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer shutdownCancel() + + var shutdownErr error + + log.Println("Shutting down main server...") + if err := s.Shutdown(shutdownCtx); err != nil { + log.Printf("Main server shutdown error: %v", err) + shutdownErr = err + } + + log.Println("Shutting down metrics server...") + if err := metricsServer.Shutdown(shutdownCtx); err != nil { + log.Printf("Metrics server shutdown error: %v", err) + if shutdownErr == nil { + shutdownErr = err + } + } + + if shutdownErr != nil { + log.Printf("Shutdown completed with errors") + os.Exit(1) + } + + log.Println("Shutdown completed successfully") +} + +func handler() http.Handler { + mux := http.NewServeMux() + + mux.Handle(goPkgDomain+"/{name}", siteHandler(modules)) + mux.Handle(goPkgDomain+"/{name}/", siteHandler(modules)) + + goGetMux := http.NewServeMux() + for _, name := range modules { + module := goPkgDomain + "/" + name + goGetMux.Handle( + module+"/", + goImportHandler(module, "https://"+forgeDomain+"/"+forgeUser+"/"+name), + ) + } + + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + status := http.StatusMethodNotAllowed + http.Error(rw, http.StatusText(status), status) + return + } + + if r.URL.Query().Get("go-get") == "1" { + goGetMux.ServeHTTP(rw, r) + return + } + mux.ServeHTTP(rw, r) + }) +} + +func goImportHandler(module, repo string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + goGetReqs.WithLabelValues(module).Inc() + w.Header().Set("Content-Type", "text/html; charset=UTF-8") + if _, err := fmt.Fprintf(w, `<head><meta name="go-import" content="%s git %s">`, module, repo); err != nil { + log.Printf("Error writing response: %v", err) + } + }) +} + +func siteHandler(names []string) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + name, version, hasV := strings.Cut(name, "@") + if !hasV { + name, _, _ = strings.Cut(name, ".") + } + if hasV && !semver.IsValid(version) || !slices.Contains(names, name) { + http.NotFound(rw, r) + return + } + u := &url.URL{ + Scheme: "https", + Host: forgeDomain, + Path: forgeUser + r.URL.Path, + } + if !hasV { + path, symbol, hasSymbol := strings.Cut(r.URL.Path, ".") + if hasSymbol { + u.Path = forgeUser + "/" + path + u.Fragment = symbol + } + } + http.Redirect(rw, r, u.String(), http.StatusFound) + }) +} diff --git a/cmd/pviz/main.go b/cmd/pviz/main.go new file mode 100644 index 0000000..89bd78d --- /dev/null +++ b/cmd/pviz/main.go @@ -0,0 +1,266 @@ +package main + +import ( + "fmt" + "os" + "strconv" + "text/tabwriter" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +type Period string + +const ( + PeriodDay Period = "day" + PeriodWeek Period = "week" + PeriodMonth Period = "month" + PeriodQuarter Period = "quarter" + PeriodYear Period = "year" +) + +var periodMinutes = map[Period]float64{ + PeriodDay: 24 * 60, + PeriodWeek: 7 * 24 * 60, + PeriodMonth: 30 * 24 * 60, + PeriodQuarter: 90 * 24 * 60, + PeriodYear: 365 * 24 * 60, +} + +var periodOrder = []Period{PeriodDay, PeriodWeek, PeriodMonth, PeriodQuarter, PeriodYear} + +type TimeUnit struct { + Minutes float64 + Formatted string +} + +type AvailabilityCalculator struct { + availability float64 +} + +func NewAvailabilityCalculator(availabilityPercentage float64) (*AvailabilityCalculator, error) { + if availabilityPercentage < 0 || availabilityPercentage > 100 { + return nil, fmt.Errorf("availability must be between 0 and 100") + } + return &AvailabilityCalculator{ + availability: availabilityPercentage / 100.0, + }, nil +} + +func (ac *AvailabilityCalculator) CalculateDowntime(period Period) TimeUnit { + totalMinutes := periodMinutes[period] + downtimeMinutes := totalMinutes * (1 - ac.availability) + return TimeUnit{ + Minutes: downtimeMinutes, + Formatted: formatDuration(downtimeMinutes), + } +} + +func (ac *AvailabilityCalculator) CalculateAllPeriods() map[Period]TimeUnit { + result := make(map[Period]TimeUnit) + for _, period := range periodOrder { + result[period] = ac.CalculateDowntime(period) + } + return result +} + +func formatDuration(minutes float64) string { + if minutes < 1 { + return fmt.Sprintf("%.1f seconds", minutes*60) + } + + hours := int(minutes / 60) + remainingMinutes := minutes - float64(hours*60) + + if hours == 0 { + return fmt.Sprintf("%.1f minutes", remainingMinutes) + } else if remainingMinutes == 0 { + return fmt.Sprintf("%d hours", hours) + } else { + return fmt.Sprintf("%d hours %.1f minutes", hours, remainingMinutes) + } +} + +func printTable(headers []string, rows [][]string) { + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + + for i, header := range headers { + if i > 0 { + _, _ = fmt.Fprint(w, "\t") + } + _, _ = fmt.Fprint(w, header) + } + _, _ = fmt.Fprintln(w) + + for _, row := range rows { + for i, cell := range row { + if i > 0 { + _, _ = fmt.Fprint(w, "\t") + } + _, _ = fmt.Fprint(w, cell) + } + _, _ = fmt.Fprintln(w) + } + + _ = w.Flush() +} + +func analyzeSingle(availability float64, focusPeriod string) error { + calc, err := NewAvailabilityCalculator(availability) + if err != nil { + return err + } + + var periods []Period + if focusPeriod != "" { + periods = []Period{Period(focusPeriod)} + } else { + periods = periodOrder + } + + headers := []string{"Time Period", "Maximum Downtime"} + var rows [][]string + + caser := cases.Title(language.English) + for _, period := range periods { + downtime := calc.CalculateDowntime(period) + rows = append(rows, []string{ + caser.String(string(period)), + downtime.Formatted, + }) + } + + printTable(headers, rows) + return nil +} + +func compareAvailabilities(availability1, availability2 float64) error { + if availability1 == availability2 { + return fmt.Errorf("cannot compare identical availability values") + } + + calc1, err := NewAvailabilityCalculator(availability1) + if err != nil { + return err + } + + calc2, err := NewAvailabilityCalculator(availability2) + if err != nil { + return err + } + + improvement := ((100 - availability1) - (100 - availability2)) / (100 - availability1) * 100 + fmt.Printf("Downtime reduction: %.1f%%\n\n", improvement) + + headers := []string{ + "Time Period", + fmt.Sprintf("%.2f%%", availability1), + fmt.Sprintf("%.2f%%", availability2), + } + var rows [][]string + + caser := cases.Title(language.English) + for _, period := range periodOrder { + downtime1 := calc1.CalculateDowntime(period) + downtime2 := calc2.CalculateDowntime(period) + rows = append(rows, []string{ + caser.String(string(period)), + downtime1.Formatted, + downtime2.Formatted, + }) + } + + printTable(headers, rows) + return nil +} + +func printUsage() { + fmt.Println("Usage:") + fmt.Println(" pviz analyze <availability> - Analyze a single availability target") + fmt.Println(" pviz analyze <availability> --focus <period> - Focus on a specific time period") + fmt.Println(" pviz compare <availability1> <availability2> - Compare two availability targets") + fmt.Println() + fmt.Println("Examples:") + fmt.Println(" pviz analyze 99.99") + fmt.Println(" pviz analyze 99.99 --focus quarter") + fmt.Println(" pviz compare 99.9 99.95") + fmt.Println() + fmt.Println("Valid periods: day, week, month, quarter, year") +} + +func main() { + if len(os.Args) < 2 { + printUsage() + os.Exit(1) + } + + command := os.Args[1] + + switch command { + case "analyze": + if len(os.Args) < 3 { + fmt.Fprintf(os.Stderr, "Error: analyze command requires availability argument\n") + os.Exit(1) + } + + availability, err := strconv.ParseFloat(os.Args[2], 64) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: invalid availability value: %v\n", err) + os.Exit(1) + } + + var focusPeriod string + if len(os.Args) >= 5 && os.Args[3] == "--focus" { + focusPeriod = os.Args[4] + validPeriod := false + for _, p := range periodOrder { + if string(p) == focusPeriod { + validPeriod = true + break + } + } + if !validPeriod { + fmt.Fprintf( + os.Stderr, + "Error: invalid period '%s'. Valid periods: day, week, month, quarter, year\n", + focusPeriod, + ) + os.Exit(1) + } + } + + if err := analyzeSingle(availability, focusPeriod); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + case "compare": + if len(os.Args) < 4 { + fmt.Fprintf(os.Stderr, "Error: compare command requires two availability arguments\n") + os.Exit(1) + } + + availability1, err := strconv.ParseFloat(os.Args[2], 64) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: invalid first availability value: %v\n", err) + os.Exit(1) + } + + availability2, err := strconv.ParseFloat(os.Args[3], 64) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: invalid second availability value: %v\n", err) + os.Exit(1) + } + + if err := compareAvailabilities(availability1, availability2); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + default: + fmt.Fprintf(os.Stderr, "Error: unknown command '%s'\n", command) + printUsage() + os.Exit(1) + } +} diff --git a/cmd/seq-stat/main.go b/cmd/seq-stat/main.go new file mode 100644 index 0000000..75bb0a3 --- /dev/null +++ b/cmd/seq-stat/main.go @@ -0,0 +1,115 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +var ticks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'} + +func histogram(sequence []float64) []rune { + if len(sequence) == 0 { + return []rune{} + } + + minVal := sequence[0] + maxVal := sequence[0] + + for _, v := range sequence { + if v < minVal { + minVal = v + } + if v > maxVal { + maxVal = v + } + } + + scale := (maxVal - minVal) * 256 / float64(len(ticks)-1) + if scale < 1 { + scale = 1 + } + + result := make([]rune, len(sequence)) + for i, v := range sequence { + index := int(((v - minVal) * 256) / scale) + if index >= len(ticks) { + index = len(ticks) - 1 + } + result[i] = ticks[index] + } + + return result +} + +func parseNumbers(input string) ([]float64, error) { + fields := strings.Fields(input) + numbers := make([]float64, 0, len(fields)) + + for _, field := range fields { + num, err := strconv.ParseFloat(field, 64) + if err != nil { + return nil, fmt.Errorf("invalid number: %s", field) + } + numbers = append(numbers, num) + } + + return numbers, nil +} + +func readFromStdin() ([]float64, error) { + var numbers []float64 + scanner := bufio.NewScanner(os.Stdin) + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + + lineNumbers, err := parseNumbers(line) + if err != nil { + return nil, err + } + numbers = append(numbers, lineNumbers...) + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error reading from stdin: %v", err) + } + + return numbers, nil +} + +func main() { + var numbers []float64 + var err error + + if len(os.Args) > 1 { + numbers = make([]float64, 0, len(os.Args)-1) + for _, arg := range os.Args[1:] { + num, parseErr := strconv.ParseFloat(arg, 64) + if parseErr != nil { + fmt.Fprintf(os.Stderr, "Invalid number: %s\n", arg) + os.Exit(1) + } + numbers = append(numbers, num) + } + } else { + numbers, err = readFromStdin() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + if len(numbers) == 0 { + fmt.Fprintln(os.Stderr, "No numbers provided") + os.Exit(1) + } + } + + h := histogram(numbers) + fmt.Println(string(h)) +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6d1a15e --- /dev/null +++ b/flake.lock @@ -0,0 +1,142 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1747046372, + "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1755274400, + "narHash": "sha256-rTInmnp/xYrfcMZyFMH3kc8oko5zYfxsowaLv1LVobY=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "ad7196ae55c295f53a7d1ec39e4a06d922f3b899", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1755446520, + "narHash": "sha256-I0Ok1OGDwc1jPd8cs2VvAYZsHriUVFGIUqW+7uSsOUM=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "4b04db83821b819bbbe32ed0a025b31e7971f22e", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": "pre-commit-hooks", + "treefmt-nix": "treefmt-nix" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1754847726, + "narHash": "sha256-2vX8QjO5lRsDbNYvN9hVHXLU6oMl+V/PsmIiJREG4rE=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "7d81f6fb2e19bf84f1c65135d1060d829fae2408", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..0833885 --- /dev/null +++ b/flake.nix @@ -0,0 +1,135 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05"; + flake-utils.url = "github:numtide/flake-utils"; + treefmt-nix = { + url = "github:numtide/treefmt-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + pre-commit-hooks = { + url = "github:cachix/git-hooks.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + outputs = + { + self, + nixpkgs, + treefmt-nix, + flake-utils, + pre-commit-hooks, + }: + let + # Import our packages and overlay + overlay = import ./nix/overlay.nix; + + # Import NixOS modules + nixosModules = { + goget = import ./nix/modules/goget.nix; + default = { + imports = [ + ./nix/modules/goget.nix + ]; + }; + }; + in + { + # Export the overlay for others to use + overlays.default = overlay; + + # Export NixOS modules + inherit nixosModules; + nixosModule = nixosModules.default; + } + // flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ overlay ]; + }; + + packages = import ./nix/packages { inherit pkgs; }; + + treefmtEval = treefmt-nix.lib.evalModule pkgs ./treefmt.nix; + + ciPackages = with pkgs; [ + go # Need that obviously + gofumpt # Go formatter + golangci-lint # Local/CI linter + gotestsum # Pretty tester + goperf # Go performance suite + ]; + + devPackages = with pkgs; [ + gopls + gotools + ]; + in + { + packages = packages // { + default = packages.goget; + }; + + formatter = treefmtEval.config.build.wrapper; + + checks = { + # Throws an error if any of the source files are not correctly formatted + # when you run `nix flake check --print-build-logs`. Useful for CI + treefmt = treefmtEval.config.build.check self; + pre-commit-check = pre-commit-hooks.lib.${system}.run { + src = ./.; + hooks = { + format = { + enable = true; + name = "Format with treefmt"; + pass_filenames = true; + entry = "${treefmtEval.config.build.wrapper}/bin/treefmt"; + stages = [ + "pre-commit" + "pre-push" + ]; + }; + golangci-lint = { + enable = true; + package = pkgs.golangci-lint; + pass_filenames = true; + stages = [ "pre-push" ]; + }; + unit-tests-go = { + enable = true; + name = "Run unit tests"; + entry = "gotestsum --format testdox ./..."; + pass_filenames = false; + stages = [ "pre-push" ]; + types = [ "go" ]; + }; + }; + }; + }; + + devShells = { + default = pkgs.mkShell { + buildInputs = + [ ] ++ ciPackages ++ devPackages ++ self.checks.${system}.pre-commit-check.enabledPackages; + + shellHook = '' + ${self.checks.${system}.pre-commit-check.shellHook} + export GOTOOLCHAIN=local + export GOPRIVATE=go.fcuny.net/* + ''; + }; + + ci = pkgs.mkShell { + buildInputs = [ ] ++ ciPackages; + CI = true; + shellHook = '' + export GOTOOLCHAIN=local + export GOPRIVATE=go.fcuny.net/* + echo "Entering CI shell. Only essential CI tools available." + ''; + }; + }; + } + ); +} @@ -0,0 +1,20 @@ +module go.fcuny.net/x + +go 1.24.5 + +require ( + github.com/prometheus/client_golang v1.23.0 + golang.org/x/mod v0.27.0 + golang.org/x/text v0.28.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.65.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + golang.org/x/sys v0.33.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect +) @@ -0,0 +1,38 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= +github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/nix/modules/goget.nix b/nix/modules/goget.nix new file mode 100644 index 0000000..3ed5e04 --- /dev/null +++ b/nix/modules/goget.nix @@ -0,0 +1,66 @@ +{ + config, + lib, + pkgs, + ... +}: + +with lib; + +let + cfg = config.services.goget; +in +{ + options.services.goget = { + enable = mkEnableOption "goget service"; + + package = mkPackageOption pkgs "goget" { }; + + port = mkOption { + type = types.port; + default = 8070; + description = "Port to listen on"; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Whether to open the firewall for the goget service"; + }; + }; + + config = mkIf cfg.enable { + systemd.services.goget = { + description = "goget service"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + wants = [ "network.target" ]; + + serviceConfig = { + Type = "exec"; + DynamicUser = true; + ExecStart = "${cfg.package}/bin/goget"; + Restart = "always"; + RestartSec = "5"; + + # Security settings + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictSUIDSGID = true; + RestrictRealtime = true; + RestrictNamespaces = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + }; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.port ]; + }; + }; +} diff --git a/nix/overlay.nix b/nix/overlay.nix new file mode 100644 index 0000000..6789627 --- /dev/null +++ b/nix/overlay.nix @@ -0,0 +1,5 @@ +final: prev: +let + packages = import ./packages { pkgs = final; }; +in +packages diff --git a/nix/packages/default.nix b/nix/packages/default.nix new file mode 100644 index 0000000..e598fa1 --- /dev/null +++ b/nix/packages/default.nix @@ -0,0 +1,5 @@ +{ pkgs }: + +{ + goget = pkgs.callPackage ./goget.nix { }; +} diff --git a/nix/packages/goget.nix b/nix/packages/goget.nix new file mode 100644 index 0000000..c767740 --- /dev/null +++ b/nix/packages/goget.nix @@ -0,0 +1,31 @@ +{ + lib, + buildGoModule, +}: + +buildGoModule rec { + pname = "goget"; + version = "0.1.0"; # Consider deriving from git tags: version = builtins.substring 0 8 self.rev; + + src = ../..; + + vendorHash = "sha256-pStRgjhjjZdsYSnYMcWNbHSF7CJ3+7ZQradZgBfi5Gw="; + + subPackages = [ "cmd/goget" ]; + + ldflags = [ + "-s" + "-w" + ]; + + doCheck = false; + + meta = with lib; { + description = "A Go tool for getting things"; # Update with actual description + homepage = "https://github.com/yourusername/yourrepo"; # Update with your repo + license = licenses.mit; + maintainers = with maintainers; [ fcuny ]; + platforms = platforms.unix; + mainProgram = "goget"; + }; +} diff --git a/treefmt.nix b/treefmt.nix new file mode 100644 index 0000000..6d19624 --- /dev/null +++ b/treefmt.nix @@ -0,0 +1,30 @@ +{ pkgs, ... }: +{ + # See https://github.com/numtide/treefmt-nix#supported-programs + projectRootFile = ".git/config"; + settings.global.includes = [ + "*.go" + "*.yaml" + "*.yml" + "*.md" + "*.nix" + ]; + settings.global.fail-on-change = true; + settings.global.no-cache = true; + programs.gofumpt = { + enable = true; + package = pkgs.gofumpt; + }; + programs.goimports.enable = true; + programs.golines.enable = true; + # GitHub Actions + programs.yamlfmt.enable = true; + programs.actionlint.enable = true; + # Markdown + programs.mdformat.enable = true; + # Nix + programs.nixfmt = { + enable = true; + package = pkgs.nixfmt-rfc-style; + }; +} |
