aboutsummaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorXe Iaso <me@xeiaso.net>2024-04-20 13:12:56 -0400
committerXe Iaso <me@xeiaso.net>2024-04-20 13:14:24 -0400
commit898fd37fa6698f3ba9630375fc14e8d178f66316 (patch)
treee6bff0445f49d5ad4ddfd4a745f007ae27a3c01a /cmd
parentb8d747ba70f04b71c659dcd684ff0af091f1bbe6 (diff)
downloadx-898fd37fa6698f3ba9630375fc14e8d178f66316.tar.xz
x-898fd37fa6698f3ba9630375fc14e8d178f66316.zip
cmd/yeet: add RPM building and README
Signed-off-by: Xe Iaso <me@xeiaso.net>
Diffstat (limited to 'cmd')
-rw-r--r--cmd/tshello/main.go52
-rw-r--r--cmd/yeet/.gitignore1
-rw-r--r--cmd/yeet/README.md380
-rw-r--r--cmd/yeet/internal/mkrpm/mkrpm.go173
-rw-r--r--cmd/yeet/main.go81
-rw-r--r--cmd/yeet/yeetfile.js13
6 files changed, 629 insertions, 71 deletions
diff --git a/cmd/tshello/main.go b/cmd/tshello/main.go
deleted file mode 100644
index 59c688f..0000000
--- a/cmd/tshello/main.go
+++ /dev/null
@@ -1,52 +0,0 @@
-// The tshello server demonstrates how to use Tailscale as a library.
-package main
-
-import (
- "flag"
- "fmt"
- "html"
- "log"
- "net/http"
- "strings"
-
- "tailscale.com/tsnet"
-)
-
-var (
- addr = flag.String("addr", ":80", "address to listen on")
- hostname = flag.String("hostname", "tshello", "hostname to use on the tailnet")
-)
-
-func main() {
- flag.Parse()
- s := new(tsnet.Server)
- defer s.Close()
- ln, err := s.Listen("tcp", *addr)
- if err != nil {
- log.Fatal(err)
- }
- defer ln.Close()
-
- lc, err := s.LocalClient()
- if err != nil {
- log.Fatal(err)
- }
-
- log.Fatal(http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- who, err := lc.WhoIs(r.Context(), r.RemoteAddr)
- if err != nil {
- http.Error(w, err.Error(), 500)
- return
- }
- fmt.Fprintf(w, "<html><body><h1>Hello, world!</h1>\n")
- fmt.Fprintf(w, "<p>You are <b>%s</b> from <b>%s</b> (%s)</p>",
- html.EscapeString(who.UserProfile.LoginName),
- html.EscapeString(firstLabel(who.Node.ComputedName)),
- r.RemoteAddr)
- })))
-}
-
-func firstLabel(s string) string {
- s, _, _ = strings.Cut(s, ".")
- return s
-}
diff --git a/cmd/yeet/.gitignore b/cmd/yeet/.gitignore
new file mode 100644
index 0000000..e7a9c13
--- /dev/null
+++ b/cmd/yeet/.gitignore
@@ -0,0 +1 @@
+*.rpm
diff --git a/cmd/yeet/README.md b/cmd/yeet/README.md
new file mode 100644
index 0000000..4e61d6f
--- /dev/null
+++ b/cmd/yeet/README.md
@@ -0,0 +1,380 @@
+# yeet
+
+Yeet out actions with maximum haste! Declare your build instructions as small JavaScript snippets and let er rip!
+
+## Usage
+
+To install the current program with `go install`:
+
+```js
+// yeetfile.js
+go.install();
+```
+
+## Available functions
+
+Yeet uses [goja](https://pkg.go.dev/github.com/dop251/goja#section-readme) to execute JavaScript. As such, it does not have access to NPM or other external JavaScript libraries. You also cannot import code/data from other files. These are not planned for inclusion into yeet. If functionality is required, it should be added to yeet itself.
+
+To make it useful, yeet exposes a bunch of helper objects full of tools. These tools fall in a few categories, each has its own section.
+
+### `docker`
+
+Aliases for `docker` commands.
+
+#### `docker.build`
+
+An alias for the `docker build` command. Builds a docker image in the current working directory's Dockerfile.
+
+Usage:
+
+`docker.build(tag);`
+
+```js
+docker.build("ghcr.io/xe/site/bin");
+docker.push("ghcr.io/xe/site/bin");
+```
+
+#### `docker.load`
+
+Loads an exported docker image by path into the local docker daemon. This is most useful when combined with tools like `nix.build`.
+
+Usage:
+
+`docker.load(path)`
+
+```js
+nix.build(".#docker.xedn");
+docker.load("./result");
+docker.push("registry.fly.io/xedn:latest");
+fly.deploy();
+```
+
+#### `docker.push`
+
+Pushes a docker image to a registry. Analogous to `docker push` in the CLI.
+
+Usage:
+
+`docker.push(tag);`
+
+```js
+docker.build("ghcr.io/xe/site/bin");
+docker.push("ghcr.io/xe/site/bin");
+```
+
+### `file`
+
+Helpers for filesystem access.
+
+#### `file.copy`
+
+Copies a file from one place to another. Analogous to the `cp` command on Linux. Automatically creates directories in the `dest` path if they don't exist.
+
+Usage:
+
+`file.copy(src, dest);`
+
+```js
+file.copy("LICENSE", `${out}/usr/share/doc/LICENSE`);
+```
+
+#### `file.read`
+
+Reads a file into memory and returns it as a string.
+
+Usage:
+
+`file.read(path);`
+
+```js
+const version = file.read("VERSION");
+```
+
+#### `file.write`
+
+Writes the contents of the `data` string to a file with mode `0660`.
+
+Usage:
+
+`file.write(path, data);`
+
+```js
+file.write("VERSION", git.tag());
+```
+
+### `fly`
+
+Automation for [flyctl](https://github.com/superfly/flyctl). Soon this will also let you manage Machines with the [Machines API](https://docs.machines.dev).
+
+#### `fly.deploy`
+
+Runs the `fly deploy` command for you.
+
+Usage:
+
+`fly.deploy();`
+
+```js
+docker.build("registry.fly.io/foobar");
+docker.push("registry.fly.io/foobar");
+fly.deploy();
+```
+
+### `git`
+
+Helpers for the Git version control system.
+
+#### `git.repoRoot`
+
+Returns the repository root as a string.
+
+`git.repoRoot();`
+
+```js
+const repoRoot = git.repoRoot();
+
+file.copy(`${repoRoot}/LICENSE`, `${out}/usr/share/doc/yeet/LICENSE`);
+```
+
+#### `git.tag`
+
+Returns the output of `git describe --tags`. Useful for getting the "current version" of the repo, where the current version will likely be different forward in time than it is backwards in time.
+
+Usage:
+
+`git.tag();`
+
+```js
+const version = git.tag();
+```
+
+### `go`
+
+Helpers for the Go programming language.
+
+#### `go.build`
+
+Runs `go build` in the current working directory with any extra arguments passed in. This is useful for building and installing Go programs in an RPM build context.
+
+Usage:
+
+`go.build(args);`
+
+```js
+go.build("-o", `${out}/usr/bin/`);
+```
+
+#### `go.install`
+
+Runs `go install`. Not useful for cross-compilation.
+
+Usage:
+
+`go.install();`
+
+```js
+go.install();
+```
+
+### `nix`
+
+Automation for running Nix ecosystem tooling.
+
+#### `nix.build`
+
+Runs `nix build` against a given flakeref.
+
+Usage:
+
+`nix.build(flakeref);`
+
+```js
+nix.build(".#docker");
+docker.load("./result");
+```
+
+#### `nix.eval`
+
+A tagged template that helps you build Nix expressions safely from JavaScript and then evaluates them. See my [nixexpr blogpost](https://xeiaso.net/blog/nixexpr/) for more information about how this works.
+
+Usage:
+
+```js
+const glibcPath = nix.eval`let pkgs = import <nixpkgs>; in pkgs.glibc`;
+```
+
+#### `nix.expr`
+
+A tagged template that helps you build Nix expressions safely from JavaScript. See my [nixexpr blogpost](https://xeiaso.net/blog/nixexpr/) for more information about how this works.
+
+Usage:
+
+```js
+go.build();
+const fname = slug.build("todayinmarch2020");
+
+const url = slug.push(fname);
+const hash = nix.hashURL(url);
+
+const expr = nix.expr`{ stdenv }:
+
+stdenv.mkDerivation {
+ name = "todayinmarch2020";
+ src = builtins.fetchurl {
+ url = ${url};
+ sha256 = ${hash};
+ };
+
+ phases = "installPhase";
+
+ installPhase = ''
+ tar xf $src
+ mkdir -p $out/bin
+ cp bin/main $out/bin/todayinmarch2020
+ '';
+}
+`;
+
+file.write(`${repoRoot}/pkgs/x/todayinmarch2020.nix`, expr);
+```
+
+#### `nix.hashURL`
+
+Hashes the contents of a given URL and returns the `sha256` SRI form. Useful when composing Nix expressions with the `nix.expr` tagged template.
+
+Usage:
+
+`nix.hashURL(url);`
+
+```js
+const hash = nix.hashURL("https://whatever.com/some_file.tgz");
+```
+
+### `rpm`
+
+Helpers for building RPM packages and docker images out of a constellation of RPM packages.
+
+#### `rpm.build`
+
+Builds an RPM package with a descriptor object. See the RPM packages section for more information. The important part of this is your `build` function. The `build` function is what will turn your package source code into an executable in `out` somehow. Everything in `out` corresponds 1:1 with paths in the resulting RPM.
+
+The resulting RPM path will be returned as a string.
+
+Usage:
+
+`rpm.build(package);`
+
+```js
+["amd64", "arm64"].forEach((goarch) =>
+ rpm.build({
+ name: "yeet",
+ description: "Yeet out actions with maximum haste!",
+ homepage: "https://within.website",
+ license: "CC0",
+ goarch,
+
+ build: (out) => {
+ go.build("-o", `${out}/usr/bin/`);
+ },
+ })
+);
+```
+
+### `yeet`
+
+This contains various "other" functions that don't have a good place to put them.
+
+#### `yeet.cwd`
+
+The current working directory. This is a constant value and is not updated at runtime.
+
+Usage:
+
+```js
+log.println(yeet.cwd);
+```
+
+#### `yeet.dateTag`
+
+A constant string representing the time that yeet was started in UTC. It is formatted in terms of `YYYYmmDDhhMM`. This is not updated at runtime. You can use it for a "unique" value per invocation of yeet (assuming you aren't a time traveler).
+
+Usage:
+
+```js
+docker.build(`ghcr.io/xe/site/bin:${git.tag()}-${yeet.dateTag}`);
+```
+
+#### `yeet.run` / `yeet.runcmd`
+
+Runs an arbitrary command and returns any output as a string.
+
+Usage:
+
+`yeet.run(cmd, arg1, arg2, ...);`
+
+```js
+yeet.run(
+ "protoc",
+ "--proto-path=.",
+ `--proto-path=${git.repoRoot()}/proto`,
+ "foo.proto"
+);
+```
+
+#### `yeet.setenv`
+
+Sets an environment variable for the process yeet is running in and all children.
+
+Usage:
+
+`yeet.setenv(key, val);`
+
+```js
+yeet.setenv("GOOS", "linux");
+```
+
+#### `yeet.goos` / `yeet.goarch`
+
+The GOOS/GOARCH value that yeet was built for. This typically corresponds with the OS and CPU architecture that yeet is running on.
+
+## Building RPM Packages
+
+When using the `rpm.build` function, you can create RPM packages from arbitrary yeet expressions. This allows you to create RPM packages from a macOS or other Linux system. As an example, here is how the yeet RPMs are built:
+
+```js
+["amd64", "arm64"].forEach((goarch) =>
+ rpm.build({
+ name: "yeet",
+ description: "Yeet out actions with maximum haste!",
+ homepage: "https://within.website",
+ license: "CC0",
+ goarch: goarch,
+
+ build: (out) => {
+ go.build("-o", `${out}/usr/bin/`);
+ },
+ })
+);
+```
+
+### Build settings
+
+The following settings are supported:
+
+| Name | Example | Description |
+| :------------ | :----------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------- |
+| `name` | `xeiaso.net-yeet` | The unique name of the package. |
+| `version` | `1.0.0` | The version of the package, if not set then it will be inferred from the git version. |
+| `description` | `Yeet out scripts with haste!` | The human-readable description of the package. |
+| `homepage` | `https://xeiaso.net` | The URL for the homepage of the package. |
+| `group` | `Network` | If set, the RPM group that this package belongs to. |
+| `license` | `MIT` | The license that the contents of this package is under. |
+| `goarch` | `amd64` / `arm64` | The GOARCH value corresponding to the architecture that the RPM is being built for. If you want to build a `noarch` package, put `any` here. |
+| `replaces` | `["foo", "bar"]` | Any packages that this package conflicts with or replaces. |
+| `depends` | `["foo", "bar"]` | Any packages that this package depends on (such as C libraries for CGo code). |
+| `emptyDirs` | `["/var/lib/yeet"]` | Any empty directories that should be created when the package is installed. |
+| `configFiles` | `{"./.env.example": "/var/lib/yeet/.env"}` | Any configuration files that should be copied over on install, but managed by administrators after installation. |
+
+## Support
+
+For support, please [subscribe to me on Patreon](https://patreon.com/cadey) and ask in the `#yeet` channel. You may open GitHub issues if you wish, but I do not often look at them.
diff --git a/cmd/yeet/internal/mkrpm/mkrpm.go b/cmd/yeet/internal/mkrpm/mkrpm.go
new file mode 100644
index 0000000..71c48fd
--- /dev/null
+++ b/cmd/yeet/internal/mkrpm/mkrpm.go
@@ -0,0 +1,173 @@
+package mkrpm
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "log/slog"
+ "os"
+ "path/filepath"
+ "runtime"
+
+ "github.com/Songmu/gitconfig"
+ "github.com/goreleaser/nfpm/v2"
+ "github.com/goreleaser/nfpm/v2/files"
+ _ "github.com/goreleaser/nfpm/v2/rpm"
+ "within.website/x/internal/yeet"
+)
+
+var (
+ userName = flag.String("git-user-name", gitUserName(), "user name in Git")
+ userEmail = flag.String("git-user-email", gitUserEmail(), "user email in Git")
+)
+
+const (
+ fallbackName = "Mimi Yasomi"
+ fallbackEmail = "mimi@xeserv.us"
+)
+
+func gitUserName() string {
+ name, err := gitconfig.User()
+ if err != nil {
+ return fallbackName
+ }
+
+ return name
+}
+
+func gitUserEmail() string {
+ email, err := gitconfig.Email()
+ if err != nil {
+ return fallbackEmail
+ }
+
+ return email
+}
+
+func gitVersion() string {
+ vers, err := yeet.GitTag(context.Background())
+ if err != nil {
+ panic(err)
+ }
+ return vers[1:]
+}
+
+type Package struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+ Description string `json:"description"`
+ Homepage string `json:"homepage"`
+ Group string `json:"group"`
+ License string `json:"license"`
+ Goarch string `json:"goarch"`
+ Replaces []string `json:"replaces"`
+ Depends []string `json:"depends"`
+ Recommends []string `json:"recommends"`
+
+ EmptyDirs []string `json:"emptyDirs"` // rpm destination path
+ ConfigFiles map[string]string `json:"configFiles"` // repo-relative source path, rpm destination path
+
+ Build func(out string) `json:"build"`
+}
+
+func Build(p Package) (foutpath string, err error) {
+ defer func() {
+ if r := recover(); r != nil {
+ switch r.(type) {
+ case error:
+ err = r.(error)
+ default:
+ err = fmt.Errorf("mkrpm: error while building: %v", r)
+ }
+ }
+ }()
+
+ if p.Version == "" {
+ p.Version = gitVersion()
+ }
+
+ dir, err := os.MkdirTemp("", "yeet-mkrpm")
+ if err != nil {
+ return "", fmt.Errorf("mkrpm: can't make temporary directory")
+ }
+ defer os.RemoveAll(dir)
+
+ defer func() {
+ os.Setenv("GOARCH", runtime.GOARCH)
+ os.Setenv("GOOS", runtime.GOOS)
+ }()
+ os.Setenv("GOARCH", p.Goarch)
+ os.Setenv("GOOS", "linux")
+
+ p.Build(dir)
+
+ var contents files.Contents
+
+ for _, d := range p.EmptyDirs {
+ if d == "" {
+ continue
+ }
+
+ contents = append(contents, &files.Content{Type: files.TypeDir, Destination: d})
+ }
+
+ for repoPath, rpmPath := range p.ConfigFiles {
+ contents = append(contents, &files.Content{Type: files.TypeConfig, Source: repoPath, Destination: rpmPath})
+ }
+
+ if err := filepath.Walk(dir, func(path string, stat os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if stat.IsDir() {
+ return nil
+ }
+
+ contents = append(contents, &files.Content{Type: files.TypeFile, Source: path, Destination: path[len(dir)+1:]})
+
+ return nil
+ }); err != nil {
+ return "", fmt.Errorf("mkrpm: can't walk output directory: %w", err)
+ }
+
+ info := nfpm.WithDefaults(&nfpm.Info{
+ Name: p.Name,
+ Version: p.Version,
+ Arch: p.Goarch,
+ Platform: "linux",
+ Description: p.Description,
+ Maintainer: fmt.Sprintf("%s <%s>", *userName, *userEmail),
+ Homepage: p.Homepage,
+ License: p.License,
+ Overridables: nfpm.Overridables{
+ Contents: contents,
+ Depends: p.Depends,
+ Recommends: p.Recommends,
+ Replaces: p.Replaces,
+ Conflicts: p.Replaces,
+ },
+ })
+
+ info.Overridables.RPM.Group = p.Group
+
+ pkg, err := nfpm.Get("rpm")
+ if err != nil {
+ return "", fmt.Errorf("mkrpm: can't get RPM packager: %w", err)
+ }
+
+ foutpath = fmt.Sprintf("%s-%s-%s.rpm", p.Name, p.Version, p.Goarch)
+ fout, err := os.Create(foutpath)
+ if err != nil {
+ return "", fmt.Errorf("mkrpm: can't create output file: %w", err)
+ }
+ defer fout.Close()
+
+ if err := pkg.Package(info, fout); err != nil {
+ return "", fmt.Errorf("mkrpm: can't build package: %w", err)
+ }
+
+ slog.Debug("built package", "name", p.Name, "version", p.Version, "path", foutpath)
+
+ return foutpath, err
+}
diff --git a/cmd/yeet/main.go b/cmd/yeet/main.go
index b143597..dd18d33 100644
--- a/cmd/yeet/main.go
+++ b/cmd/yeet/main.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"flag"
"fmt"
+ "io"
"log"
"log/slog"
"os"
@@ -15,6 +16,7 @@ import (
"strings"
"github.com/dop251/goja"
+ "within.website/x/cmd/yeet/internal/mkrpm"
"within.website/x/internal"
"within.website/x/internal/appsluggr"
"within.website/x/internal/kahless"
@@ -23,8 +25,8 @@ import (
)
var (
- fname = flag.String("fname", "yeetfile.js", "filename for the yeetfile")
- flyctl = flag.String("flyctl-path", flyctlPath(), "path to flyctl binary")
+ fname = flag.String("fname", "yeetfile.js", "filename for the yeetfile")
+ flyctl = flag.String("flyctl-path", flyctlPath(), "path to flyctl binary")
protocPath = flag.String("protoc-path", "protoc", "path to protoc binary")
)
@@ -46,7 +48,7 @@ func flyctlPath() string {
func runcmd(cmdName string, args ...string) string {
ctx := context.Background()
- slog.Info("running command", "cmd", cmdName, "args", args)
+ slog.Debug("running command", "cmd", cmdName, "args", args)
result, err := yeet.Output(ctx, cmdName, args...)
if err != nil {
@@ -74,10 +76,6 @@ func dockerload(fname string) {
yeet.DockerLoadResult(context.Background(), fname)
}
-func dockertag(org, repo, image string) string {
- return yeet.DockerTag(context.Background(), org, repo, image)
-}
-
func dockerbuild(tag string, args ...string) {
yeet.DockerBuild(context.Background(), yeet.WD, tag, args...)
}
@@ -150,11 +148,11 @@ func hostname() string {
}
type protocInput struct {
- Input string `json:"input"`
+ Input string `json:"input"`
Output string `json:"output"`
- Kinds []struct {
+ Kinds []struct {
Kind string `json:"kind"`
- Opt string `json:"opt"`
+ Opt string `json:"opt"`
} `json:"kinds"`
}
@@ -182,15 +180,52 @@ func main() {
"build": dockerbuild,
"load": dockerload,
"push": dockerpush,
- "tag": dockertag,
})
vm.Set("file", map[string]any{
+ "read": func(fname string) string {
+ data, err := os.ReadFile(fname)
+ if err != nil {
+ panic(err)
+ }
+ return string(data)
+ },
"write": func(fname, data string) {
if err := os.WriteFile(fname, []byte(data), 0660); err != nil {
panic(err)
}
},
+ "copy": func(from, to string) {
+ st, err := os.Stat(from)
+ if err != nil {
+ panic(err)
+ }
+
+ fin, err := os.Open(from)
+ if err != nil {
+ panic(err)
+ }
+ defer fin.Close()
+
+ dir := filepath.Dir(to)
+ os.MkdirAll(dir, 0777)
+
+ fout, err := os.OpenFile(to, os.O_CREATE, st.Mode())
+ if err != nil {
+ panic(err)
+ }
+ defer fout.Close()
+
+ n, err := io.Copy(fout, fin)
+ if err != nil {
+ panic(err)
+ }
+
+ if n != st.Size() {
+ slog.Error("wrong number of bytes written", "from", from, "to", to, "want", st.Size(), "got", n)
+ panic("copy failed")
+ }
+ },
})
vm.Set("fly", map[string]any{
@@ -201,17 +236,17 @@ func main() {
"repoRoot": func() string {
return runcmd("git", "rev-parse", "--show-toplevel")
},
+ "tag": gittag,
})
vm.Set("go", map[string]any{
- "build": func() { runcmd("go", "build") },
+ "build": func(args ...string) {
+ args = append([]string{"build"}, args...)
+ runcmd("go", args...)
+ },
"install": func() { runcmd("go", "install") },
})
- vm.Set("git", map[string]any{
- "tag": gittag,
- })
-
vm.Set("log", map[string]any{
"info": lg.Println,
"println": fmt.Println,
@@ -219,9 +254,19 @@ func main() {
vm.Set("nix", map[string]any{
"build": nixbuild,
- "hashURL": func(fileURL string) string { return strings.TrimSpace(runcmd("nix-prefetch-url", fileURL)) },
- "expr": buildNixExpr,
"eval": evalNixExpr,
+ "expr": buildNixExpr,
+ "hashURL": func(fileURL string) string { return strings.TrimSpace(runcmd("nix-prefetch-url", fileURL)) },
+ })
+
+ vm.Set("rpm", map[string]any{
+ "build": func(p mkrpm.Package) string {
+ foutpath, err := mkrpm.Build(p)
+ if err != nil {
+ panic(err)
+ }
+ return foutpath
+ },
})
vm.Set("slug", map[string]any{
diff --git a/cmd/yeet/yeetfile.js b/cmd/yeet/yeetfile.js
index 312fd4f..ebf7acf 100644
--- a/cmd/yeet/yeetfile.js
+++ b/cmd/yeet/yeetfile.js
@@ -1,2 +1,13 @@
-yeet.setenv("CGO_ENABLED", "0");
go.install();
+
+["amd64", "arm64"].forEach(goarch => rpm.build({
+ name: "yeet",
+ description: "Yeet out actions with maximum haste!",
+ homepage: "https://within.website",
+ license: "CC0",
+ goarch,
+
+ build: (out) => {
+ go.build("-o", `${out}/usr/bin/`);
+ },
+})); \ No newline at end of file