Compare commits
39 Commits
develop
...
pkgserver-
| Author | SHA1 | Date | |
|---|---|---|---|
|
6f78444b11
|
|||
|
2a3f6f5384
|
|||
|
ef8663461b
|
|||
|
0b3be27b9a
|
|||
|
61a25c88ae
|
|||
| c7e195fe64 | |||
| d5db9add98 | |||
| ab8abdc82b | |||
| 770fd46510 | |||
| 99f1c6aab4 | |||
| 9ee629d402 | |||
| f475dde8b9 | |||
| c43a0c41b6 | |||
| 55827f1a85 | |||
| 721bdddfa1 | |||
| fb18e599dd | |||
| ec9005c794 | |||
| c6d35b4003 | |||
| 6401533cc2 | |||
| 5d6c401beb | |||
| 0a2d6aec14 | |||
| 67b11335d6 | |||
| ef3bd1b60a | |||
| beae7c89db | |||
| ed26d1a1c2 | |||
| faa0006d47 | |||
| 796ddbc977 | |||
| 98ab020160 | |||
| 26a346036d | |||
| 4ac9c72132 | |||
| c39c07d440 | |||
| b3fa0fe271 | |||
| 92a90582bb | |||
| 2e5ac56bdf | |||
| 75133e0234 | |||
| c120d4de4f | |||
| d6af8edb4a | |||
| da25d609d5 | |||
| 95ceed0de0 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -27,6 +27,10 @@ go.work.sum
|
||||
|
||||
# go generate
|
||||
/cmd/hakurei/LICENSE
|
||||
/cmd/pkgserver/.sass-cache
|
||||
/cmd/pkgserver/ui/static/*.js
|
||||
/cmd/pkgserver/ui/static/*.css*
|
||||
/cmd/pkgserver/ui/static/*.css.map
|
||||
/internal/pkg/testdata/testtool
|
||||
/internal/rosa/hakurei_current.tar.gz
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<a href="https://git.gensokyo.uk/rosa/hakurei">
|
||||
<a href="https://git.gensokyo.uk/security/hakurei">
|
||||
<picture>
|
||||
<img src="https://basement.gensokyo.uk/images/yukari1.png" width="200px" alt="Yukari">
|
||||
</picture>
|
||||
@@ -8,16 +8,16 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://pkg.go.dev/hakurei.app"><img src="https://pkg.go.dev/badge/hakurei.app.svg" alt="Go Reference" /></a>
|
||||
<a href="https://git.gensokyo.uk/rosa/hakurei/actions"><img src="https://git.gensokyo.uk/rosa/hakurei/actions/workflows/test.yml/badge.svg?branch=staging&style=flat-square" alt="Gitea Workflow Status" /></a>
|
||||
<a href="https://git.gensokyo.uk/security/hakurei/actions"><img src="https://git.gensokyo.uk/security/hakurei/actions/workflows/test.yml/badge.svg?branch=staging&style=flat-square" alt="Gitea Workflow Status" /></a>
|
||||
<br/>
|
||||
<a href="https://git.gensokyo.uk/rosa/hakurei/releases"><img src="https://img.shields.io/gitea/v/release/rosa/hakurei?gitea_url=https%3A%2F%2Fgit.gensokyo.uk&color=purple" alt="Release" /></a>
|
||||
<a href="https://git.gensokyo.uk/security/hakurei/releases"><img src="https://img.shields.io/gitea/v/release/security/hakurei?gitea_url=https%3A%2F%2Fgit.gensokyo.uk&color=purple" alt="Release" /></a>
|
||||
<a href="https://goreportcard.com/report/hakurei.app"><img src="https://goreportcard.com/badge/hakurei.app" alt="Go Report Card" /></a>
|
||||
<a href="https://hakurei.app"><img src="https://img.shields.io/website?url=https%3A%2F%2Fhakurei.app" alt="Website" /></a>
|
||||
</p>
|
||||
|
||||
Hakurei is a tool for running sandboxed desktop applications as dedicated
|
||||
subordinate users on the Linux kernel. It implements the application container
|
||||
of [planterette (WIP)](https://git.gensokyo.uk/rosa/planterette), a
|
||||
of [planterette (WIP)](https://git.gensokyo.uk/security/planterette), a
|
||||
self-contained Android-like package manager with modern security features.
|
||||
|
||||
Interaction with hakurei happens entirely through structures described by
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
. "syscall"
|
||||
)
|
||||
|
||||
@@ -13,22 +12,6 @@ func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("earlyinit: ")
|
||||
|
||||
var (
|
||||
option map[string]string
|
||||
flags []string
|
||||
)
|
||||
if len(os.Args) > 1 {
|
||||
option = make(map[string]string)
|
||||
for _, s := range os.Args[1:] {
|
||||
key, value, ok := strings.Cut(s, "=")
|
||||
if !ok {
|
||||
flags = append(flags, s)
|
||||
continue
|
||||
}
|
||||
option[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if err := Mount(
|
||||
"devtmpfs",
|
||||
"/dev/",
|
||||
@@ -72,56 +55,4 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// staying in rootfs, these are no longer used
|
||||
must(os.Remove("/root"))
|
||||
must(os.Remove("/init"))
|
||||
|
||||
must(os.Mkdir("/proc", 0))
|
||||
mustSyscall("mount proc", Mount(
|
||||
"proc",
|
||||
"/proc",
|
||||
"proc",
|
||||
MS_NOSUID|MS_NOEXEC|MS_NODEV,
|
||||
"hidepid=1",
|
||||
))
|
||||
|
||||
must(os.Mkdir("/sys", 0))
|
||||
mustSyscall("mount sysfs", Mount(
|
||||
"sysfs",
|
||||
"/sys",
|
||||
"sysfs",
|
||||
0,
|
||||
"",
|
||||
))
|
||||
|
||||
// after top level has been set up
|
||||
mustSyscall("remount root", Mount(
|
||||
"",
|
||||
"/",
|
||||
"",
|
||||
MS_REMOUNT|MS_BIND|
|
||||
MS_RDONLY|MS_NODEV|MS_NOSUID|MS_NOEXEC,
|
||||
"",
|
||||
))
|
||||
|
||||
must(os.WriteFile(
|
||||
"/sys/module/firmware_class/parameters/path",
|
||||
[]byte("/system/lib/firmware"),
|
||||
0,
|
||||
))
|
||||
|
||||
}
|
||||
|
||||
// mustSyscall calls [log.Fatalln] if err is non-nil.
|
||||
func mustSyscall(action string, err error) {
|
||||
if err != nil {
|
||||
log.Fatalln("cannot "+action+":", err)
|
||||
}
|
||||
}
|
||||
|
||||
// must calls [log.Fatal] with err if it is non-nil.
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
176
cmd/pkgserver/api.go
Normal file
176
cmd/pkgserver/api.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"hakurei.app/internal/info"
|
||||
"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)
|
||||
}
|
||||
|
||||
// newStatusHandler returns a [http.HandlerFunc] that offers status files for
|
||||
// viewing or download, if available.
|
||||
func (index *packageIndex) newStatusHandler(disposition bool) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
m, ok := index.names[path.Base(r.URL.Path)]
|
||||
if !ok || !m.HasReport {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
contentType := "text/plain; charset=utf-8"
|
||||
if disposition {
|
||||
contentType = "application/octet-stream"
|
||||
|
||||
// quoting like this is unsound, but okay, because metadata is hardcoded
|
||||
contentDisposition := `attachment; filename="`
|
||||
contentDisposition += m.Name + "-"
|
||||
if m.Version != "" {
|
||||
contentDisposition += m.Version + "-"
|
||||
}
|
||||
contentDisposition += m.ids + `.log"`
|
||||
w.Header().Set("Content-Disposition", contentDisposition)
|
||||
}
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
if err := func() (err error) {
|
||||
defer index.handleAccess(&err)()
|
||||
_, err = w.Write(m.status)
|
||||
return
|
||||
}(); err != nil {
|
||||
log.Println(err)
|
||||
http.Error(
|
||||
w, "cannot deliver status, contact maintainers",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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]))]
|
||||
writeAPIPayload(w, &struct {
|
||||
Values []*metadata `json:"values"`
|
||||
}{values})
|
||||
}
|
||||
|
||||
func (index *packageIndex) handleSearch(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
|
||||
}
|
||||
search, err := url.PathUnescape(q.Get("search"))
|
||||
if len(search) > 100 || err != nil {
|
||||
http.Error(
|
||||
w, "search must be a string between 0 and 100 characters long",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
desc := q.Get("desc") == "true"
|
||||
n, res, err := index.performSearchQuery(limit, i, search, desc)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
writeAPIPayload(w, &struct {
|
||||
Count int `json:"count"`
|
||||
Results []searchResult `json:"results"`
|
||||
}{n, res})
|
||||
}
|
||||
|
||||
// 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+"/search", index.handleSearch)
|
||||
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),
|
||||
},
|
||||
})
|
||||
}
|
||||
105
cmd/pkgserver/index.go
Normal file
105
cmd/pkgserver/index.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/internal/rosa"
|
||||
)
|
||||
|
||||
const (
|
||||
declarationAscending = iota
|
||||
declarationDescending
|
||||
nameAscending
|
||||
nameDescending
|
||||
sizeAscending
|
||||
sizeDescending
|
||||
|
||||
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
|
||||
search searchCache
|
||||
// Taken from [rosa.Report] if available.
|
||||
handleAccess func(*error) func()
|
||||
}
|
||||
|
||||
// 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"`
|
||||
// Output data size, available if present in report.
|
||||
Size int64 `json:"size,omitempty"`
|
||||
// Whether the underlying [pkg.Artifact] is present in the report.
|
||||
HasReport bool `json:"report"`
|
||||
|
||||
// Ident string encoded ahead of time.
|
||||
ids string
|
||||
// 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)()
|
||||
index.handleAccess = report.HandleAccess
|
||||
}
|
||||
|
||||
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 {
|
||||
id := cache.Ident(rosa.Std.Load(p))
|
||||
m.ids = pkg.Encode(id.Value())
|
||||
m.status, m.Size = report.ArtifactOf(id)
|
||||
m.HasReport = m.Size >= 0
|
||||
}
|
||||
|
||||
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][:])
|
||||
|
||||
index.sorts[sizeAscending] = work
|
||||
slices.SortFunc(index.sorts[sizeAscending][:], func(a, b *metadata) int {
|
||||
return cmp.Compare(a.Size, b.Size)
|
||||
})
|
||||
index.sorts[sizeDescending] = index.sorts[sizeAscending]
|
||||
slices.Reverse(index.sorts[sizeDescending][:])
|
||||
|
||||
return
|
||||
}
|
||||
114
cmd/pkgserver/main.go
Normal file
114
cmd/pkgserver/main.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/internal/rosa"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
const shutdownTimeout = 15 * time.Second
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("pkgserver: ")
|
||||
|
||||
var (
|
||||
flagBaseDir string
|
||||
flagAddr string
|
||||
)
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
defer stop()
|
||||
msg := message.New(log.Default())
|
||||
|
||||
c := command.New(os.Stderr, log.Printf, "pkgserver", func(args []string) error {
|
||||
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
|
||||
}
|
||||
|
||||
cache, err = pkg.Open(ctx, msg, 0, baseDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cache.Close()
|
||||
|
||||
report, err = rosa.OpenReport(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
return errors.New("pkgserver requires 1 argument")
|
||||
|
||||
}
|
||||
|
||||
var index packageIndex
|
||||
index.search = make(searchCache)
|
||||
if err := index.populate(cache, report); err != nil {
|
||||
return err
|
||||
}
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
index.search.clean()
|
||||
}
|
||||
}
|
||||
}()
|
||||
var mux http.ServeMux
|
||||
uiRoutes(&mux)
|
||||
index.registerAPI(&mux)
|
||||
server := http.Server{
|
||||
Addr: flagAddr,
|
||||
Handler: &mux,
|
||||
}
|
||||
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(
|
||||
&flagAddr,
|
||||
"addr", command.StringFlag(":8067"),
|
||||
"TCP network address to listen on",
|
||||
)
|
||||
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)
|
||||
}
|
||||
}
|
||||
77
cmd/pkgserver/search.go
Normal file
77
cmd/pkgserver/search.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"maps"
|
||||
"regexp"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
type searchCache map[string]searchCacheEntry
|
||||
type searchResult struct {
|
||||
NameIndices [][]int `json:"name_matches"`
|
||||
DescIndices [][]int `json:"desc_matches,omitempty"`
|
||||
Score float64 `json:"score"`
|
||||
*metadata
|
||||
}
|
||||
type searchCacheEntry struct {
|
||||
query string
|
||||
results []searchResult
|
||||
expiry time.Time
|
||||
}
|
||||
|
||||
func (index *packageIndex) performSearchQuery(limit int, i int, search string, desc bool) (int, []searchResult, error) {
|
||||
entry, ok := index.search[search]
|
||||
if ok {
|
||||
return len(entry.results), entry.results[i:min(i+limit, len(entry.results))], nil
|
||||
}
|
||||
|
||||
regex, err := regexp.Compile(search)
|
||||
if err != nil {
|
||||
return 0, make([]searchResult, 0), err
|
||||
}
|
||||
res := make([]searchResult, 0)
|
||||
for p := range maps.Values(index.names) {
|
||||
nameIndices := regex.FindAllIndex([]byte(p.Name), -1)
|
||||
var descIndices [][]int = nil
|
||||
if desc {
|
||||
descIndices = regex.FindAllIndex([]byte(p.Description), -1)
|
||||
}
|
||||
if nameIndices == nil && descIndices == nil {
|
||||
continue
|
||||
}
|
||||
score := float64(indexsum(nameIndices)) / (float64(len(nameIndices)) + 1)
|
||||
if desc {
|
||||
score += float64(indexsum(descIndices)) / (float64(len(descIndices)) + 1) / 10.0
|
||||
}
|
||||
res = append(res, searchResult{
|
||||
NameIndices: nameIndices,
|
||||
DescIndices: descIndices,
|
||||
Score: score,
|
||||
metadata: p,
|
||||
})
|
||||
}
|
||||
slices.SortFunc(res[:], func(a, b searchResult) int { return -cmp.Compare(a.Score, b.Score) })
|
||||
expiry := time.Now().Add(1 * time.Minute)
|
||||
entry = searchCacheEntry{
|
||||
query: search,
|
||||
results: res,
|
||||
expiry: expiry,
|
||||
}
|
||||
index.search[search] = entry
|
||||
|
||||
return len(res), res[i:min(i+limit, len(entry.results))], nil
|
||||
}
|
||||
func (s *searchCache) clean() {
|
||||
maps.DeleteFunc(*s, func(_ string, v searchCacheEntry) bool {
|
||||
return v.expiry.Before(time.Now())
|
||||
})
|
||||
}
|
||||
func indexsum(in [][]int) int {
|
||||
sum := 0
|
||||
for i := 0; i < len(in); i++ {
|
||||
sum += in[i][1] - in[i][0]
|
||||
}
|
||||
return sum
|
||||
}
|
||||
48
cmd/pkgserver/ui.go
Normal file
48
cmd/pkgserver/ui.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import "net/http"
|
||||
|
||||
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")
|
||||
}
|
||||
case "/favicon.ico":
|
||||
http.ServeFileFS(w, r, content, "ui/static/favicon.ico")
|
||||
case "/static/index.js":
|
||||
http.ServeFileFS(w, r, content, "ui/static/index.js")
|
||||
case "/static/test.js":
|
||||
http.ServeFileFS(w, r, content, "ui/static/test.js")
|
||||
case "/static/test.css":
|
||||
http.ServeFileFS(w, r, content, "ui/static/test.css")
|
||||
case "/static/test_tests.js":
|
||||
http.ServeFileFS(w, r, content, "ui/static/test_tests.js")
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
|
||||
}
|
||||
}
|
||||
func serveTester(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFileFS(w, r, content, "ui/test.html")
|
||||
}
|
||||
|
||||
func uiRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /{$}", serveWebUI)
|
||||
mux.HandleFunc("GET /favicon.ico", serveStaticContent)
|
||||
mux.HandleFunc("GET /static/", serveStaticContent)
|
||||
mux.HandleFunc("GET /test.html", serveTester)
|
||||
}
|
||||
35
cmd/pkgserver/ui/index.html
Normal file
35
cmd/pkgserver/ui/index.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="static/style.css">
|
||||
<title>Hakurei PkgServer</title>
|
||||
<script src="static/index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hakurei PkgServer</h1>
|
||||
|
||||
<table id="pkg-list">
|
||||
<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>
|
||||
<span><label for="count">Entries per page: </label><select name="count" id="count">
|
||||
<option value="10">10</option>
|
||||
<option value="20">20</option>
|
||||
<option value="30">30</option>
|
||||
<option value="50">50</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>
|
||||
<option value="4">Size (ascending)</option>
|
||||
<option value="5">Size (descending)</option>
|
||||
</select></span>
|
||||
</body>
|
||||
<footer>
|
||||
<p>©<a href="https://hakurei.app/">Hakurei</a> (<span id="hakurei-version">unknown</span>). Licensed under the MIT license.</p>
|
||||
</footer>
|
||||
</html>
|
||||
0
cmd/pkgserver/ui/static/_common.scss
Normal file
0
cmd/pkgserver/ui/static/_common.scss
Normal file
6
cmd/pkgserver/ui/static/dark.scss
Normal file
6
cmd/pkgserver/ui/static/dark.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
@use 'common';
|
||||
|
||||
html {
|
||||
background-color: #2c2c2c;
|
||||
color: ghostwhite;
|
||||
}
|
||||
BIN
cmd/pkgserver/ui/static/favicon.ico
Normal file
BIN
cmd/pkgserver/ui/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
155
cmd/pkgserver/ui/static/index.ts
Normal file
155
cmd/pkgserver/ui/static/index.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
class PackageIndexEntry {
|
||||
name: string
|
||||
size: number | null
|
||||
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 s = entry.size != null ? `<p>Size: ${toByteSizeString(entry.size)} (${entry.size})</p>` : ""
|
||||
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}
|
||||
${s}
|
||||
${w}
|
||||
${r}
|
||||
</td>`
|
||||
return row
|
||||
}
|
||||
|
||||
function toByteSizeString(bytes: number): string {
|
||||
if(bytes == null || bytes < 1024) return `${bytes}B`
|
||||
if(bytes < Math.pow(1024, 2)) return `${(bytes/1024).toFixed(2)}kiB`
|
||||
if(bytes < Math.pow(1024, 3)) return `${(bytes/Math.pow(1024, 2)).toFixed(2)}MiB`
|
||||
if(bytes < Math.pow(1024, 4)) return `${(bytes/Math.pow(1024, 3)).toFixed(2)}GiB`
|
||||
if(bytes < Math.pow(1024, 5)) return `${(bytes/Math.pow(1024, 4)).toFixed(2)}TiB`
|
||||
return "not only is it big, it's large"
|
||||
}
|
||||
|
||||
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 {
|
||||
values: PackageIndexEntry[]
|
||||
}
|
||||
|
||||
enum SortOrders {
|
||||
DeclarationAscending,
|
||||
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
|
||||
entryIndex: number = 0
|
||||
maxEntries: number = 0
|
||||
sort: SortOrders = SortOrders.DeclarationAscending
|
||||
|
||||
getEntriesPerPage(): number {
|
||||
return this.entriesPerPage
|
||||
}
|
||||
setEntriesPerPage(entriesPerPage: number) {
|
||||
this.entriesPerPage = entriesPerPage
|
||||
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()
|
||||
}
|
||||
getMaxEntries(): number {
|
||||
return this.maxEntries
|
||||
}
|
||||
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.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 = ''
|
||||
res.values.forEach((row) => {
|
||||
table.appendChild(toHTML(row))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let STATE: State
|
||||
|
||||
function prevPage() {
|
||||
let index = STATE.getEntryIndex()
|
||||
STATE.setEntryIndex(Math.max(0, index - STATE.getEntriesPerPage()))
|
||||
}
|
||||
function nextPage() {
|
||||
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 {
|
||||
if(str === undefined) return ""
|
||||
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))
|
||||
})
|
||||
})
|
||||
6
cmd/pkgserver/ui/static/light.scss
Normal file
6
cmd/pkgserver/ui/static/light.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
@use 'common';
|
||||
|
||||
html {
|
||||
background-color: #d3d3d3;
|
||||
color: black;
|
||||
}
|
||||
4
cmd/pkgserver/ui/static/run_tests.ts
Normal file
4
cmd/pkgserver/ui/static/run_tests.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
import "./test_tests.js";
|
||||
import { run, StreamReporter } from "./test.js";
|
||||
run(new StreamReporter({ writeln: console.log }));
|
||||
27
cmd/pkgserver/ui/static/test.scss
Normal file
27
cmd/pkgserver/ui/static/test.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
.root {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
details.test-node {
|
||||
margin-left: 1rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-left: 2px dashed black;
|
||||
> summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
&.failure > summary::marker {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
p.test-desc {
|
||||
margin: 0 0 0 1rem;
|
||||
padding: 2px 0;
|
||||
> pre {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
250
cmd/pkgserver/ui/static/test.ts
Normal file
250
cmd/pkgserver/ui/static/test.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
// =============================================================================
|
||||
// DSL
|
||||
|
||||
type TestTree = { name: string } & (TestGroup | Test);
|
||||
type TestGroup = { children: TestTree[] };
|
||||
type Test = { test: (TestController) => void };
|
||||
|
||||
let TESTS: ({ name: string } & TestGroup)[] = [];
|
||||
|
||||
export function suite(name: string, children: TestTree[]) {
|
||||
checkDuplicates(name, children)
|
||||
TESTS.push({ name, children });
|
||||
}
|
||||
|
||||
export function context(name: string, children: TestTree[]): TestTree {
|
||||
checkDuplicates(name, children)
|
||||
return { name, children };
|
||||
}
|
||||
export const group = context;
|
||||
|
||||
export function test(name: string, test: (TestController) => void): TestTree {
|
||||
return { name, test };
|
||||
}
|
||||
|
||||
function checkDuplicates(parent: string, names: { name: string }[]) {
|
||||
let seen = new Set<string>();
|
||||
for (const { name } of names) {
|
||||
if (seen.has(name)) {
|
||||
throw new RangeError(`duplicate name '${name}' in '${parent}'`);
|
||||
}
|
||||
seen.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
class FailNowSentinel {}
|
||||
|
||||
class TestController {
|
||||
#logBuf: string[];
|
||||
#failed: boolean;
|
||||
|
||||
constructor() {
|
||||
this.#logBuf = [];
|
||||
this.#failed = false;
|
||||
}
|
||||
|
||||
fail() {
|
||||
this.#failed = true;
|
||||
}
|
||||
|
||||
failed(): boolean {
|
||||
return this.#failed;
|
||||
}
|
||||
|
||||
failNow(): never {
|
||||
this.fail();
|
||||
throw new FailNowSentinel();
|
||||
}
|
||||
|
||||
log(message: string) {
|
||||
this.#logBuf.push(message);
|
||||
}
|
||||
|
||||
error(message: string) {
|
||||
this.log(message);
|
||||
this.fail();
|
||||
}
|
||||
|
||||
fatal(message: string): never {
|
||||
this.log(message);
|
||||
this.failNow();
|
||||
}
|
||||
|
||||
getLog(): string {
|
||||
return this.#logBuf.join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Execution
|
||||
|
||||
export interface TestResult {
|
||||
success: boolean;
|
||||
output: string;
|
||||
}
|
||||
|
||||
function runTests(reporter: Reporter, parents: string[], tree: TestTree) {
|
||||
const path = [...parents, tree.name];
|
||||
if ("children" in tree) {
|
||||
for (const c of tree.children) runTests(reporter, path, c);
|
||||
return;
|
||||
}
|
||||
let controller = new TestController();
|
||||
let excStr: string;
|
||||
try {
|
||||
tree.test(controller);
|
||||
} catch (e) {
|
||||
if (!(e instanceof FailNowSentinel)) {
|
||||
controller.fail();
|
||||
excStr = extractExceptionString(e);
|
||||
}
|
||||
}
|
||||
const log = controller.getLog();
|
||||
const output = (log && excStr) ? `${log}\n${excStr}` : `${log}${excStr ?? ''}`;
|
||||
reporter.update(path, { success: !controller.failed(), output });
|
||||
}
|
||||
|
||||
export function run(reporter: Reporter) {
|
||||
for (const suite of TESTS) {
|
||||
for (const c of suite.children) runTests(reporter, [suite.name], c);
|
||||
}
|
||||
reporter.finalize();
|
||||
}
|
||||
|
||||
function extractExceptionString(e: any): string {
|
||||
// String() instead of .toString() as null and undefined don't have
|
||||
// properties.
|
||||
const s = String(e);
|
||||
if (!(e instanceof Error && "stack" in e)) return s;
|
||||
// v8 (Chromium, NodeJS) include the error message, while
|
||||
// Firefox and WebKit do not.
|
||||
if (e.stack.includes(s)) return e.stack;
|
||||
return `${s}\n${e.stack}`;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Reporting
|
||||
|
||||
export interface Reporter {
|
||||
update(path: string[], result: TestResult): void;
|
||||
finalize(): void;
|
||||
}
|
||||
|
||||
export interface Stream {
|
||||
writeln(s: string): void;
|
||||
}
|
||||
|
||||
export class StreamReporter implements Reporter {
|
||||
stream: Stream;
|
||||
verbose: boolean;
|
||||
#failures: ({ path: string[] } & TestResult)[];
|
||||
counts: { successes: number, failures: number };
|
||||
|
||||
constructor(stream: Stream, verbose: boolean = false) {
|
||||
this.stream = stream;
|
||||
this.verbose = verbose;
|
||||
this.#failures = [];
|
||||
this.counts = { successes: 0, failures: 0 };
|
||||
}
|
||||
|
||||
update(path: string[], result: TestResult) {
|
||||
if (path.length === 0) throw new RangeError("path is empty");
|
||||
const pathStr = path.join(" ❯ ");
|
||||
if (result.success) {
|
||||
this.counts.successes++;
|
||||
if (this.verbose) this.stream.writeln(`✅️ ${pathStr}`);
|
||||
} else {
|
||||
this.counts.failures++;
|
||||
this.stream.writeln(`⚠️ ${pathStr}`);
|
||||
this.#failures.push({ path, ...result });
|
||||
}
|
||||
}
|
||||
|
||||
finalize() {
|
||||
// Transform [{ path: ["a", "b", "c"] }, { path: ["a", "b", "d"] }]
|
||||
// into { "a ❯ b": ["c", "d"] }.
|
||||
let pathMap = new Map<string, ({ name: string } & TestResult)[]>();
|
||||
for (const f of this.#failures) {
|
||||
const key = f.path.slice(0, -1).join(" ❯ ");
|
||||
if (!pathMap.has(key)) pathMap.set(key, []);
|
||||
pathMap.get(key).push({ name: f.path.at(-1), ...f });
|
||||
}
|
||||
|
||||
this.stream.writeln("");
|
||||
this.stream.writeln("FAILURES");
|
||||
this.stream.writeln("========");
|
||||
|
||||
for (const [path, tests] of pathMap) {
|
||||
if (tests.length === 1) {
|
||||
this.#writeOutput(tests[0], path ? `${path} ❯ ` : "", false);
|
||||
} else {
|
||||
this.stream.writeln(path);
|
||||
for (const t of tests) this.#writeOutput(t, " - ", true);
|
||||
}
|
||||
}
|
||||
|
||||
this.stream.writeln("");
|
||||
const { successes, failures } = this.counts;
|
||||
this.stream.writeln(`${successes} succeeded, ${failures} failed`);
|
||||
}
|
||||
|
||||
#writeOutput(test: { name: string } & TestResult, prefix: string, nested: boolean) {
|
||||
let output = "";
|
||||
if (test.output) {
|
||||
const lines = test.output.split("\n");
|
||||
if (lines.length <= 1) {
|
||||
output = `: ${test.output}`;
|
||||
} else {
|
||||
const padding = nested ? " " : " ";
|
||||
output = ":\n" + lines.map((line) => padding + line).join("\n");
|
||||
}
|
||||
}
|
||||
this.stream.writeln(`${prefix}${test.name}${output}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class DOMReporter implements Reporter {
|
||||
update(path: string[], result: TestResult) {
|
||||
if (path.length === 0) throw new RangeError("path is empty");
|
||||
const counter = document.getElementById(result.success ? "successes" : "failures");
|
||||
counter.innerText = (Number(counter.innerText) + 1).toString();
|
||||
let parent = document.getElementById("root");
|
||||
for (const node of path) {
|
||||
let child = null;
|
||||
outer: for (const d of parent.children) {
|
||||
for (const s of d.children) {
|
||||
if (!(s instanceof HTMLElement)) continue;
|
||||
if (s.tagName !== "SUMMARY" || s.innerText !== node) continue;
|
||||
child = d;
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
if (child === null) {
|
||||
child = document.createElement("details");
|
||||
child.className = "test-node";
|
||||
const summary = document.createElement("summary");
|
||||
summary.appendChild(document.createTextNode(node));
|
||||
child.appendChild(summary);
|
||||
parent.appendChild(child);
|
||||
}
|
||||
if (!result.success) {
|
||||
child.open = true;
|
||||
child.classList.add("failure");
|
||||
}
|
||||
parent = child;
|
||||
}
|
||||
const p = document.createElement("p");
|
||||
p.classList.add("test-desc");
|
||||
if (result.output) {
|
||||
const pre = document.createElement("pre");
|
||||
pre.appendChild(document.createTextNode(result.output));
|
||||
p.appendChild(pre);
|
||||
} else {
|
||||
p.classList.add("italic");
|
||||
p.appendChild(document.createTextNode("No output."));
|
||||
}
|
||||
parent.appendChild(p);
|
||||
}
|
||||
|
||||
finalize() {}
|
||||
}
|
||||
40
cmd/pkgserver/ui/static/test_tests.ts
Normal file
40
cmd/pkgserver/ui/static/test_tests.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { context, group, suite, test } from "./test.js";
|
||||
|
||||
suite("dog", [
|
||||
group("tail", [
|
||||
test("wags when happy", (t) => {
|
||||
if (0 / 0 !== Infinity / Infinity) {
|
||||
t.fatal("undefined must not be defined");
|
||||
}
|
||||
}),
|
||||
test("idle when down", (t) => {
|
||||
t.log("test test");
|
||||
t.error("dog whining noises go here");
|
||||
}),
|
||||
]),
|
||||
test("likes headpats", (t) => {
|
||||
if (2 !== 2) {
|
||||
t.error("IEEE 754 violated: 2 is NaN");
|
||||
}
|
||||
}),
|
||||
context("near cat", [
|
||||
test("is ecstatic", (t) => {
|
||||
if (("b" + "a" + + "a" + "a").toLowerCase() == "banana") {
|
||||
t.error("🍌🍌🍌");
|
||||
t.error("🍌🍌🍌");
|
||||
t.error("🍌🍌🍌");
|
||||
t.failNow();
|
||||
}
|
||||
}),
|
||||
test("playfully bites cats' tails", (t) => {
|
||||
t.log("arf!");
|
||||
throw new Error("nom");
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
|
||||
suite("cat", [
|
||||
test("likes headpats", (t) => {
|
||||
t.log("meow");
|
||||
}),
|
||||
]);
|
||||
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"
|
||||
}
|
||||
}
|
||||
28
cmd/pkgserver/ui/test.html
Normal file
28
cmd/pkgserver/ui/test.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="static/style.css">
|
||||
<link rel="stylesheet" href="static/test.css">
|
||||
<title>PkgServer Tests</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>PkgServer Tests</h1>
|
||||
|
||||
<main>
|
||||
<div id="counters">
|
||||
<span id="successes">0</span> succeeded, <span id="failures">0</span> failed.
|
||||
</div>
|
||||
|
||||
<div id="root">
|
||||
</div>
|
||||
|
||||
<script type="module" src="./static/test_tests.js"></script>
|
||||
<script type="module">
|
||||
import { DOMReporter, run } from "./static/test.js";
|
||||
run(new DOMReporter());
|
||||
</script>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
9
cmd/pkgserver/ui_full.go
Normal file
9
cmd/pkgserver/ui_full.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build frontend
|
||||
|
||||
package main
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:generate sh -c "sass ui/static/dark.scss ui/static/dark.css && sass ui/static/light.scss ui/static/light.css && sass ui/static/test.scss ui/static/test.css && tsc -p ui/static"
|
||||
//go:embed ui/*
|
||||
var content embed.FS
|
||||
7
cmd/pkgserver/ui_stub.go
Normal file
7
cmd/pkgserver/ui_stub.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !frontend
|
||||
|
||||
package main
|
||||
|
||||
import "testing/fstest"
|
||||
|
||||
var content fstest.MapFS
|
||||
@@ -82,7 +82,7 @@ type Config struct {
|
||||
//
|
||||
// Do not set this to true, it is insecure under any configuration.
|
||||
//
|
||||
// [the /.flatpak-info hack]: https://git.gensokyo.uk/rosa/hakurei/issues/21
|
||||
// [the /.flatpak-info hack]: https://git.gensokyo.uk/security/hakurei/issues/21
|
||||
DirectPipeWire bool `json:"direct_pipewire,omitempty"`
|
||||
|
||||
// Direct access to PulseAudio socket, no attempt is made to establish
|
||||
|
||||
@@ -20,10 +20,8 @@ const (
|
||||
LLVMRuntimes
|
||||
LLVMClang
|
||||
|
||||
// EarlyInit is the Rosa OS init program.
|
||||
// EarlyInit is the Rosa OS initramfs init program.
|
||||
EarlyInit
|
||||
// ImageSystem is the Rosa OS /system image.
|
||||
ImageSystem
|
||||
// ImageInitramfs is the Rosa OS initramfs archive.
|
||||
ImageInitramfs
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package rosa
|
||||
|
||||
import (
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/internal/pkg"
|
||||
)
|
||||
import "hakurei.app/internal/pkg"
|
||||
|
||||
func init() {
|
||||
artifactsM[EarlyInit] = Metadata{
|
||||
@@ -27,36 +24,12 @@ echo
|
||||
}
|
||||
}
|
||||
|
||||
func (t Toolchain) newImageSystem() (pkg.Artifact, string) {
|
||||
return t.New("system.img", TNoToolchain, t.AppendPresets(nil,
|
||||
SquashfsTools,
|
||||
), nil, nil, `
|
||||
mksquashfs /mnt/system /work/system.img
|
||||
`, pkg.Path(fhs.AbsRoot.Append("mnt"), false, t.AppendPresets(nil,
|
||||
Musl,
|
||||
Mksh,
|
||||
Toybox,
|
||||
|
||||
Kmod,
|
||||
Kernel,
|
||||
Firmware,
|
||||
)...)), Unversioned
|
||||
}
|
||||
func init() {
|
||||
artifactsM[ImageSystem] = Metadata{
|
||||
Name: "system-image",
|
||||
Description: "Rosa OS system image",
|
||||
|
||||
f: Toolchain.newImageSystem,
|
||||
}
|
||||
}
|
||||
|
||||
func (t Toolchain) newImageInitramfs() (pkg.Artifact, string) {
|
||||
return t.New("initramfs", TNoToolchain, t.AppendPresets(nil,
|
||||
Zstd,
|
||||
EarlyInit,
|
||||
GenInitCPIO,
|
||||
), nil, nil, `
|
||||
return t.New("initramfs", TNoToolchain, []pkg.Artifact{
|
||||
t.Load(Zstd),
|
||||
t.Load(EarlyInit),
|
||||
t.Load(GenInitCPIO),
|
||||
}, nil, nil, `
|
||||
gen_init_cpio -t 4294967295 -c /usr/src/initramfs | zstd > /work/initramfs.zst
|
||||
`, pkg.Path(AbsUsrSrc.Append("initramfs"), false, pkg.NewFile("initramfs", []byte(`
|
||||
dir /dev 0755 0 0
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
# Automatically generated file; DO NOT EDIT.
|
||||
# Linux/x86 6.12.76 Kernel Configuration
|
||||
#
|
||||
CONFIG_CC_VERSION_TEXT="clang version 22.1.1"
|
||||
CONFIG_CC_VERSION_TEXT="clang version 22.1.0"
|
||||
CONFIG_GCC_VERSION=0
|
||||
CONFIG_CC_IS_CLANG=y
|
||||
CONFIG_CLANG_VERSION=220101
|
||||
CONFIG_CLANG_VERSION=220100
|
||||
CONFIG_AS_IS_LLVM=y
|
||||
CONFIG_AS_VERSION=220101
|
||||
CONFIG_AS_VERSION=220100
|
||||
CONFIG_LD_VERSION=0
|
||||
CONFIG_LD_IS_LLD=y
|
||||
CONFIG_LLD_VERSION=220101
|
||||
CONFIG_LLD_VERSION=220100
|
||||
CONFIG_RUSTC_VERSION=0
|
||||
CONFIG_RUSTC_LLVM_VERSION=0
|
||||
CONFIG_CC_HAS_ASM_GOTO_OUTPUT=y
|
||||
@@ -2402,7 +2402,7 @@ CONFIG_PREVENT_FIRMWARE_BUILD=y
|
||||
#
|
||||
# Firmware loader
|
||||
#
|
||||
CONFIG_FW_LOADER=y
|
||||
CONFIG_FW_LOADER=m
|
||||
CONFIG_FW_LOADER_DEBUG=y
|
||||
CONFIG_FW_LOADER_PAGED_BUF=y
|
||||
CONFIG_FW_LOADER_SYSFS=y
|
||||
@@ -2749,7 +2749,7 @@ CONFIG_BLK_DEV_NULL_BLK=m
|
||||
CONFIG_BLK_DEV_FD=m
|
||||
# CONFIG_BLK_DEV_FD_RAWCMD is not set
|
||||
CONFIG_CDROM=m
|
||||
CONFIG_BLK_DEV_PCIESSD_MTIP32XX=y
|
||||
CONFIG_BLK_DEV_PCIESSD_MTIP32XX=m
|
||||
CONFIG_ZRAM=m
|
||||
# CONFIG_ZRAM_BACKEND_LZ4 is not set
|
||||
# CONFIG_ZRAM_BACKEND_LZ4HC is not set
|
||||
@@ -2775,9 +2775,9 @@ CONFIG_CDROM_PKTCDVD=m
|
||||
CONFIG_CDROM_PKTCDVD_BUFFERS=8
|
||||
# CONFIG_CDROM_PKTCDVD_WCACHE is not set
|
||||
CONFIG_ATA_OVER_ETH=m
|
||||
CONFIG_XEN_BLKDEV_FRONTEND=y
|
||||
# CONFIG_XEN_BLKDEV_BACKEND is not set
|
||||
CONFIG_VIRTIO_BLK=y
|
||||
CONFIG_XEN_BLKDEV_FRONTEND=m
|
||||
CONFIG_XEN_BLKDEV_BACKEND=m
|
||||
CONFIG_VIRTIO_BLK=m
|
||||
CONFIG_BLK_DEV_RBD=m
|
||||
CONFIG_BLK_DEV_UBLK=m
|
||||
CONFIG_BLKDEV_UBLK_LEGACY_OPCODES=y
|
||||
@@ -2788,12 +2788,13 @@ CONFIG_BLK_DEV_RNBD_SERVER=m
|
||||
#
|
||||
# NVME Support
|
||||
#
|
||||
CONFIG_NVME_KEYRING=y
|
||||
CONFIG_NVME_AUTH=y
|
||||
CONFIG_NVME_CORE=y
|
||||
CONFIG_BLK_DEV_NVME=y
|
||||
CONFIG_NVME_KEYRING=m
|
||||
CONFIG_NVME_AUTH=m
|
||||
CONFIG_NVME_CORE=m
|
||||
CONFIG_BLK_DEV_NVME=m
|
||||
CONFIG_NVME_MULTIPATH=y
|
||||
# CONFIG_NVME_VERBOSE_ERRORS is not set
|
||||
CONFIG_NVME_HWMON=y
|
||||
CONFIG_NVME_FABRICS=m
|
||||
CONFIG_NVME_RDMA=m
|
||||
CONFIG_NVME_FC=m
|
||||
@@ -2910,10 +2911,10 @@ CONFIG_KEBA_CP500=m
|
||||
#
|
||||
# SCSI device support
|
||||
#
|
||||
CONFIG_SCSI_MOD=y
|
||||
CONFIG_SCSI_MOD=m
|
||||
CONFIG_RAID_ATTRS=m
|
||||
CONFIG_SCSI_COMMON=y
|
||||
CONFIG_SCSI=y
|
||||
CONFIG_SCSI_COMMON=m
|
||||
CONFIG_SCSI=m
|
||||
CONFIG_SCSI_DMA=y
|
||||
CONFIG_SCSI_NETLINK=y
|
||||
CONFIG_SCSI_PROC_FS=y
|
||||
@@ -2921,7 +2922,7 @@ CONFIG_SCSI_PROC_FS=y
|
||||
#
|
||||
# SCSI support type (disk, tape, CD-ROM)
|
||||
#
|
||||
CONFIG_BLK_DEV_SD=y
|
||||
CONFIG_BLK_DEV_SD=m
|
||||
CONFIG_CHR_DEV_ST=m
|
||||
CONFIG_BLK_DEV_SR=m
|
||||
CONFIG_CHR_DEV_SG=m
|
||||
@@ -3041,7 +3042,7 @@ CONFIG_SCSI_DEBUG=m
|
||||
CONFIG_SCSI_PMCRAID=m
|
||||
CONFIG_SCSI_PM8001=m
|
||||
CONFIG_SCSI_BFA_FC=m
|
||||
CONFIG_SCSI_VIRTIO=y
|
||||
CONFIG_SCSI_VIRTIO=m
|
||||
CONFIG_SCSI_CHELSIO_FCOE=m
|
||||
CONFIG_SCSI_LOWLEVEL_PCMCIA=y
|
||||
CONFIG_PCMCIA_AHA152X=m
|
||||
@@ -3051,7 +3052,7 @@ CONFIG_PCMCIA_SYM53C500=m
|
||||
# CONFIG_SCSI_DH is not set
|
||||
# end of SCSI device support
|
||||
|
||||
CONFIG_ATA=y
|
||||
CONFIG_ATA=m
|
||||
CONFIG_SATA_HOST=y
|
||||
CONFIG_PATA_TIMINGS=y
|
||||
CONFIG_ATA_VERBOSE_ERROR=y
|
||||
@@ -3063,39 +3064,39 @@ CONFIG_SATA_PMP=y
|
||||
#
|
||||
# Controllers with non-SFF native interface
|
||||
#
|
||||
CONFIG_SATA_AHCI=y
|
||||
CONFIG_SATA_AHCI=m
|
||||
CONFIG_SATA_MOBILE_LPM_POLICY=3
|
||||
CONFIG_SATA_AHCI_PLATFORM=y
|
||||
CONFIG_AHCI_DWC=y
|
||||
CONFIG_AHCI_CEVA=y
|
||||
CONFIG_SATA_AHCI_PLATFORM=m
|
||||
CONFIG_AHCI_DWC=m
|
||||
CONFIG_AHCI_CEVA=m
|
||||
CONFIG_SATA_INIC162X=m
|
||||
CONFIG_SATA_ACARD_AHCI=y
|
||||
CONFIG_SATA_SIL24=y
|
||||
CONFIG_SATA_ACARD_AHCI=m
|
||||
CONFIG_SATA_SIL24=m
|
||||
CONFIG_ATA_SFF=y
|
||||
|
||||
#
|
||||
# SFF controllers with custom DMA interface
|
||||
#
|
||||
CONFIG_PDC_ADMA=y
|
||||
CONFIG_SATA_QSTOR=y
|
||||
CONFIG_PDC_ADMA=m
|
||||
CONFIG_SATA_QSTOR=m
|
||||
CONFIG_SATA_SX4=m
|
||||
CONFIG_ATA_BMDMA=y
|
||||
|
||||
#
|
||||
# SATA SFF controllers with BMDMA
|
||||
#
|
||||
CONFIG_ATA_PIIX=y
|
||||
CONFIG_SATA_DWC=y
|
||||
CONFIG_ATA_PIIX=m
|
||||
CONFIG_SATA_DWC=m
|
||||
# CONFIG_SATA_DWC_OLD_DMA is not set
|
||||
CONFIG_SATA_MV=y
|
||||
CONFIG_SATA_NV=y
|
||||
CONFIG_SATA_PROMISE=y
|
||||
CONFIG_SATA_SIL=y
|
||||
CONFIG_SATA_SIS=y
|
||||
CONFIG_SATA_SVW=y
|
||||
CONFIG_SATA_ULI=y
|
||||
CONFIG_SATA_VIA=y
|
||||
CONFIG_SATA_VITESSE=y
|
||||
CONFIG_SATA_MV=m
|
||||
CONFIG_SATA_NV=m
|
||||
CONFIG_SATA_PROMISE=m
|
||||
CONFIG_SATA_SIL=m
|
||||
CONFIG_SATA_SIS=m
|
||||
CONFIG_SATA_SVW=m
|
||||
CONFIG_SATA_ULI=m
|
||||
CONFIG_SATA_VIA=m
|
||||
CONFIG_SATA_VITESSE=m
|
||||
|
||||
#
|
||||
# PATA SFF controllers with BMDMA
|
||||
@@ -3129,7 +3130,7 @@ CONFIG_PATA_RDC=m
|
||||
CONFIG_PATA_SCH=m
|
||||
CONFIG_PATA_SERVERWORKS=m
|
||||
CONFIG_PATA_SIL680=m
|
||||
CONFIG_PATA_SIS=y
|
||||
CONFIG_PATA_SIS=m
|
||||
CONFIG_PATA_TOSHIBA=m
|
||||
CONFIG_PATA_TRIFLEX=m
|
||||
CONFIG_PATA_VIA=m
|
||||
@@ -3171,8 +3172,8 @@ CONFIG_PATA_PARPORT_ON26=m
|
||||
#
|
||||
# Generic fallback / legacy drivers
|
||||
#
|
||||
CONFIG_PATA_ACPI=y
|
||||
CONFIG_ATA_GENERIC=y
|
||||
CONFIG_PATA_ACPI=m
|
||||
CONFIG_ATA_GENERIC=m
|
||||
CONFIG_PATA_LEGACY=m
|
||||
CONFIG_MD=y
|
||||
CONFIG_BLK_DEV_MD=m
|
||||
@@ -9620,11 +9621,11 @@ CONFIG_EFI_SECRET=m
|
||||
CONFIG_SEV_GUEST=m
|
||||
CONFIG_TDX_GUEST_DRIVER=m
|
||||
CONFIG_VIRTIO_ANCHOR=y
|
||||
CONFIG_VIRTIO=y
|
||||
CONFIG_VIRTIO_PCI_LIB=y
|
||||
CONFIG_VIRTIO_PCI_LIB_LEGACY=y
|
||||
CONFIG_VIRTIO=m
|
||||
CONFIG_VIRTIO_PCI_LIB=m
|
||||
CONFIG_VIRTIO_PCI_LIB_LEGACY=m
|
||||
CONFIG_VIRTIO_MENU=y
|
||||
CONFIG_VIRTIO_PCI=y
|
||||
CONFIG_VIRTIO_PCI=m
|
||||
CONFIG_VIRTIO_PCI_ADMIN_LEGACY=y
|
||||
CONFIG_VIRTIO_PCI_LEGACY=y
|
||||
CONFIG_VIRTIO_VDPA=m
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
# Automatically generated file; DO NOT EDIT.
|
||||
# Linux/arm64 6.12.76 Kernel Configuration
|
||||
#
|
||||
CONFIG_CC_VERSION_TEXT="clang version 22.1.1"
|
||||
CONFIG_CC_VERSION_TEXT="clang version 22.1.0"
|
||||
CONFIG_GCC_VERSION=0
|
||||
CONFIG_CC_IS_CLANG=y
|
||||
CONFIG_CLANG_VERSION=220101
|
||||
CONFIG_CLANG_VERSION=220100
|
||||
CONFIG_AS_IS_LLVM=y
|
||||
CONFIG_AS_VERSION=220101
|
||||
CONFIG_AS_VERSION=220100
|
||||
CONFIG_LD_VERSION=0
|
||||
CONFIG_LD_IS_LLD=y
|
||||
CONFIG_LLD_VERSION=220101
|
||||
CONFIG_LLD_VERSION=220100
|
||||
CONFIG_RUSTC_VERSION=0
|
||||
CONFIG_RUSTC_LLVM_VERSION=0
|
||||
CONFIG_CC_HAS_ASM_GOTO_OUTPUT=y
|
||||
@@ -2384,7 +2384,7 @@ CONFIG_PREVENT_FIRMWARE_BUILD=y
|
||||
#
|
||||
# Firmware loader
|
||||
#
|
||||
CONFIG_FW_LOADER=y
|
||||
CONFIG_FW_LOADER=m
|
||||
CONFIG_FW_LOADER_DEBUG=y
|
||||
CONFIG_FW_LOADER_PAGED_BUF=y
|
||||
CONFIG_FW_LOADER_SYSFS=y
|
||||
@@ -2849,8 +2849,8 @@ CONFIG_CDROM_PKTCDVD=m
|
||||
CONFIG_CDROM_PKTCDVD_BUFFERS=8
|
||||
# CONFIG_CDROM_PKTCDVD_WCACHE is not set
|
||||
CONFIG_ATA_OVER_ETH=m
|
||||
CONFIG_XEN_BLKDEV_FRONTEND=y
|
||||
# CONFIG_XEN_BLKDEV_BACKEND is not set
|
||||
CONFIG_XEN_BLKDEV_FRONTEND=m
|
||||
CONFIG_XEN_BLKDEV_BACKEND=m
|
||||
CONFIG_VIRTIO_BLK=m
|
||||
CONFIG_BLK_DEV_RBD=m
|
||||
CONFIG_BLK_DEV_UBLK=m
|
||||
@@ -2862,12 +2862,13 @@ CONFIG_BLK_DEV_RNBD_SERVER=m
|
||||
#
|
||||
# NVME Support
|
||||
#
|
||||
CONFIG_NVME_KEYRING=y
|
||||
CONFIG_NVME_AUTH=y
|
||||
CONFIG_NVME_CORE=y
|
||||
CONFIG_BLK_DEV_NVME=y
|
||||
CONFIG_NVME_KEYRING=m
|
||||
CONFIG_NVME_AUTH=m
|
||||
CONFIG_NVME_CORE=m
|
||||
CONFIG_BLK_DEV_NVME=m
|
||||
CONFIG_NVME_MULTIPATH=y
|
||||
# CONFIG_NVME_VERBOSE_ERRORS is not set
|
||||
CONFIG_NVME_HWMON=y
|
||||
CONFIG_NVME_FABRICS=m
|
||||
CONFIG_NVME_RDMA=m
|
||||
CONFIG_NVME_FC=m
|
||||
@@ -2976,10 +2977,10 @@ CONFIG_KEBA_CP500=m
|
||||
#
|
||||
# SCSI device support
|
||||
#
|
||||
CONFIG_SCSI_MOD=y
|
||||
CONFIG_SCSI_MOD=m
|
||||
CONFIG_RAID_ATTRS=m
|
||||
CONFIG_SCSI_COMMON=y
|
||||
CONFIG_SCSI=y
|
||||
CONFIG_SCSI_COMMON=m
|
||||
CONFIG_SCSI=m
|
||||
CONFIG_SCSI_DMA=y
|
||||
CONFIG_SCSI_NETLINK=y
|
||||
CONFIG_SCSI_PROC_FS=y
|
||||
@@ -2987,7 +2988,7 @@ CONFIG_SCSI_PROC_FS=y
|
||||
#
|
||||
# SCSI support type (disk, tape, CD-ROM)
|
||||
#
|
||||
CONFIG_BLK_DEV_SD=y
|
||||
CONFIG_BLK_DEV_SD=m
|
||||
CONFIG_CHR_DEV_ST=m
|
||||
CONFIG_BLK_DEV_SR=m
|
||||
CONFIG_CHR_DEV_SG=m
|
||||
@@ -3107,7 +3108,7 @@ CONFIG_SCSI_DEBUG=m
|
||||
CONFIG_SCSI_PMCRAID=m
|
||||
CONFIG_SCSI_PM8001=m
|
||||
CONFIG_SCSI_BFA_FC=m
|
||||
CONFIG_SCSI_VIRTIO=y
|
||||
CONFIG_SCSI_VIRTIO=m
|
||||
CONFIG_SCSI_CHELSIO_FCOE=m
|
||||
CONFIG_SCSI_LOWLEVEL_PCMCIA=y
|
||||
CONFIG_PCMCIA_AHA152X=m
|
||||
@@ -3117,7 +3118,7 @@ CONFIG_PCMCIA_SYM53C500=m
|
||||
# CONFIG_SCSI_DH is not set
|
||||
# end of SCSI device support
|
||||
|
||||
CONFIG_ATA=y
|
||||
CONFIG_ATA=m
|
||||
CONFIG_SATA_HOST=y
|
||||
CONFIG_PATA_TIMINGS=y
|
||||
CONFIG_ATA_VERBOSE_ERROR=y
|
||||
@@ -3129,23 +3130,23 @@ CONFIG_SATA_PMP=y
|
||||
#
|
||||
# Controllers with non-SFF native interface
|
||||
#
|
||||
CONFIG_SATA_AHCI=y
|
||||
CONFIG_SATA_AHCI=m
|
||||
CONFIG_SATA_MOBILE_LPM_POLICY=3
|
||||
CONFIG_SATA_AHCI_PLATFORM=y
|
||||
CONFIG_AHCI_BRCM=y
|
||||
CONFIG_AHCI_DWC=y
|
||||
CONFIG_SATA_AHCI_PLATFORM=m
|
||||
CONFIG_AHCI_BRCM=m
|
||||
CONFIG_AHCI_DWC=m
|
||||
CONFIG_AHCI_IMX=m
|
||||
CONFIG_AHCI_CEVA=y
|
||||
CONFIG_AHCI_MTK=y
|
||||
CONFIG_AHCI_MVEBU=y
|
||||
CONFIG_AHCI_SUNXI=y
|
||||
CONFIG_AHCI_TEGRA=y
|
||||
CONFIG_AHCI_CEVA=m
|
||||
CONFIG_AHCI_MTK=m
|
||||
CONFIG_AHCI_MVEBU=m
|
||||
CONFIG_AHCI_SUNXI=m
|
||||
CONFIG_AHCI_TEGRA=m
|
||||
CONFIG_AHCI_XGENE=m
|
||||
CONFIG_AHCI_QORIQ=y
|
||||
CONFIG_SATA_AHCI_SEATTLE=y
|
||||
CONFIG_AHCI_QORIQ=m
|
||||
CONFIG_SATA_AHCI_SEATTLE=m
|
||||
CONFIG_SATA_INIC162X=m
|
||||
CONFIG_SATA_ACARD_AHCI=y
|
||||
CONFIG_SATA_SIL24=y
|
||||
CONFIG_SATA_ACARD_AHCI=m
|
||||
CONFIG_SATA_SIL24=m
|
||||
CONFIG_ATA_SFF=y
|
||||
|
||||
#
|
||||
@@ -3159,19 +3160,19 @@ CONFIG_ATA_BMDMA=y
|
||||
#
|
||||
# SATA SFF controllers with BMDMA
|
||||
#
|
||||
CONFIG_ATA_PIIX=y
|
||||
CONFIG_SATA_DWC=y
|
||||
CONFIG_ATA_PIIX=m
|
||||
CONFIG_SATA_DWC=m
|
||||
# CONFIG_SATA_DWC_OLD_DMA is not set
|
||||
CONFIG_SATA_MV=y
|
||||
CONFIG_SATA_NV=y
|
||||
CONFIG_SATA_PROMISE=y
|
||||
CONFIG_SATA_RCAR=y
|
||||
CONFIG_SATA_SIL=y
|
||||
CONFIG_SATA_SIS=y
|
||||
CONFIG_SATA_SVW=y
|
||||
CONFIG_SATA_ULI=y
|
||||
CONFIG_SATA_VIA=y
|
||||
CONFIG_SATA_VITESSE=y
|
||||
CONFIG_SATA_MV=m
|
||||
CONFIG_SATA_NV=m
|
||||
CONFIG_SATA_PROMISE=m
|
||||
CONFIG_SATA_RCAR=m
|
||||
CONFIG_SATA_SIL=m
|
||||
CONFIG_SATA_SIS=m
|
||||
CONFIG_SATA_SVW=m
|
||||
CONFIG_SATA_ULI=m
|
||||
CONFIG_SATA_VIA=m
|
||||
CONFIG_SATA_VITESSE=m
|
||||
|
||||
#
|
||||
# PATA SFF controllers with BMDMA
|
||||
@@ -3206,7 +3207,7 @@ CONFIG_PATA_RDC=m
|
||||
CONFIG_PATA_SCH=m
|
||||
CONFIG_PATA_SERVERWORKS=m
|
||||
CONFIG_PATA_SIL680=m
|
||||
CONFIG_PATA_SIS=y
|
||||
CONFIG_PATA_SIS=m
|
||||
CONFIG_PATA_TOSHIBA=m
|
||||
CONFIG_PATA_TRIFLEX=m
|
||||
CONFIG_PATA_VIA=m
|
||||
@@ -3248,8 +3249,8 @@ CONFIG_PATA_PARPORT_ON26=m
|
||||
#
|
||||
# Generic fallback / legacy drivers
|
||||
#
|
||||
CONFIG_PATA_ACPI=y
|
||||
CONFIG_ATA_GENERIC=y
|
||||
CONFIG_PATA_ACPI=m
|
||||
CONFIG_ATA_GENERIC=m
|
||||
CONFIG_PATA_LEGACY=m
|
||||
CONFIG_MD=y
|
||||
CONFIG_BLK_DEV_MD=m
|
||||
@@ -10435,11 +10436,11 @@ CONFIG_VMGENID=m
|
||||
CONFIG_NITRO_ENCLAVES=m
|
||||
CONFIG_ARM_PKVM_GUEST=y
|
||||
CONFIG_VIRTIO_ANCHOR=y
|
||||
CONFIG_VIRTIO=y
|
||||
CONFIG_VIRTIO_PCI_LIB=y
|
||||
CONFIG_VIRTIO_PCI_LIB_LEGACY=y
|
||||
CONFIG_VIRTIO=m
|
||||
CONFIG_VIRTIO_PCI_LIB=m
|
||||
CONFIG_VIRTIO_PCI_LIB_LEGACY=m
|
||||
CONFIG_VIRTIO_MENU=y
|
||||
CONFIG_VIRTIO_PCI=y
|
||||
CONFIG_VIRTIO_PCI=m
|
||||
CONFIG_VIRTIO_PCI_LEGACY=y
|
||||
CONFIG_VIRTIO_VDPA=m
|
||||
CONFIG_VIRTIO_PMEM=m
|
||||
|
||||
@@ -73,8 +73,14 @@ func llvmFlagName(flag int) string {
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
llvmVersionMajor = "22"
|
||||
llvmVersion = llvmVersionMajor + ".1.1"
|
||||
)
|
||||
|
||||
// newLLVMVariant returns a [pkg.Artifact] containing a LLVM variant.
|
||||
func (t Toolchain) newLLVMVariant(variant string, attr *llvmAttr) pkg.Artifact {
|
||||
const checksum = "bQvV6D8AZvQykg7-uQb_saTbVavnSo1ykNJ3g57F5iE-evU3HuOYtcRnVIXTK76e"
|
||||
|
||||
if attr == nil {
|
||||
panic("LLVM attr must be non-nil")
|
||||
@@ -163,7 +169,7 @@ ln -s ld.lld /work/system/bin/ld
|
||||
return t.NewPackage("llvm", llvmVersion, pkg.NewHTTPGetTar(
|
||||
nil, "https://github.com/llvm/llvm-project/archive/refs/tags/"+
|
||||
"llvmorg-"+llvmVersion+".tar.gz",
|
||||
mustDecode(llvmChecksum),
|
||||
mustDecode(checksum),
|
||||
pkg.TarGzip,
|
||||
), &PackageAttr{
|
||||
Patches: attr.patches,
|
||||
@@ -310,7 +316,7 @@ ln -s clang++ /work/system/bin/c++
|
||||
ninja check-all
|
||||
`,
|
||||
|
||||
patches: slices.Concat([][2]string{
|
||||
patches: [][2]string{
|
||||
{"add-rosa-vendor", `diff --git a/llvm/include/llvm/TargetParser/Triple.h b/llvm/include/llvm/TargetParser/Triple.h
|
||||
index 9c83abeeb3b1..5acfe5836a23 100644
|
||||
--- a/llvm/include/llvm/TargetParser/Triple.h
|
||||
@@ -482,7 +488,7 @@ index 64324a3f8b01..15ce70b68217 100644
|
||||
"/System/Library/Frameworks"};
|
||||
|
||||
`},
|
||||
}, clangPatches),
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
package rosa
|
||||
|
||||
// clangPatches are patches applied to the LLVM source tree for building clang.
|
||||
var clangPatches [][2]string
|
||||
@@ -1,12 +0,0 @@
|
||||
package rosa
|
||||
|
||||
// clangPatches are patches applied to the LLVM source tree for building clang.
|
||||
var clangPatches [][2]string
|
||||
|
||||
// one version behind, latest fails 5 tests with 2 flaky on arm64
|
||||
const (
|
||||
llvmVersionMajor = "21"
|
||||
llvmVersion = llvmVersionMajor + ".1.8"
|
||||
|
||||
llvmChecksum = "8SUpqDkcgwOPsqHVtmf9kXfFeVmjVxl4LMn-qSE1AI_Xoeju-9HaoPNGtidyxyka"
|
||||
)
|
||||
@@ -1,11 +0,0 @@
|
||||
//go:build !arm64
|
||||
|
||||
package rosa
|
||||
|
||||
// latest version of LLVM, conditional to temporarily avoid broken new releases
|
||||
const (
|
||||
llvmVersionMajor = "22"
|
||||
llvmVersion = llvmVersionMajor + ".1.1"
|
||||
|
||||
llvmChecksum = "bQvV6D8AZvQykg7-uQb_saTbVavnSo1ykNJ3g57F5iE-evU3HuOYtcRnVIXTK76e"
|
||||
)
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
buildGo126Module rec {
|
||||
pname = "hakurei";
|
||||
version = "0.3.7";
|
||||
version = "0.3.6";
|
||||
|
||||
srcFiltered = builtins.path {
|
||||
name = "${pname}-src";
|
||||
|
||||
Reference in New Issue
Block a user