forked from security/hakurei
Compare commits
32 Commits
pkgserver-
...
pkgserver
| Author | SHA1 | Date | |
|---|---|---|---|
|
7011f8a580
|
|||
|
dac33d7720
|
|||
|
50649fdbf4
|
|||
|
91aa21d92d
|
|||
|
a1b515074e
|
|||
|
e130443cf4
|
|||
|
112c32fee2
|
|||
|
6d925b3d43
|
|||
|
2ec49a525f
|
|||
|
ce914abb57
|
|||
|
b03ad185de
|
|||
|
534cac83fb
|
|||
|
887edcbe48
|
|||
|
fa9bc70b39
|
|||
|
4a63fbbc2a
|
|||
| b104ad6e2d | |||
| 469bd1ee99 | |||
| 52a4e5b87d | |||
|
35d76c5d2b
|
|||
|
dfd3301a33
|
|||
|
a4ce41ea9a
|
|||
|
773e43a215
|
|||
|
f150e1fdd6
|
|||
|
dec7010c35
|
|||
|
69bd88282c
|
|||
|
ca2053d3ba
|
|||
|
8d0aa1127c
|
|||
|
48cdf8bf85
|
|||
|
7fb42ba49d
|
|||
|
19a2737148
|
|||
|
baf2def9cc
|
|||
|
242e042cb9
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,6 +27,7 @@ go.work.sum
|
||||
|
||||
# go generate
|
||||
/cmd/hakurei/LICENSE
|
||||
/cmd/pkgserver/.sass-cache
|
||||
/internal/pkg/testdata/testtool
|
||||
/internal/rosa/hakurei_current.tar.gz
|
||||
|
||||
|
||||
144
cmd/pkgserver/api.go
Normal file
144
cmd/pkgserver/api.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/internal/rosa"
|
||||
)
|
||||
|
||||
// for lazy initialisation of serveInfo
|
||||
var (
|
||||
infoPayload struct {
|
||||
// Current package count.
|
||||
Count int `json:"count"`
|
||||
// Hakurei version, set at link time.
|
||||
HakureiVersion string `json:"hakurei_version"`
|
||||
}
|
||||
infoPayloadOnce sync.Once
|
||||
)
|
||||
|
||||
// handleInfo writes constant system information.
|
||||
func handleInfo(w http.ResponseWriter, _ *http.Request) {
|
||||
infoPayloadOnce.Do(func() {
|
||||
infoPayload.Count = int(rosa.PresetUnexportedStart)
|
||||
infoPayload.HakureiVersion = info.Version()
|
||||
})
|
||||
// TODO(mae): cache entire response if no additional fields are planned
|
||||
writeAPIPayload(w, infoPayload)
|
||||
}
|
||||
|
||||
func (index *packageIndex) newStatusHandler(disposition bool) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
name := path.Base(r.URL.Path)
|
||||
p, ok := rosa.ResolveName(name)
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
m := rosa.GetMetadata(p)
|
||||
pk, ok := index.names[m.Name]
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if len(pk.status) > 0 {
|
||||
if disposition {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
}
|
||||
if disposition {
|
||||
var version string
|
||||
if pk.Version != "\u0000" {
|
||||
version = pk.Version
|
||||
} else {
|
||||
version = "unknown"
|
||||
}
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-%s-%s.log\"", pk.Name, version, pkg.Encode(pk.ident.Value())))
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
_, err := io.Copy(w, bytes.NewReader(pk.status))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleGet writes a slice of metadata with specified order.
|
||||
func (index *packageIndex) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
limit, err := strconv.Atoi(q.Get("limit"))
|
||||
if err != nil || limit > 100 || limit < 1 {
|
||||
http.Error(
|
||||
w, "limit must be an integer between 1 and 100",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
i, err := strconv.Atoi(q.Get("index"))
|
||||
if err != nil || i >= len(index.sorts[0]) || i < 0 {
|
||||
http.Error(
|
||||
w, "index must be an integer between 0 and "+
|
||||
strconv.Itoa(int(rosa.PresetUnexportedStart-1)),
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
sort, err := strconv.Atoi(q.Get("sort"))
|
||||
if err != nil || sort >= len(index.sorts) || sort < 0 {
|
||||
http.Error(
|
||||
w, "sort must be an integer between 0 and "+
|
||||
strconv.Itoa(sortOrderEnd),
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
values := index.sorts[sort][i:min(i+limit, len(index.sorts[sort]))]
|
||||
// TODO(mae): remove count field
|
||||
writeAPIPayload(w, &struct {
|
||||
Count int `json:"count"`
|
||||
Values []*metadata `json:"values"`
|
||||
}{len(values), values})
|
||||
}
|
||||
|
||||
// apiVersion is the name of the current API revision, as part of the pattern.
|
||||
const apiVersion = "v1"
|
||||
|
||||
// registerAPI registers API handler functions.
|
||||
func (index *packageIndex) registerAPI(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /api/"+apiVersion+"/info", handleInfo)
|
||||
mux.HandleFunc("GET /api/"+apiVersion+"/get", index.handleGet)
|
||||
mux.HandleFunc("GET /api/"+apiVersion+"/status/", index.newStatusHandler(false))
|
||||
mux.HandleFunc("GET /status/", index.newStatusHandler(true))
|
||||
}
|
||||
|
||||
// writeAPIPayload sets headers common to API responses and encodes payload as
|
||||
// JSON for the response body.
|
||||
func writeAPIPayload(w http.ResponseWriter, payload any) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
log.Println(err)
|
||||
http.Error(
|
||||
w, "cannot encode payload, contact maintainers",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
}
|
||||
183
cmd/pkgserver/api_test.go
Normal file
183
cmd/pkgserver/api_test.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/rosa"
|
||||
)
|
||||
|
||||
// prefix is prepended to every API path.
|
||||
const prefix = "/api/" + apiVersion + "/"
|
||||
|
||||
func TestAPIInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
handleInfo(w, httptest.NewRequestWithContext(
|
||||
t.Context(),
|
||||
http.MethodGet,
|
||||
prefix+"info",
|
||||
nil,
|
||||
))
|
||||
|
||||
resp := w.Result()
|
||||
checkStatus(t, resp, http.StatusOK)
|
||||
checkAPIHeader(t, w.Header())
|
||||
|
||||
checkPayload(t, resp, struct {
|
||||
Count int `json:"count"`
|
||||
HakureiVersion string `json:"hakurei_version"`
|
||||
}{int(rosa.PresetUnexportedStart), info.Version()})
|
||||
}
|
||||
|
||||
func TestAPIGet(t *testing.T) {
|
||||
t.Parallel()
|
||||
const target = prefix + "get"
|
||||
|
||||
index := newIndex(t)
|
||||
newRequest := func(suffix string) *httptest.ResponseRecorder {
|
||||
w := httptest.NewRecorder()
|
||||
index.handleGet(w, httptest.NewRequestWithContext(
|
||||
t.Context(),
|
||||
http.MethodGet,
|
||||
target+suffix,
|
||||
nil,
|
||||
))
|
||||
return w
|
||||
}
|
||||
|
||||
checkValidate := func(t *testing.T, suffix string, vmin, vmax int, wantErr string) {
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := newRequest("?" + suffix + "=invalid")
|
||||
resp := w.Result()
|
||||
checkError(t, resp, wantErr, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("min", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := newRequest("?" + suffix + "=" + strconv.Itoa(vmin-1))
|
||||
resp := w.Result()
|
||||
checkError(t, resp, wantErr, http.StatusBadRequest)
|
||||
|
||||
w = newRequest("?" + suffix + "=" + strconv.Itoa(vmin))
|
||||
resp = w.Result()
|
||||
checkStatus(t, resp, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("max", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := newRequest("?" + suffix + "=" + strconv.Itoa(vmax+1))
|
||||
resp := w.Result()
|
||||
checkError(t, resp, wantErr, http.StatusBadRequest)
|
||||
|
||||
w = newRequest("?" + suffix + "=" + strconv.Itoa(vmax))
|
||||
resp = w.Result()
|
||||
checkStatus(t, resp, http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("limit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
checkValidate(
|
||||
t, "index=0&sort=0&limit", 1, 100,
|
||||
"limit must be an integer between 1 and 100",
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("index", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
checkValidate(
|
||||
t, "limit=1&sort=0&index", 0, int(rosa.PresetUnexportedStart-1),
|
||||
"index must be an integer between 0 and "+strconv.Itoa(int(rosa.PresetUnexportedStart-1)),
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("sort", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
checkValidate(
|
||||
t, "index=0&limit=1&sort", 0, int(sortOrderEnd),
|
||||
"sort must be an integer between 0 and "+strconv.Itoa(int(sortOrderEnd)),
|
||||
)
|
||||
})
|
||||
|
||||
checkWithSuffix := func(name, suffix string, want []*metadata) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := newRequest(suffix)
|
||||
resp := w.Result()
|
||||
checkStatus(t, resp, http.StatusOK)
|
||||
checkAPIHeader(t, w.Header())
|
||||
checkPayloadFunc(t, resp, func(got *struct {
|
||||
Count int `json:"count"`
|
||||
Values []*metadata `json:"values"`
|
||||
}) bool {
|
||||
return got.Count == len(want) &&
|
||||
slices.EqualFunc(got.Values, want, func(a, b *metadata) bool {
|
||||
return (a.Version == b.Version ||
|
||||
a.Version == rosa.Unversioned ||
|
||||
b.Version == rosa.Unversioned) &&
|
||||
a.HasReport == b.HasReport &&
|
||||
a.Name == b.Name &&
|
||||
a.Description == b.Description &&
|
||||
a.Website == b.Website
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
checkWithSuffix("declarationAscending", "?limit=2&index=0&sort=0", []*metadata{
|
||||
{
|
||||
Metadata: rosa.GetMetadata(0),
|
||||
Version: rosa.Std.Version(0),
|
||||
},
|
||||
{
|
||||
Metadata: rosa.GetMetadata(1),
|
||||
Version: rosa.Std.Version(1),
|
||||
},
|
||||
})
|
||||
checkWithSuffix("declarationAscending offset", "?limit=3&index=5&sort=0", []*metadata{
|
||||
{
|
||||
Metadata: rosa.GetMetadata(5),
|
||||
Version: rosa.Std.Version(5),
|
||||
},
|
||||
{
|
||||
Metadata: rosa.GetMetadata(6),
|
||||
Version: rosa.Std.Version(6),
|
||||
},
|
||||
{
|
||||
Metadata: rosa.GetMetadata(7),
|
||||
Version: rosa.Std.Version(7),
|
||||
},
|
||||
})
|
||||
checkWithSuffix("declarationDescending", "?limit=3&index=0&sort=1", []*metadata{
|
||||
{
|
||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 1),
|
||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 1),
|
||||
},
|
||||
{
|
||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 2),
|
||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 2),
|
||||
},
|
||||
{
|
||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 3),
|
||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 3),
|
||||
},
|
||||
})
|
||||
checkWithSuffix("declarationDescending offset", "?limit=1&index=37&sort=1", []*metadata{
|
||||
{
|
||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 38),
|
||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 38),
|
||||
},
|
||||
})
|
||||
}
|
||||
92
cmd/pkgserver/index.go
Normal file
92
cmd/pkgserver/index.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
"unique"
|
||||
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/internal/rosa"
|
||||
)
|
||||
|
||||
const (
|
||||
declarationAscending = iota
|
||||
declarationDescending
|
||||
nameAscending
|
||||
nameDescending
|
||||
|
||||
sortOrderEnd = iota - 1
|
||||
)
|
||||
|
||||
// packageIndex refers to metadata by name and various sort orders.
|
||||
type packageIndex struct {
|
||||
sorts [sortOrderEnd + 1][rosa.PresetUnexportedStart]*metadata
|
||||
names map[string]*metadata
|
||||
}
|
||||
|
||||
// metadata holds [rosa.Metadata] extended with additional information.
|
||||
type metadata struct {
|
||||
p rosa.PArtifact
|
||||
*rosa.Metadata
|
||||
|
||||
// Populated via [rosa.Toolchain.Version], [rosa.Unversioned] is equivalent
|
||||
// to the zero value. Otherwise, the zero value is invalid.
|
||||
Version string `json:"version,omitempty"`
|
||||
// Whether the underlying [pkg.Artifact] is present in the report.
|
||||
HasReport bool `json:"report"`
|
||||
|
||||
// Ident resolved from underlying [pkg.Artifact].
|
||||
ident unique.Handle[pkg.ID]
|
||||
// Backed by [rosa.Report], access must be prepared by HandleAccess.
|
||||
status []byte
|
||||
}
|
||||
|
||||
// populate deterministically populates packageIndex, optionally with a report.
|
||||
func (index *packageIndex) populate(cache *pkg.Cache, report *rosa.Report) (err error) {
|
||||
if report != nil {
|
||||
defer report.HandleAccess(&err)()
|
||||
}
|
||||
|
||||
var work [rosa.PresetUnexportedStart]*metadata
|
||||
index.names = make(map[string]*metadata)
|
||||
for p := range rosa.PresetUnexportedStart {
|
||||
m := metadata{
|
||||
p: p,
|
||||
|
||||
Metadata: rosa.GetMetadata(p),
|
||||
Version: rosa.Std.Version(p),
|
||||
}
|
||||
if m.Version == "" {
|
||||
return errors.New("invalid version from " + m.Name)
|
||||
}
|
||||
if m.Version == rosa.Unversioned {
|
||||
m.Version = ""
|
||||
}
|
||||
|
||||
if cache != nil && report != nil {
|
||||
m.ident = cache.Ident(rosa.Std.Load(p))
|
||||
status, n := report.ArtifactOf(m.ident)
|
||||
if n >= 0 {
|
||||
m.HasReport = true
|
||||
m.status = status
|
||||
}
|
||||
}
|
||||
|
||||
work[p] = &m
|
||||
index.names[m.Name] = &m
|
||||
}
|
||||
|
||||
index.sorts[declarationAscending] = work
|
||||
index.sorts[declarationDescending] = work
|
||||
slices.Reverse(index.sorts[declarationDescending][:])
|
||||
|
||||
index.sorts[nameAscending] = work
|
||||
slices.SortFunc(index.sorts[nameAscending][:], func(a, b *metadata) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
index.sorts[nameDescending] = index.sorts[nameAscending]
|
||||
slices.Reverse(index.sorts[nameDescending][:])
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,20 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/container/check"
|
||||
@@ -23,157 +17,15 @@ import (
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
//go:generate sh -c "sass ui/static/dark.scss ui/static/dark.css && sass ui/static/light.scss ui/static/light.css && tsc ui/static/index.ts"
|
||||
//go:embed ui/*
|
||||
var content embed.FS
|
||||
const shutdownTimeout = 15 * time.Second
|
||||
|
||||
func serveWebUI(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Printf("serveWebUI: %s\n", r.URL.Path)
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("X-XSS-Protection", "1")
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
|
||||
http.ServeFileFS(w, r, content, "ui/index.html")
|
||||
}
|
||||
func serveStaticContent(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Printf("serveStaticContent: %s\n", r.URL.Path)
|
||||
switch r.URL.Path {
|
||||
case "/static/style.css":
|
||||
darkTheme := r.CookiesNamed("dark_theme")
|
||||
if len(darkTheme) > 0 && darkTheme[0].Value == "true" {
|
||||
http.ServeFileFS(w, r, content, "ui/static/dark.css")
|
||||
} else {
|
||||
http.ServeFileFS(w, r, content, "ui/static/light.css")
|
||||
}
|
||||
break
|
||||
case "/favicon.ico":
|
||||
http.ServeFileFS(w, r, content, "ui/static/favicon.ico")
|
||||
break
|
||||
case "/static/index.js":
|
||||
http.ServeFileFS(w, r, content, "ui/static/index.js")
|
||||
break
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
|
||||
}
|
||||
}
|
||||
func serveAPI(index *PackageIndex) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {}
|
||||
}
|
||||
|
||||
func serveStatus(index *PackageIndex) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if index == nil {
|
||||
http.Error(w, "index is nil", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
base := path.Base(r.URL.Path)
|
||||
name := strings.TrimSuffix(base, ".log")
|
||||
p, ok := rosa.ResolveName(name)
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
m := rosa.GetMetadata(p)
|
||||
pk, ok := index.names[m.Name]
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if len(pk.status) > 0 {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err := io.Copy(w, bytes.NewReader(pk.status))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type SortOrders int
|
||||
|
||||
const (
|
||||
DeclarationAscending SortOrders = iota
|
||||
DeclarationDescending
|
||||
NameAscending
|
||||
NameDescending
|
||||
limitSortOrders
|
||||
)
|
||||
|
||||
type PackageIndex struct {
|
||||
sorts [limitSortOrders][rosa.PresetUnexportedStart]*PackageIndexEntry
|
||||
names map[string]*PackageIndexEntry
|
||||
}
|
||||
|
||||
type PackageIndexEntry struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Website string `json:"website"`
|
||||
Version string `json:"version"`
|
||||
status []byte
|
||||
}
|
||||
|
||||
func createPackageIndex(cache *pkg.Cache, report *rosa.Report) (_ *PackageIndex, err error) {
|
||||
index := new(PackageIndex)
|
||||
index.names = make(map[string]*PackageIndexEntry, rosa.PresetUnexportedStart)
|
||||
work := make([]PackageIndexEntry, rosa.PresetUnexportedStart)
|
||||
defer report.HandleAccess(&err)()
|
||||
for p := range rosa.PresetUnexportedStart {
|
||||
m := rosa.GetMetadata(p)
|
||||
v := rosa.Std.Version(p)
|
||||
a := rosa.Std.Load(p)
|
||||
id := cache.Ident(a)
|
||||
st, n := report.ArtifactOf(id)
|
||||
var status []byte
|
||||
if n < 1 {
|
||||
status = nil
|
||||
} else {
|
||||
status = st
|
||||
}
|
||||
log.Printf("Processing package %s...\n", m.Name)
|
||||
entry := PackageIndexEntry{
|
||||
Name: m.Name,
|
||||
Description: m.Description,
|
||||
Website: m.Website,
|
||||
Version: v,
|
||||
status: status,
|
||||
}
|
||||
work[p] = entry
|
||||
index.names[m.Name] = &entry
|
||||
}
|
||||
for i, p := range work {
|
||||
index.sorts[DeclarationAscending][i] = &p
|
||||
}
|
||||
slices.Reverse(work)
|
||||
for i, p := range work {
|
||||
index.sorts[DeclarationDescending][i] = &p
|
||||
}
|
||||
slices.SortFunc(work, func(a PackageIndexEntry, b PackageIndexEntry) int {
|
||||
return cmp.Compare(a.Name, b.Name)
|
||||
})
|
||||
for i, p := range work {
|
||||
index.sorts[NameAscending][i] = &p
|
||||
}
|
||||
slices.Reverse(work)
|
||||
for i, p := range work {
|
||||
index.sorts[NameDescending][i] = &p
|
||||
}
|
||||
return index, err
|
||||
}
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("pkgserver: ")
|
||||
|
||||
var (
|
||||
flagBaseDir string
|
||||
flagPort int
|
||||
flagAddr string
|
||||
)
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
@@ -181,49 +33,70 @@ func main() {
|
||||
msg := message.New(log.Default())
|
||||
|
||||
c := command.New(os.Stderr, log.Printf, "pkgserver", func(args []string) error {
|
||||
reportPath := args[0]
|
||||
var (
|
||||
cache *pkg.Cache
|
||||
report *rosa.Report
|
||||
)
|
||||
switch len(args) {
|
||||
case 0:
|
||||
break
|
||||
|
||||
case 1:
|
||||
baseDir, err := check.NewAbs(flagBaseDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("baseDir:", baseDir)
|
||||
cache, err := pkg.Open(ctx, msg, 0, baseDir)
|
||||
|
||||
cache, err = pkg.Open(ctx, msg, 0, baseDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
report, err := rosa.OpenReport(reportPath)
|
||||
defer cache.Close()
|
||||
|
||||
report, err = rosa.OpenReport(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("reportPath:", reportPath)
|
||||
log.Println("indexing packages...")
|
||||
index, err := createPackageIndex(cache, report)
|
||||
if err != nil {
|
||||
|
||||
default:
|
||||
return errors.New("pkgserver requires 1 argument")
|
||||
|
||||
}
|
||||
|
||||
var index packageIndex
|
||||
if err := index.populate(cache, report); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("created package index")
|
||||
http.HandleFunc("GET /{$}", serveWebUI)
|
||||
http.HandleFunc("GET /favicon.ico", serveStaticContent)
|
||||
http.HandleFunc("GET /static/", serveStaticContent)
|
||||
http.HandleFunc("GET /api/", serveAPI(index))
|
||||
http.HandleFunc("GET /api/status/", serveStatus(index))
|
||||
log.Println("listening on", flagPort)
|
||||
err = http.ListenAndServe(fmt.Sprintf(":%d", flagPort), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
var mux http.ServeMux
|
||||
uiRoutes(&mux)
|
||||
index.registerAPI(&mux)
|
||||
server := http.Server{
|
||||
Addr: flagAddr,
|
||||
Handler: &mux,
|
||||
}
|
||||
return nil
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
c, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(c); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
return server.ListenAndServe()
|
||||
}).Flag(
|
||||
&flagBaseDir,
|
||||
"b", command.StringFlag(""),
|
||||
"base directory for cache",
|
||||
).Flag(
|
||||
&flagPort,
|
||||
"p", command.IntFlag(8067),
|
||||
"http listen port",
|
||||
&flagAddr,
|
||||
"addr", command.StringFlag(":8067"),
|
||||
"TCP network address to listen on",
|
||||
)
|
||||
c.MustParse(os.Args[1:], func(e error) {
|
||||
log.Fatal(e)
|
||||
})
|
||||
|
||||
c.MustParse(os.Args[1:], func(err error) {
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
os.Exit(0)
|
||||
}
|
||||
log.Fatal(err)
|
||||
})
|
||||
}
|
||||
|
||||
96
cmd/pkgserver/main_test.go
Normal file
96
cmd/pkgserver/main_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// newIndex returns the address of a newly populated packageIndex.
|
||||
func newIndex(t *testing.T) *packageIndex {
|
||||
t.Helper()
|
||||
|
||||
var index packageIndex
|
||||
if err := index.populate(nil, nil); err != nil {
|
||||
t.Fatalf("populate: error = %v", err)
|
||||
}
|
||||
return &index
|
||||
}
|
||||
|
||||
// checkStatus checks response status code.
|
||||
func checkStatus(t *testing.T, resp *http.Response, want int) {
|
||||
t.Helper()
|
||||
|
||||
if resp.StatusCode != want {
|
||||
t.Errorf(
|
||||
"StatusCode: %s, want %s",
|
||||
http.StatusText(resp.StatusCode),
|
||||
http.StatusText(want),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// checkHeader checks the value of a header entry.
|
||||
func checkHeader(t *testing.T, h http.Header, key, want string) {
|
||||
t.Helper()
|
||||
|
||||
if got := h.Get(key); got != want {
|
||||
t.Errorf("%s: %q, want %q", key, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// checkAPIHeader checks common entries set for API endpoints.
|
||||
func checkAPIHeader(t *testing.T, h http.Header) {
|
||||
t.Helper()
|
||||
|
||||
checkHeader(t, h, "Content-Type", "application/json; charset=utf-8")
|
||||
checkHeader(t, h, "Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
checkHeader(t, h, "Pragma", "no-cache")
|
||||
checkHeader(t, h, "Expires", "0")
|
||||
}
|
||||
|
||||
// checkPayloadFunc checks the JSON response of an API endpoint by passing it to f.
|
||||
func checkPayloadFunc[T any](
|
||||
t *testing.T,
|
||||
resp *http.Response,
|
||||
f func(got *T) bool,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
var got T
|
||||
r := io.Reader(resp.Body)
|
||||
if testing.Verbose() {
|
||||
var buf bytes.Buffer
|
||||
r = io.TeeReader(r, &buf)
|
||||
defer func() { t.Helper(); t.Log(buf.String()) }()
|
||||
}
|
||||
if err := json.NewDecoder(r).Decode(&got); err != nil {
|
||||
t.Fatalf("Decode: error = %v", err)
|
||||
}
|
||||
|
||||
if !f(&got) {
|
||||
t.Errorf("Body: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// checkPayload checks the JSON response of an API endpoint.
|
||||
func checkPayload[T any](t *testing.T, resp *http.Response, want T) {
|
||||
t.Helper()
|
||||
|
||||
checkPayloadFunc(t, resp, func(got *T) bool {
|
||||
return reflect.DeepEqual(got, &want)
|
||||
})
|
||||
}
|
||||
|
||||
func checkError(t *testing.T, resp *http.Response, error string, code int) {
|
||||
t.Helper()
|
||||
|
||||
checkStatus(t, resp, code)
|
||||
if got, _ := io.ReadAll(resp.Body); string(got) != fmt.Sprintln(error) {
|
||||
t.Errorf("Body: %q, want %q", string(got), error)
|
||||
}
|
||||
}
|
||||
48
cmd/pkgserver/ui.go
Normal file
48
cmd/pkgserver/ui.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:generate sh -c "sass ui/static/dark.scss ui/static/dark.css && sass ui/static/light.scss ui/static/light.css && tsc -p ui/static"
|
||||
//go:embed ui/*
|
||||
var content embed.FS
|
||||
|
||||
func serveWebUI(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("X-XSS-Protection", "1")
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
|
||||
http.ServeFileFS(w, r, content, "ui/index.html")
|
||||
}
|
||||
func serveStaticContent(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/static/style.css":
|
||||
darkTheme := r.CookiesNamed("dark_theme")
|
||||
if len(darkTheme) > 0 && darkTheme[0].Value == "true" {
|
||||
http.ServeFileFS(w, r, content, "ui/static/dark.css")
|
||||
} else {
|
||||
http.ServeFileFS(w, r, content, "ui/static/light.css")
|
||||
}
|
||||
break
|
||||
case "/favicon.ico":
|
||||
http.ServeFileFS(w, r, content, "ui/static/favicon.ico")
|
||||
break
|
||||
case "/static/index.js":
|
||||
http.ServeFileFS(w, r, content, "ui/static/index.js")
|
||||
break
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func uiRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /{$}", serveWebUI)
|
||||
mux.HandleFunc("GET /favicon.ico", serveStaticContent)
|
||||
mux.HandleFunc("GET /static/", serveStaticContent)
|
||||
}
|
||||
@@ -4,14 +4,13 @@
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="static/style.css">
|
||||
<title>Hakurei PkgServer</title>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
|
||||
<script src="static/index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hakurei PkgServer</h1>
|
||||
|
||||
<table id="pkg-list">
|
||||
<tr><th>Status</th><th>Name</th><th>Version</th></tr>
|
||||
<tr><td>Loading...</td></tr>
|
||||
</table>
|
||||
<p>Showing entries <span id="entry-counter"></span>.</p>
|
||||
<span class="bottom-nav"><a href="javascript:prevPage()">« Previous</a> <span id="page-number">1</span> <a href="javascript:nextPage()">Next »</a></span>
|
||||
@@ -21,6 +20,14 @@
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select></span>
|
||||
<span><label for="sort">Sort by: </label><select name="sort" id="sort">
|
||||
<option value="0">Definition (ascending)</option>
|
||||
<option value="1">Definition (descending)</option>
|
||||
<option value="2">Name (ascending)</option>
|
||||
<option value="3">Name (descending)</option>
|
||||
</select></span>
|
||||
</body>
|
||||
<footer>© <a href="https://hakurei.app/">Hakurei</a>. Licensed under the MIT license.</footer>
|
||||
<footer>
|
||||
<p>©<a href="https://hakurei.app/">Hakurei</a> (<span id="hakurei-version">unknown</span>). Licensed under the MIT license.</p>
|
||||
</footer>
|
||||
</html>
|
||||
@@ -1,67 +1,134 @@
|
||||
"use strict";
|
||||
var PackageEntry = /** @class */ (function () {
|
||||
function PackageEntry() {
|
||||
class PackageIndexEntry {
|
||||
name;
|
||||
description;
|
||||
website;
|
||||
version;
|
||||
report;
|
||||
}
|
||||
return PackageEntry;
|
||||
}());
|
||||
var State = /** @class */ (function () {
|
||||
function State() {
|
||||
this.entriesPerPage = 10;
|
||||
this.currentPage = 1;
|
||||
this.entryIndex = 0;
|
||||
this.loadedEntries = [];
|
||||
function toHTML(entry) {
|
||||
let v = entry.version != null ? `<span>${escapeHtml(entry.version)}</span>` : "";
|
||||
let d = entry.description != null ? `<p>${escapeHtml(entry.description)}</p>` : "";
|
||||
let w = entry.website != null ? `<a href="${encodeURI(entry.website)}">Website</a>` : "";
|
||||
let r = entry.report ? `Log (<a href=\"${encodeURI('/api/v1/status/' + entry.name)}\">View</a> | <a href=\"${encodeURI('/status/' + entry.name)}\">Download</a>)` : "";
|
||||
let row = (document.createElement('tr'));
|
||||
row.innerHTML = `<td>
|
||||
<h2>${escapeHtml(entry.name)} ${v}</h2>
|
||||
${d}
|
||||
${w}
|
||||
${r}
|
||||
</td>`;
|
||||
return row;
|
||||
}
|
||||
State.prototype.getEntriesPerPage = function () {
|
||||
const API_VERSION = 1;
|
||||
const ENDPOINT = `/api/v${API_VERSION}`;
|
||||
class InfoPayload {
|
||||
count;
|
||||
hakurei_version;
|
||||
}
|
||||
async function infoRequest() {
|
||||
const res = await fetch(`${ENDPOINT}/info`);
|
||||
const payload = await res.json();
|
||||
return payload;
|
||||
}
|
||||
class GetPayload {
|
||||
count;
|
||||
values;
|
||||
}
|
||||
var SortOrders;
|
||||
(function (SortOrders) {
|
||||
SortOrders[SortOrders["DeclarationAscending"] = 0] = "DeclarationAscending";
|
||||
SortOrders[SortOrders["DeclarationDescending"] = 1] = "DeclarationDescending";
|
||||
SortOrders[SortOrders["NameAscending"] = 2] = "NameAscending";
|
||||
SortOrders[SortOrders["NameDescending"] = 3] = "NameDescending";
|
||||
})(SortOrders || (SortOrders = {}));
|
||||
async function getRequest(limit, index, sort) {
|
||||
const res = await fetch(`${ENDPOINT}/get?limit=${limit}&index=${index}&sort=${sort.valueOf()}`);
|
||||
const payload = await res.json();
|
||||
return payload;
|
||||
}
|
||||
class State {
|
||||
entriesPerPage = 10;
|
||||
entryIndex = 0;
|
||||
maxEntries = 0;
|
||||
sort = SortOrders.DeclarationAscending;
|
||||
getEntriesPerPage() {
|
||||
return this.entriesPerPage;
|
||||
};
|
||||
State.prototype.setEntriesPerPage = function (entriesPerPage) {
|
||||
this.entriesPerPage = entriesPerPage;
|
||||
this.updateRange();
|
||||
};
|
||||
State.prototype.getCurrentPage = function () {
|
||||
return this.currentPage;
|
||||
};
|
||||
State.prototype.setCurrentPage = function (page) {
|
||||
this.currentPage = page;
|
||||
document.getElementById("page-number").innerText = String(this.currentPage);
|
||||
this.updateRange();
|
||||
};
|
||||
State.prototype.getEntryIndex = function () {
|
||||
return this.entryIndex;
|
||||
};
|
||||
State.prototype.setEntryIndex = function (entryIndex) {
|
||||
this.entryIndex = entryIndex;
|
||||
this.updateRange();
|
||||
};
|
||||
State.prototype.getLoadedEntries = function () {
|
||||
return this.loadedEntries;
|
||||
};
|
||||
State.prototype.getMaxPage = function () {
|
||||
return this.loadedEntries.length / this.entriesPerPage;
|
||||
};
|
||||
State.prototype.updateRange = function () {
|
||||
var max = Math.min(this.entryIndex + this.entriesPerPage, this.loadedEntries.length);
|
||||
document.getElementById("entry-counter").innerText = "".concat(this.entryIndex, "-").concat(max, " of ").concat(this.loadedEntries.length);
|
||||
};
|
||||
return State;
|
||||
}());
|
||||
var STATE;
|
||||
function prevPage() {
|
||||
var current = STATE.getCurrentPage();
|
||||
if (current > 1) {
|
||||
STATE.setCurrentPage(STATE.getCurrentPage() - 1);
|
||||
}
|
||||
setEntriesPerPage(entriesPerPage) {
|
||||
this.entriesPerPage = entriesPerPage;
|
||||
this.setEntryIndex(Math.floor(this.getEntryIndex() / entriesPerPage) * entriesPerPage);
|
||||
}
|
||||
getEntryIndex() {
|
||||
return this.entryIndex;
|
||||
}
|
||||
setEntryIndex(entryIndex) {
|
||||
this.entryIndex = entryIndex;
|
||||
this.updatePage();
|
||||
this.updateRange();
|
||||
this.updateListings();
|
||||
}
|
||||
getMaxEntries() {
|
||||
return this.maxEntries;
|
||||
}
|
||||
setMaxEntries(max) {
|
||||
this.maxEntries = max;
|
||||
}
|
||||
getSortOrder() {
|
||||
return this.sort;
|
||||
}
|
||||
setSortOrder(sortOrder) {
|
||||
this.sort = sortOrder;
|
||||
this.setEntryIndex(0);
|
||||
}
|
||||
updatePage() {
|
||||
let page = Math.ceil(((this.getEntryIndex() + this.getEntriesPerPage()) - 1) / this.getEntriesPerPage());
|
||||
document.getElementById("page-number").innerText = String(page);
|
||||
}
|
||||
updateRange() {
|
||||
let max = Math.min(this.getEntryIndex() + this.getEntriesPerPage(), this.getMaxEntries());
|
||||
document.getElementById("entry-counter").innerText = `${this.getEntryIndex() + 1}-${max} of ${this.getMaxEntries()}`;
|
||||
}
|
||||
updateListings() {
|
||||
getRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSortOrder())
|
||||
.then(res => {
|
||||
let table = document.getElementById("pkg-list");
|
||||
table.innerHTML = '';
|
||||
for (let i = 0; i < res.count; i++) {
|
||||
table.appendChild(toHTML(res.values[i]));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
let STATE;
|
||||
function prevPage() {
|
||||
let index = STATE.getEntryIndex();
|
||||
STATE.setEntryIndex(Math.max(0, index - STATE.getEntriesPerPage()));
|
||||
}
|
||||
function nextPage() {
|
||||
var current = STATE.getCurrentPage();
|
||||
if (current < STATE.getMaxPage()) {
|
||||
STATE.setCurrentPage(STATE.getCurrentPage() + 1);
|
||||
let index = STATE.getEntryIndex();
|
||||
STATE.setEntryIndex(Math.min((Math.ceil(STATE.getMaxEntries() / STATE.getEntriesPerPage()) * STATE.getEntriesPerPage()) - STATE.getEntriesPerPage(), index + STATE.getEntriesPerPage()));
|
||||
}
|
||||
function escapeHtml(str) {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
STATE = new State();
|
||||
infoRequest()
|
||||
.then(res => {
|
||||
STATE.setMaxEntries(res.count);
|
||||
document.getElementById("hakurei-version").innerText = res.hakurei_version;
|
||||
STATE.updateRange();
|
||||
document.getElementById("count").addEventListener("change", function (event) {
|
||||
STATE.updateListings();
|
||||
});
|
||||
document.getElementById("count").addEventListener("change", (event) => {
|
||||
STATE.setEntriesPerPage(parseInt(event.target.value));
|
||||
});
|
||||
document.getElementById("sort").addEventListener("change", (event) => {
|
||||
STATE.setSortOrder(parseInt(event.target.value));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,66 +1,143 @@
|
||||
"use strict"
|
||||
class PackageIndexEntry {
|
||||
name: string
|
||||
description: string | null
|
||||
website: string | null
|
||||
version: string | null
|
||||
report: boolean
|
||||
}
|
||||
function toHTML(entry: PackageIndexEntry): HTMLTableRowElement {
|
||||
let v = entry.version != null ? `<span>${escapeHtml(entry.version)}</span>` : ""
|
||||
let d = entry.description != null ? `<p>${escapeHtml(entry.description)}</p>` : ""
|
||||
let w = entry.website != null ? `<a href="${encodeURI(entry.website)}">Website</a>` : ""
|
||||
let r = entry.report ? `Log (<a href=\"${encodeURI('/api/v1/status/' + entry.name)}\">View</a> | <a href=\"${encodeURI('/status/' + entry.name)}\">Download</a>)` : ""
|
||||
let row = <HTMLTableRowElement>(document.createElement('tr'))
|
||||
row.innerHTML = `<td>
|
||||
<h2>${escapeHtml(entry.name)} ${v}</h2>
|
||||
${d}
|
||||
${w}
|
||||
${r}
|
||||
</td>`
|
||||
return row
|
||||
}
|
||||
|
||||
class PackageEntry {
|
||||
const API_VERSION = 1
|
||||
const ENDPOINT = `/api/v${API_VERSION}`
|
||||
class InfoPayload {
|
||||
count: number
|
||||
hakurei_version: string
|
||||
}
|
||||
|
||||
async function infoRequest(): Promise<InfoPayload> {
|
||||
const res = await fetch(`${ENDPOINT}/info`)
|
||||
const payload = await res.json()
|
||||
return payload as InfoPayload
|
||||
}
|
||||
class GetPayload {
|
||||
count: number
|
||||
values: PackageIndexEntry[]
|
||||
}
|
||||
|
||||
enum SortOrders {
|
||||
DeclarationAscending = 0,
|
||||
DeclarationDescending,
|
||||
NameAscending,
|
||||
NameDescending
|
||||
}
|
||||
async function getRequest(limit: number, index: number, sort: SortOrders): Promise<GetPayload> {
|
||||
const res = await fetch(`${ENDPOINT}/get?limit=${limit}&index=${index}&sort=${sort.valueOf()}`)
|
||||
const payload = await res.json()
|
||||
return payload as GetPayload
|
||||
}
|
||||
class State {
|
||||
entriesPerPage: number = 10
|
||||
currentPage: number = 1
|
||||
entryIndex: number = 0
|
||||
loadedEntries: PackageEntry[] = []
|
||||
maxEntries: number = 0
|
||||
sort: SortOrders = SortOrders.DeclarationAscending
|
||||
|
||||
getEntriesPerPage(): number {
|
||||
return this.entriesPerPage
|
||||
}
|
||||
setEntriesPerPage(entriesPerPage: number) {
|
||||
this.entriesPerPage = entriesPerPage
|
||||
this.updateRange()
|
||||
}
|
||||
getCurrentPage(): number {
|
||||
return this.currentPage
|
||||
}
|
||||
setCurrentPage(page: number) {
|
||||
this.currentPage = page
|
||||
document.getElementById("page-number").innerText = String(this.currentPage)
|
||||
this.updateRange()
|
||||
this.setEntryIndex(Math.floor(this.getEntryIndex() / entriesPerPage) * entriesPerPage)
|
||||
}
|
||||
getEntryIndex(): number {
|
||||
return this.entryIndex
|
||||
}
|
||||
setEntryIndex(entryIndex: number) {
|
||||
this.entryIndex = entryIndex
|
||||
this.updatePage()
|
||||
this.updateRange()
|
||||
this.updateListings()
|
||||
}
|
||||
getLoadedEntries(): PackageEntry[] {
|
||||
return this.loadedEntries
|
||||
getMaxEntries(): number {
|
||||
return this.maxEntries
|
||||
}
|
||||
getMaxPage(): number {
|
||||
return this.loadedEntries.length / this.entriesPerPage
|
||||
setMaxEntries(max: number) {
|
||||
this.maxEntries = max
|
||||
}
|
||||
getSortOrder(): SortOrders {
|
||||
return this.sort
|
||||
}
|
||||
setSortOrder(sortOrder: SortOrders) {
|
||||
this.sort = sortOrder
|
||||
this.setEntryIndex(0)
|
||||
}
|
||||
updatePage() {
|
||||
let page = Math.ceil(((this.getEntryIndex() + this.getEntriesPerPage()) - 1) / this.getEntriesPerPage())
|
||||
document.getElementById("page-number").innerText = String(page)
|
||||
}
|
||||
updateRange() {
|
||||
let max = Math.min(this.entryIndex + this.entriesPerPage, this.loadedEntries.length)
|
||||
document.getElementById("entry-counter").innerText = `${this.entryIndex}-${max} of ${this.loadedEntries.length}`
|
||||
let max = Math.min(this.getEntryIndex() + this.getEntriesPerPage(), this.getMaxEntries())
|
||||
document.getElementById("entry-counter").innerText = `${this.getEntryIndex() + 1}-${max} of ${this.getMaxEntries()}`
|
||||
}
|
||||
updateListings() {
|
||||
getRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSortOrder())
|
||||
.then(res => {
|
||||
let table = document.getElementById("pkg-list")
|
||||
table.innerHTML = ''
|
||||
for(let i = 0; i < res.count; i++) {
|
||||
table.appendChild(toHTML(res.values[i]))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let STATE: State
|
||||
|
||||
function prevPage() {
|
||||
let current = STATE.getCurrentPage()
|
||||
if (current > 1) {
|
||||
STATE.setCurrentPage(STATE.getCurrentPage() - 1)
|
||||
}
|
||||
let index = STATE.getEntryIndex()
|
||||
STATE.setEntryIndex(Math.max(0, index - STATE.getEntriesPerPage()))
|
||||
}
|
||||
function nextPage() {
|
||||
let current = STATE.getCurrentPage()
|
||||
if (current < STATE.getMaxPage()) {
|
||||
STATE.setCurrentPage(STATE.getCurrentPage() + 1)
|
||||
let index = STATE.getEntryIndex()
|
||||
STATE.setEntryIndex(Math.min((Math.ceil(STATE.getMaxEntries() / STATE.getEntriesPerPage()) * STATE.getEntriesPerPage()) - STATE.getEntriesPerPage(), index + STATE.getEntriesPerPage()))
|
||||
}
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
STATE = new State()
|
||||
infoRequest()
|
||||
.then(res => {
|
||||
STATE.setMaxEntries(res.count)
|
||||
document.getElementById("hakurei-version").innerText = res.hakurei_version
|
||||
STATE.updateRange()
|
||||
STATE.updateListings()
|
||||
})
|
||||
|
||||
document.getElementById("count").addEventListener("change", (event) => {
|
||||
STATE.setEntriesPerPage(parseInt((event.target as HTMLSelectElement).value))
|
||||
})
|
||||
document.getElementById("sort").addEventListener("change", (event) => {
|
||||
STATE.setSortOrder(parseInt((event.target as HTMLSelectElement).value))
|
||||
})
|
||||
})
|
||||
5
cmd/pkgserver/ui/static/tsconfig.json
Normal file
5
cmd/pkgserver/ui/static/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2024"
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ type (
|
||||
AllowOrphan bool
|
||||
// Scheduling policy to set via sched_setscheduler(2). The zero value
|
||||
// skips this call. Supported policies are [SCHED_BATCH], [SCHED_IDLE].
|
||||
SchedPolicy int
|
||||
SchedPolicy SchedPolicy
|
||||
// Cgroup fd, nil to disable.
|
||||
Cgroup *int
|
||||
// ExtraFiles passed through to initial process in the container, with
|
||||
@@ -373,12 +373,23 @@ func (p *Container) Start() error {
|
||||
|
||||
// sched_setscheduler: thread-directed but acts on all processes
|
||||
// created from the calling thread
|
||||
if p.SchedPolicy > 0 {
|
||||
if p.SchedPolicy > 0 && p.SchedPolicy <= _SCHED_LAST {
|
||||
var param schedParam
|
||||
if priority, err := p.SchedPolicy.GetPriorityMin(); err != nil {
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
Step: "get minimum priority",
|
||||
Err: err,
|
||||
}
|
||||
} else {
|
||||
param.priority = priority
|
||||
}
|
||||
|
||||
p.msg.Verbosef("setting scheduling policy %d", p.SchedPolicy)
|
||||
if err := schedSetscheduler(
|
||||
0, // calling thread
|
||||
p.SchedPolicy,
|
||||
&schedParam{0},
|
||||
¶m,
|
||||
); err != nil {
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"strconv"
|
||||
"sync"
|
||||
. "syscall"
|
||||
"unsafe"
|
||||
|
||||
@@ -43,18 +46,132 @@ func Isatty(fd int) bool {
|
||||
return r == 0
|
||||
}
|
||||
|
||||
// SchedPolicy denotes a scheduling policy defined in include/uapi/linux/sched.h.
|
||||
type SchedPolicy int
|
||||
|
||||
// include/uapi/linux/sched.h
|
||||
const (
|
||||
SCHED_NORMAL = iota
|
||||
SCHED_NORMAL SchedPolicy = iota
|
||||
SCHED_FIFO
|
||||
SCHED_RR
|
||||
SCHED_BATCH
|
||||
_ // SCHED_ISO: reserved but not implemented yet
|
||||
_SCHED_ISO // SCHED_ISO: reserved but not implemented yet
|
||||
SCHED_IDLE
|
||||
SCHED_DEADLINE
|
||||
SCHED_EXT
|
||||
|
||||
_SCHED_LAST SchedPolicy = iota - 1
|
||||
)
|
||||
|
||||
var _ encoding.TextMarshaler = _SCHED_LAST
|
||||
var _ encoding.TextUnmarshaler = new(_SCHED_LAST)
|
||||
|
||||
// String returns a unique representation of policy, also used in encoding.
|
||||
func (policy SchedPolicy) String() string {
|
||||
switch policy {
|
||||
case SCHED_NORMAL:
|
||||
return ""
|
||||
case SCHED_FIFO:
|
||||
return "fifo"
|
||||
case SCHED_RR:
|
||||
return "rr"
|
||||
case SCHED_BATCH:
|
||||
return "batch"
|
||||
case SCHED_IDLE:
|
||||
return "idle"
|
||||
case SCHED_DEADLINE:
|
||||
return "deadline"
|
||||
case SCHED_EXT:
|
||||
return "ext"
|
||||
|
||||
default:
|
||||
return "invalid policy " + strconv.Itoa(int(policy))
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalText performs bounds checking and returns the result of String.
|
||||
func (policy SchedPolicy) MarshalText() ([]byte, error) {
|
||||
if policy == _SCHED_ISO || policy < 0 || policy > _SCHED_LAST {
|
||||
return nil, EINVAL
|
||||
}
|
||||
return []byte(policy.String()), nil
|
||||
}
|
||||
|
||||
// InvalidSchedPolicyError is an invalid string representation of a [SchedPolicy].
|
||||
type InvalidSchedPolicyError string
|
||||
|
||||
func (InvalidSchedPolicyError) Unwrap() error { return EINVAL }
|
||||
func (e InvalidSchedPolicyError) Error() string {
|
||||
return "invalid scheduling policy " + strconv.Quote(string(e))
|
||||
}
|
||||
|
||||
// UnmarshalText is the inverse of MarshalText.
|
||||
func (policy *SchedPolicy) UnmarshalText(text []byte) error {
|
||||
switch string(text) {
|
||||
case "fifo":
|
||||
*policy = SCHED_FIFO
|
||||
case "rr":
|
||||
*policy = SCHED_RR
|
||||
case "batch":
|
||||
*policy = SCHED_BATCH
|
||||
case "idle":
|
||||
*policy = SCHED_IDLE
|
||||
case "deadline":
|
||||
*policy = SCHED_DEADLINE
|
||||
case "ext":
|
||||
*policy = SCHED_EXT
|
||||
|
||||
case "":
|
||||
*policy = 0
|
||||
return nil
|
||||
default:
|
||||
return InvalidSchedPolicyError(text)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// for sched_get_priority_max and sched_get_priority_min
|
||||
var (
|
||||
schedPriority [_SCHED_LAST + 1][2]std.Int
|
||||
schedPriorityErr [_SCHED_LAST + 1][2]error
|
||||
schedPriorityOnce [_SCHED_LAST + 1][2]sync.Once
|
||||
)
|
||||
|
||||
// GetPriorityMax returns the maximum priority value that can be used with the
|
||||
// scheduling algorithm identified by policy.
|
||||
func (policy SchedPolicy) GetPriorityMax() (std.Int, error) {
|
||||
schedPriorityOnce[policy][0].Do(func() {
|
||||
priority, _, errno := Syscall(
|
||||
SYS_SCHED_GET_PRIORITY_MAX,
|
||||
uintptr(policy),
|
||||
0, 0,
|
||||
)
|
||||
schedPriority[policy][0] = std.Int(priority)
|
||||
if schedPriority[policy][0] < 0 {
|
||||
schedPriorityErr[policy][0] = errno
|
||||
}
|
||||
})
|
||||
return schedPriority[policy][0], schedPriorityErr[policy][0]
|
||||
}
|
||||
|
||||
// GetPriorityMin returns the minimum priority value that can be used with the
|
||||
// scheduling algorithm identified by policy.
|
||||
func (policy SchedPolicy) GetPriorityMin() (std.Int, error) {
|
||||
schedPriorityOnce[policy][1].Do(func() {
|
||||
priority, _, errno := Syscall(
|
||||
SYS_SCHED_GET_PRIORITY_MIN,
|
||||
uintptr(policy),
|
||||
0, 0,
|
||||
)
|
||||
schedPriority[policy][1] = std.Int(priority)
|
||||
if schedPriority[policy][1] < 0 {
|
||||
schedPriorityErr[policy][1] = errno
|
||||
}
|
||||
})
|
||||
return schedPriority[policy][1], schedPriorityErr[policy][1]
|
||||
|
||||
}
|
||||
|
||||
// schedParam is equivalent to struct sched_param from include/linux/sched.h.
|
||||
type schedParam struct {
|
||||
// sched_priority
|
||||
@@ -74,7 +191,7 @@ type schedParam struct {
|
||||
// this if you do not have something similar in place!
|
||||
//
|
||||
// [very subtle to use correctly]: https://www.openwall.com/lists/musl/2016/03/01/4
|
||||
func schedSetscheduler(tid, policy int, param *schedParam) error {
|
||||
func schedSetscheduler(tid int, policy SchedPolicy, param *schedParam) error {
|
||||
if r, _, errno := Syscall(
|
||||
SYS_SCHED_SETSCHEDULER,
|
||||
uintptr(tid),
|
||||
|
||||
100
container/syscall_test.go
Normal file
100
container/syscall_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package container_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math"
|
||||
"reflect"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/std"
|
||||
)
|
||||
|
||||
func TestSchedPolicyJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
policy container.SchedPolicy
|
||||
want string
|
||||
encodeErr error
|
||||
decodeErr error
|
||||
}{
|
||||
{container.SCHED_NORMAL, `""`, nil, nil},
|
||||
{container.SCHED_FIFO, `"fifo"`, nil, nil},
|
||||
{container.SCHED_RR, `"rr"`, nil, nil},
|
||||
{container.SCHED_BATCH, `"batch"`, nil, nil},
|
||||
{4, `"invalid policy 4"`, syscall.EINVAL, container.InvalidSchedPolicyError("invalid policy 4")},
|
||||
{container.SCHED_IDLE, `"idle"`, nil, nil},
|
||||
{container.SCHED_DEADLINE, `"deadline"`, nil, nil},
|
||||
{container.SCHED_EXT, `"ext"`, nil, nil},
|
||||
{math.MaxInt, `"iso"`, syscall.EINVAL, container.InvalidSchedPolicyError("iso")},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
name := tc.policy.String()
|
||||
if tc.policy == container.SCHED_NORMAL {
|
||||
name = "normal"
|
||||
}
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := json.Marshal(tc.policy)
|
||||
if !errors.Is(err, tc.encodeErr) {
|
||||
t.Fatalf("Marshal: error = %v, want %v", err, tc.encodeErr)
|
||||
}
|
||||
if err == nil && string(got) != tc.want {
|
||||
t.Fatalf("Marshal: %s, want %s", string(got), tc.want)
|
||||
}
|
||||
|
||||
var v container.SchedPolicy
|
||||
if err = json.Unmarshal([]byte(tc.want), &v); !reflect.DeepEqual(err, tc.decodeErr) {
|
||||
t.Fatalf("Unmarshal: error = %v, want %v", err, tc.decodeErr)
|
||||
}
|
||||
if err == nil && v != tc.policy {
|
||||
t.Fatalf("Unmarshal: %d, want %d", v, tc.policy)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchedPolicyMinMax(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
policy container.SchedPolicy
|
||||
min, max std.Int
|
||||
err error
|
||||
}{
|
||||
{container.SCHED_NORMAL, 0, 0, nil},
|
||||
{container.SCHED_FIFO, 1, 99, nil},
|
||||
{container.SCHED_RR, 1, 99, nil},
|
||||
{container.SCHED_BATCH, 0, 0, nil},
|
||||
{4, -1, -1, syscall.EINVAL},
|
||||
{container.SCHED_IDLE, 0, 0, nil},
|
||||
{container.SCHED_DEADLINE, 0, 0, nil},
|
||||
{container.SCHED_EXT, 0, 0, nil},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
name := tc.policy.String()
|
||||
if tc.policy == container.SCHED_NORMAL {
|
||||
name = "normal"
|
||||
}
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if priority, err := tc.policy.GetPriorityMax(); !reflect.DeepEqual(err, tc.err) {
|
||||
t.Fatalf("GetPriorityMax: error = %v, want %v", err, tc.err)
|
||||
} else if priority != tc.max {
|
||||
t.Fatalf("GetPriorityMax: %d, want %d", priority, tc.max)
|
||||
}
|
||||
if priority, err := tc.policy.GetPriorityMin(); !reflect.DeepEqual(err, tc.err) {
|
||||
t.Fatalf("GetPriorityMin: error = %v, want %v", err, tc.err)
|
||||
} else if priority != tc.min {
|
||||
t.Fatalf("GetPriorityMin: %d, want %d", priority, tc.min)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
12
flake.lock
generated
12
flake.lock
generated
@@ -7,11 +7,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1765384171,
|
||||
"narHash": "sha256-FuFtkJrW1Z7u+3lhzPRau69E0CNjADku1mLQQflUORo=",
|
||||
"lastModified": 1772985280,
|
||||
"narHash": "sha256-FdrNykOoY9VStevU4zjSUdvsL9SzJTcXt4omdEDZDLk=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "44777152652bc9eacf8876976fa72cc77ca8b9d8",
|
||||
"rev": "8f736f007139d7f70752657dff6a401a585d6cbc",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -23,11 +23,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1765311797,
|
||||
"narHash": "sha256-mSD5Ob7a+T2RNjvPvOA1dkJHGVrNVl8ZOrAwBjKBDQo=",
|
||||
"lastModified": 1772822230,
|
||||
"narHash": "sha256-yf3iYLGbGVlIthlQIk5/4/EQDZNNEmuqKZkQssMljuw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "09eb77e94fa25202af8f3e81ddc7353d9970ac1b",
|
||||
"rev": "71caefce12ba78d84fe618cf61644dce01cf3a96",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
hakurei = pkgs.pkgsStatic.callPackage ./package.nix {
|
||||
inherit (pkgs)
|
||||
# passthru.buildInputs
|
||||
go
|
||||
go_1_26
|
||||
clang
|
||||
|
||||
# nativeBuildInputs
|
||||
@@ -182,7 +182,7 @@
|
||||
let
|
||||
# this is used for interactive vm testing during development, where tests might be broken
|
||||
package = self.packages.${pkgs.stdenv.hostPlatform.system}.hakurei.override {
|
||||
buildGoModule = previousArgs: pkgs.pkgsStatic.buildGoModule (previousArgs // { doCheck = false; });
|
||||
buildGo126Module = previousArgs: pkgs.pkgsStatic.buildGo126Module (previousArgs // { doCheck = false; });
|
||||
};
|
||||
in
|
||||
{
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
//go:generate gocc -a azalea.bnf
|
||||
package azalea
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
)
|
||||
|
||||
type Parser struct {
|
||||
Generator
|
||||
}
|
||||
|
||||
func NewParser(gen Generator) *Parser {
|
||||
return &Parser{
|
||||
Generator: gen,
|
||||
}
|
||||
}
|
||||
func (p Parser) Initialise() {
|
||||
|
||||
}
|
||||
|
||||
func (p Parser) Consume(ns string, file io.Reader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConsumeDir walks a directory and consumes all Azalea source files within it and all its subdirectories, as long as they end with the .az extension.
|
||||
func (p Parser) ConsumeDir(dir *check.Absolute) error {
|
||||
ds := dir.String()
|
||||
return filepath.WalkDir(ds, func(path string, d fs.DirEntry, err error) (e error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() || !strings.HasSuffix(d.Name(), ".az") {
|
||||
return
|
||||
}
|
||||
rel, e := filepath.Rel(ds, path)
|
||||
ns := strings.TrimSuffix(rel, ".az")
|
||||
f, e := os.Open(path)
|
||||
return p.Consume(ns, f)
|
||||
})
|
||||
}
|
||||
|
||||
// ConsumeAll consumes all provided readers as Azalea source code, each given the namespace `r%d` where `%d` is the index of the reader in the provided arguments.
|
||||
func (p Parser) ConsumeAll(in ...io.Reader) error {
|
||||
for i, r := range in {
|
||||
err := p.Consume("r"+strconv.FormatInt(int64(i), 10), r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConsumeStrings consumes all provided strings as Azalea source code, each given the namespace `s%d` where `%d` is the index of the string in the provided arugments.
|
||||
func (p Parser) ConsumeStrings(in ...string) error {
|
||||
for i, s := range in {
|
||||
err := p.Consume("s"+strconv.FormatInt(int64(i), 10), strings.NewReader(s))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package azalea
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type Generator interface {
|
||||
Finalise() (error, io.Writer)
|
||||
}
|
||||
|
||||
type JsonGenerator struct {
|
||||
t any
|
||||
}
|
||||
|
||||
func NewJsonGenerator[T any]() JsonGenerator {
|
||||
t := new(T)
|
||||
|
||||
return JsonGenerator{
|
||||
t,
|
||||
}
|
||||
}
|
||||
|
||||
func (j *JsonGenerator) Finalise() (error, io.Writer) {
|
||||
|
||||
}
|
||||
|
||||
type PkgIRGenerator struct {
|
||||
}
|
||||
|
||||
func NewPkgIRGenerator() PkgIRGenerator {
|
||||
return PkgIRGenerator{}
|
||||
}
|
||||
|
||||
func (p *PkgIRGenerator) Finalise() (error, io.Writer) {
|
||||
|
||||
}
|
||||
@@ -40,7 +40,7 @@ type ExecPath struct {
|
||||
}
|
||||
|
||||
// SchedPolicy is the [container] scheduling policy.
|
||||
var SchedPolicy int
|
||||
var SchedPolicy container.SchedPolicy
|
||||
|
||||
// PromoteLayers returns artifacts with identical-by-content layers promoted to
|
||||
// the highest priority instance, as if mounted via [ExecPath].
|
||||
|
||||
@@ -82,6 +82,11 @@ install -Dm0500 \
|
||||
echo "Installing linux $1..."
|
||||
cp -av "$2" "$4"
|
||||
cp -av "$3" "$4"
|
||||
`))),
|
||||
pkg.Path(AbsUsrSrc.Append(
|
||||
".depmod",
|
||||
), false, pkg.NewFile("depmod", []byte(`#!/bin/sh
|
||||
exec /system/sbin/depmod -m /lib/modules "$@"
|
||||
`))),
|
||||
},
|
||||
|
||||
@@ -1210,6 +1215,11 @@ cgit 1.2.3-korg
|
||||
"all",
|
||||
},
|
||||
Install: `
|
||||
# kernel is not aware of kmod moduledir
|
||||
install -Dm0500 \
|
||||
/usr/src/.depmod \
|
||||
/sbin/depmod
|
||||
|
||||
make \
|
||||
"-j$(nproc)" \
|
||||
-f /usr/src/kernel/Makefile \
|
||||
@@ -1217,9 +1227,10 @@ make \
|
||||
LLVM=1 \
|
||||
INSTALL_PATH=/work \
|
||||
install \
|
||||
INSTALL_MOD_PATH=/work \
|
||||
INSTALL_MOD_PATH=/work/system \
|
||||
DEPMOD=/sbin/depmod \
|
||||
modules_install
|
||||
rm -v /work/lib/modules/` + kernelVersion + `/build
|
||||
rm -v /work/system/lib/modules/` + kernelVersion + `/build
|
||||
`,
|
||||
},
|
||||
Flex,
|
||||
|
||||
@@ -14,6 +14,7 @@ func (t Toolchain) newKmod() (pkg.Artifact, string) {
|
||||
pkg.TarGzip,
|
||||
), nil, &MesonHelper{
|
||||
Setup: [][2]string{
|
||||
{"Dmoduledir", "/system/lib/modules"},
|
||||
{"Dsysconfdir", "/system/etc"},
|
||||
{"Dbashcompletiondir", "no"},
|
||||
{"Dfishcompletiondir", "no"},
|
||||
|
||||
@@ -125,6 +125,8 @@ func (t Toolchain) newLLVMVariant(variant string, attr *llvmAttr) pkg.Artifact {
|
||||
|
||||
[2]string{"LLVM_INSTALL_BINUTILS_SYMLINKS", "ON"},
|
||||
[2]string{"LLVM_INSTALL_CCTOOLS_SYMLINKS", "ON"},
|
||||
|
||||
[2]string{"LLVM_LIT_ARGS", "'--verbose'"},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
16
package.nix
16
package.nix
@@ -1,7 +1,7 @@
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
buildGoModule,
|
||||
buildGo126Module,
|
||||
makeBinaryWrapper,
|
||||
xdg-dbus-proxy,
|
||||
pkg-config,
|
||||
@@ -17,7 +17,7 @@
|
||||
fuse3,
|
||||
|
||||
# for passthru.buildInputs
|
||||
go,
|
||||
go_1_26,
|
||||
clang,
|
||||
|
||||
# for check
|
||||
@@ -28,7 +28,7 @@
|
||||
withStatic ? stdenv.hostPlatform.isStatic,
|
||||
}:
|
||||
|
||||
buildGoModule rec {
|
||||
buildGo126Module rec {
|
||||
pname = "hakurei";
|
||||
version = "0.3.6";
|
||||
|
||||
@@ -51,7 +51,7 @@ buildGoModule rec {
|
||||
];
|
||||
|
||||
nativeBuildInputs = [
|
||||
go
|
||||
go_1_26
|
||||
pkg-config
|
||||
wayland-scanner
|
||||
];
|
||||
@@ -125,8 +125,11 @@ buildGoModule rec {
|
||||
--inherit-argv0 --prefix PATH : ${lib.makeBinPath appPackages}
|
||||
'';
|
||||
|
||||
passthru.targetPkgs = [
|
||||
go
|
||||
passthru = {
|
||||
go = go_1_26;
|
||||
|
||||
targetPkgs = [
|
||||
go_1_26
|
||||
clang
|
||||
xorg.xorgproto
|
||||
util-linux
|
||||
@@ -137,4 +140,5 @@ buildGoModule rec {
|
||||
]
|
||||
++ buildInputs
|
||||
++ nativeBuildInputs;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ testers.nixosTest {
|
||||
(writeShellScriptBin "hakurei-test" ''
|
||||
# Assert hst CGO_ENABLED=0: ${
|
||||
with pkgs;
|
||||
runCommand "hakurei-hst-cgo" { nativeBuildInputs = [ go ]; } ''
|
||||
runCommand "hakurei-hst-cgo" { nativeBuildInputs = [ self.packages.${system}.hakurei.go ]; } ''
|
||||
cp -r ${options.environment.hakurei.package.default.src} "$out"
|
||||
chmod -R +w "$out"
|
||||
cp ${writeText "hst_cgo_test.go" ''package hakurei_test;import("testing";"hakurei.app/hst");func TestTemplate(t *testing.T){hst.Template()}''} "$out/hst_cgo_test.go"
|
||||
|
||||
Reference in New Issue
Block a user