Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

20 changed files with 3 additions and 2444 deletions

1
.gitignore vendored
View File

@ -1,2 +1 @@
*.swp
vendor

41
Gopkg.lock generated
View File

@ -1,41 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = [
"cast5",
"openpgp",
"openpgp/armor",
"openpgp/elgamal",
"openpgp/errors",
"openpgp/packet",
"openpgp/s2k"
]
revision = "8c653846df49742c4c85ec37e5d9f8d3ba657895"
[[projects]]
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "7f97868eec74b32b0982dd158a51a446d1da7eb5"
version = "v2.1.1"
[[projects]]
name = "mcquay.me/fs"
packages = ["."]
revision = "d40468470ff3ca131b1eaf3d99ca05c2dd3bcdfc"
version = "v1.0.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "a4d32970fbd71fcaec77a9ad03f9bbe56005cd80f00fff4329b314a3458e8d1f"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -1,46 +0,0 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/pkg/errors"
version = "0.8.0"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"
[[constraint]]
name = "mcquay.me/fs"
version = "1.0.0"
[prune]
go-tests = true
unused-packages = true
[[constraint]]
name = "gopkg.in/yaml.v2"
version = "2.1.1"

View File

@ -1,10 +0,0 @@
default: lint test
.PHONY: lint
lint:
@golint $(shell go list mcquay.me/pm/...)
@go vet $(shell go list mcquay.me/pm/...)
.PHONY: test
test:
@go test -cover $(shell go list mcquay.me/pm/...)

View File

@ -49,6 +49,7 @@ following files:
```yaml
name: foo
version: 2.3.29
namespace: /darwin/amd64
description: Foo is the world's simplest frobnicator
deps: [baz, bar@0.9.2]
```
@ -87,8 +88,8 @@ The example remote url:
encodes a remote that is served over `https` on the host `pm.mcquay.me` and
informs the client to pull packages from the `/darwin/amd64/testing` namespace,
specified by the Path. `pm pull` will collect available package information
from configured remote and will populate its local database with the contents
of the response. `pm` can then list available packages, and the user can then
from the remote for a given namespace and will populate its local database with
the contents of the response. `pm` can then list available packages, and
request that they be installed.
As a practical example a client can be configured to pull from two `remotes`

View File

@ -1,187 +0,0 @@
package pm
import (
"fmt"
"net/url"
"sort"
"strings"
"github.com/pkg/errors"
)
// Name exists to document the keys in Available
type Name string
// Names is a slice of names ... with sorting!
type Names []Name
func (n Names) Len() int { return len(n) }
func (n Names) Swap(a, b int) { n[a], n[b] = n[b], n[a] }
func (n Names) Less(a, b int) bool { return n[a] < n[b] }
// Version exists to document the keys in Available
type Version string
// Versions is a slice of Version ... with sorting!
type Versions []Version
// TODO (sm): make this semver sort?
func (v Versions) Len() int { return len(v) }
func (v Versions) Swap(a, b int) { v[a], v[b] = v[b], v[a] }
func (v Versions) Less(a, b int) bool { return v[a] < v[b] }
type label struct {
n Name
v Version
}
type labels []label
// TODO (sm): make this semver sort?
func (n labels) Len() int { return len(n) }
func (n labels) Swap(a, b int) { n[a], n[b] = n[b], n[a] }
func (n labels) Less(a, b int) bool {
if n[a].n != n[b].n {
return n[a].n < n[b].n
}
return n[a].v < n[b].v
}
// Available is the structure used to represent the collection of all packages
// that can be installed.
type Available map[Name]map[Version]Meta
// Get returns the meta stored at a[n][v] or an error explaining why it could
// not be Get.
func (a Available) Get(n Name, v Version) (Meta, error) {
if _, ok := a[n]; !ok {
return Meta{}, errors.Errorf("could not find package named %q", n)
}
if v == "" {
if len(a[n]) == 0 {
return Meta{}, errors.Errorf("no configured versions for %q", n)
}
vers := Versions{}
for ver := range a[n] {
vers = append(vers, ver)
}
sort.Sort(vers)
v = vers[len(vers)-1]
}
if _, ok := a[n][v]; !ok {
return Meta{}, errors.Errorf("could not find %v@%v in database", n, v)
}
return a[n][v], nil
}
// Add inserts m into a.
func (a Available) Add(m Meta) error {
if _, err := m.Valid(); err != nil {
return errors.Wrap(err, "invalid meta")
}
if _, ok := a[Name(m.Name)]; !ok {
a[m.Name] = map[Version]Meta{}
}
a[m.Name][m.Version] = m
return nil
}
// Update inserts all data from o into a.
func (a Available) Update(o Available) error {
for _, vers := range o {
for _, m := range vers {
if err := a.Add(m); err != nil {
return errors.Wrap(err, "adding")
}
}
}
return nil
}
// SetRemote adds the information in the url to the database.
func (a Available) SetRemote(u url.URL) {
for n, vers := range a {
for v := range vers {
m := a[n][v]
m.Remote = u
a[n][v] = m
}
}
}
// Traverse returns a chan of Meta that will be sanely sorted.
func (a Available) Traverse() <-chan Meta {
r := make(chan Meta)
go func() {
names := Names{}
nvs := map[Name]Versions{}
for n, vers := range a {
names = append(names, n)
for v := range vers {
nvs[n] = append(nvs[n], v)
}
sort.Sort(nvs[n])
}
sort.Sort(names)
for _, n := range names {
for _, v := range nvs[n] {
r <- a[n][v]
}
}
close(r)
}()
return r
}
func labelForString(s string) (label, error) {
r := label{}
c := strings.Count(s, "@")
switch c {
case 0:
r.n = Name(s)
case 1:
sp := strings.Split(s, "@")
r.n, r.v = Name(sp[0]), Version(sp[1])
default:
return r, fmt.Errorf("unexpected number of '@' found, got %v, want 1", c)
}
if r.n == "" {
return r, fmt.Errorf("name cannot be empty")
}
return r, nil
}
// Installable calculates if the packages requested in "in" can be installed.
func (a Available) Installable(in []string) (Metas, error) {
ls := labels{}
for _, i := range in {
l, err := labelForString(i)
if err != nil {
return nil, errors.Wrap(err, "parsing name/version")
}
ls = append(ls, l)
}
seen := map[Name]bool{}
for _, l := range ls {
if _, ok := seen[l.n]; ok {
return nil, fmt.Errorf("can only ask to install %q once", l.n)
}
seen[l.n] = true
}
ms := Metas{}
for _, l := range ls {
m, err := a.Get(l.n, l.v)
if err != nil {
return ms, errors.Wrapf(err, "getting %v", l)
}
ms = append(ms, m)
}
return ms, nil
}

View File

@ -1,105 +0,0 @@
package pm
import (
"errors"
"net/url"
"testing"
)
func TestAvailableAdd(t *testing.T) {
tests := []struct {
label string
m Meta
count int
err error
}{
{
label: "good",
m: Meta{Name: "a", Version: "v1.0.0", Description: "test"},
count: 1,
},
{
label: "bad meta",
m: Meta{Name: "a"},
count: 1,
err: errors.New("missing"),
},
{
label: "dupe is last in",
m: Meta{Name: "a", Version: "v1.0.0", Description: "better version"},
count: 1,
},
{
label: "another good",
m: Meta{Name: "a", Version: "v1.0.0", Description: "better version"},
count: 1,
},
}
a := Available{}
for _, test := range tests {
t.Run(test.label, func(t *testing.T) {
if err := a.Add(test.m); (err == nil) != (test.err == nil) {
t.Fatalf("adding meta%v", err)
}
if got, want := len(a), test.count; got != want {
t.Fatalf("unexpected length after Add: got %v, want %v", got, want)
}
})
}
if got, want := a["a"]["v1.0.0"].Description, "better version"; got != want {
t.Fatalf("version: got %v, want %v", got, want)
}
}
func TestAvailableUpdate(t *testing.T) {
a := Available{}
if err := a.Add(Meta{Name: "a", Version: "v1.0.0", Description: "test"}); err != nil {
t.Fatalf("add: %v", err)
}
if err := a.Add(Meta{Name: "b", Version: "v2.0.0", Description: "test"}); err != nil {
t.Fatalf("add: %v", err)
}
b := Available{}
a.Update(b)
if got, want := len(a), 2; got != want {
t.Fatalf("len after empty update: got %v, want %v", got, want)
}
if err := b.Add(Meta{Name: "a", Version: "v1.0.0", Description: "test last in"}); err != nil {
t.Fatalf("add: %v", err)
}
if err := b.Add(Meta{Name: "b", Version: "v2.1.0", Description: "test"}); err != nil {
t.Fatalf("add: %v", err)
}
if err := a.Update(b); err != nil {
t.Fatalf("update: %v", err)
}
if got, want := len(a), 2; got != want {
t.Fatalf("len after update: got %v, want %v", got, want)
}
if got, want := len(a["a"]), 1; got != want {
t.Fatalf("len after update: got %v, want %v", got, want)
}
if got, want := len(a["b"]), 2; got != want {
t.Fatalf("len after update: got %v, want %v", got, want)
}
if got, want := a["a"]["v1.0.0"].Description, "test last in"; got != want {
t.Fatalf("last in didn't override")
}
u, err := url.Parse("https://pm.mcquay.me/darwin/amd64")
if err != nil {
t.Fatalf("parsing url: %v", err)
}
a.SetRemote(*u)
if got, want := a["a"]["v1.0.0"].Remote, *u; got != want {
t.Fatalf("last in didn't override")
}
}

View File

@ -1,275 +0,0 @@
package main
import (
"bufio"
"fmt"
"os"
"path/filepath"
"github.com/pkg/errors"
"mcquay.me/fs"
"mcquay.me/pm/db"
"mcquay.me/pm/keyring"
"mcquay.me/pm/pkg"
)
// Version stores the current version, and is updated at build time.
const Version = "dev"
const usage = `pm: simple, cross-platform system package manager
subcommands:
available (av) -- print out all installable packages
environ (env) -- print environment information
install (in) -- install packages
keyring (key) -- interact with pm's OpenPGP keyring
ls -- list installed packages
package (pkg) -- create packages
pull -- fetch all available packages from all configured remotes
remote -- configure remote pmd servers
rm -- remove packages
version (v) -- print version information
`
const keyUsage = `pm keyring: interact with pm's OpenPGP keyring
subcommands:
create (c) -- create a fresh keypair
export (e) -- export a public key to stdout
import (i) -- import a public key from stdin
ls -- list configured key info
rm -- remove a key from the keyring
sign (s) -- sign a file
verify (v) -- verify a detached signature
`
const pkgUsage = `pm package: generate pm-compatible packages
subcommands:
create (c) -- create a fresh keypair
`
const remoteUsage = `pm remote: configure remote pmd servers
subcommands:
add (a) -- add a URI
ls -- list configured remotes
rm -- remove a URI
`
func main() {
if len(os.Args) < 2 {
fatalf("pm: missing subcommand\n\n%v", usage)
}
cmd := os.Args[1]
root := os.Getenv("PM_ROOT")
if root == "" {
root = "/usr/local"
}
signID := os.Getenv("PM_PGP_ID")
switch cmd {
case "env", "environ":
fmt.Printf("PM_ROOT=%q\n", root)
fmt.Printf("PM_PGP_ID=%q\n", signID)
case "key", "keyring":
if len(os.Args[1:]) < 2 {
fatalf("pm keyring: insufficient args\n\nusage: %v", keyUsage)
}
sub, args := os.Args[2], os.Args[3:]
switch sub {
case "ls":
if err := keyring.ListKeys(root, os.Stdout); err != nil {
fatalf("listing keypair: %v\n", err)
}
case "c", "create":
var name, email string
s := bufio.NewScanner(os.Stdin)
fmt.Printf("name: ")
s.Scan()
if err := s.Err(); err != nil {
fatalf("reading name: %v\n", err)
}
name = s.Text()
fmt.Printf("email: ")
s.Scan()
if err := s.Err(); err != nil {
fatalf("reading email: %v\n", err)
}
email = s.Text()
if err := os.Stdin.Close(); err != nil {
fatalf("%v\n", err)
}
if err := keyring.NewKeyPair(root, name, email); err != nil {
fatalf("creating keypair: %v\n", err)
}
case "export", "e":
if len(args) != 1 {
fatalf("missing email argument\n")
}
email := args[0]
if err := keyring.Export(root, os.Stdout, email); err != nil {
fatalf("exporting public key for %q: %v\n", email, err)
}
case "sign", "s":
if signID == "" {
fatalf("must set PM_PGP_ID\n")
}
e, err := keyring.FindSecretEntity(root, signID)
if err != nil {
fatalf("find secret key: %v\n", err)
}
if err := keyring.Sign(e, os.Stdin, os.Stdout); err != nil {
fatalf("signing: %v\n", err)
}
case "verify", "v":
if len(args) != 2 {
fatalf("usage: pm key verify <file> <sig>\n")
}
fn, sn := args[0], args[1]
ff, err := os.Open(fn)
if err != nil {
fatalf("opening %q: %v\n", fn, err)
}
defer ff.Close()
sf, err := os.Open(sn)
if err != nil {
fatalf("opening %q: %v\n", fn, err)
}
defer sf.Close()
if err := keyring.Verify(root, ff, sf); err != nil {
fatalf("detached sig verify: %v\n", err)
}
case "i", "import":
if err := keyring.Import(root, os.Stdin); err != nil {
fatalf("importing key: %v\n", err)
}
case "rm":
if len(args) != 1 {
fatalf("missing key id\n\nusage: pm key remove <id>\n")
}
id := args[0]
if err := keyring.Remove(root, id); err != nil {
fatalf("removing key for %q: %v\n", id, err)
}
default:
fatalf("unknown keyring subcommand: %q\n\nusage: %v", sub, keyUsage)
}
case "package", "pkg":
if len(os.Args[1:]) < 2 {
fatalf("pm package: insufficient args\n\nusage: %v", pkgUsage)
}
sub := os.Args[2]
switch sub {
case "create", "creat", "c":
if signID == "" {
fatalf("must set PM_PGP_ID\n")
}
args := os.Args[3:]
if len(args) != 1 {
fatalf("usage: pm package create <directory>\n")
}
dir := args[0]
e, err := keyring.FindSecretEntity(root, signID)
if err != nil {
fatalf("find secret key: %v\n", err)
}
if err := pkg.Create(e, dir); err != nil {
fatalf("creating package: %v\n", err)
}
default:
fatalf("unknown package subcommand: %q\n\nusage: %v", sub, pkgUsage)
}
case "remote":
if len(os.Args[1:]) < 2 {
fatalf("pm remote: insufficient args\n\nusage: %v", remoteUsage)
}
sub := os.Args[2]
args := os.Args[3:]
if err := mkdirs(root); err != nil {
fatalf("making pm var directories: %v\n", err)
}
switch sub {
case "add", "a":
if len(args) < 1 {
fatalf("missing arg\n\nusage: pm remote add [<uris>]\n")
}
if err := db.AddRemotes(root, args); err != nil {
fatalf("remote add: %v\n", err)
}
case "rm":
if len(args) < 1 {
fatalf("missing arg\n\nusage: pm remote rm [<uris>]\n")
}
if err := db.RemoveRemotes(root, args); err != nil {
fatalf("remote remove: %v\n", err)
}
case "ls":
if err := db.ListRemotes(root, os.Stdout); err != nil {
fatalf("list: %v\n", err)
}
default:
fatalf("unknown package subcommand: %q\n\nusage: %v", sub, remoteUsage)
}
case "pull":
if err := db.Pull(root); err != nil {
fatalf("pulling available packages: %v\n", err)
}
case "available", "av":
if err := db.ListAvailable(root, os.Stdout); err != nil {
fatalf("pulling available packages: %v\n", err)
}
case "install", "in":
if len(os.Args[1:]) < 2 {
fatalf("pm install: insufficient args\n\nusage: pm install [pkg1, pkg2, ..., pkgN]\n")
}
pkgs := os.Args[2:]
if err := pkg.Install(root, pkgs); err != nil {
fatalf("installing: %v\n", err)
}
case "ls":
if len(os.Args[1:]) == 1 {
if err := db.ListInstalled(root, os.Stdout); err != nil {
fatalf("listing installed: %v\n", err)
}
} else {
if err := db.ListInstalledFiles(root, os.Stdout, os.Args[2:]); err != nil {
fatalf("listing installed: %v\n", err)
}
}
case "rm":
if len(os.Args[1:]) < 2 {
fatalf("pm rm: insufficient args\n\nusage: pm rm [pkg1, pkg2, ..., pkgN]\n")
}
pkgs := os.Args[2:]
if err := pkg.Remove(root, pkgs); err != nil {
fatalf("removing: %v\n", err)
}
case "version", "v":
fmt.Printf("pm: version %v\n", Version)
default:
fatalf("uknown subcommand %q\n\nusage: %v", cmd, usage)
}
}
func fatalf(f string, args ...interface{}) {
fmt.Fprintf(os.Stderr, f, args...)
os.Exit(1)
}
func mkdirs(root string) error {
d := filepath.Join(root, "var", "lib", "pm")
if !fs.Exists(d) {
if err := os.MkdirAll(d, 0700); err != nil {
return errors.Wrap(err, "mk pm dir")
}
}
return nil
}

22
cs.go
View File

@ -1,22 +0,0 @@
package pm
import (
"bufio"
"fmt"
"io"
"strings"
)
// ParseCS returns a parsed checksum file.
func ParseCS(f io.Reader) (map[string]string, error) {
cs := map[string]string{}
s := bufio.NewScanner(f)
for s.Scan() {
elems := strings.Split(s.Text(), "\t")
if len(elems) != 2 {
return nil, fmt.Errorf("manifest format error; got %d elements, want 2", len(elems))
}
cs[elems[1]] = elems[0]
}
return cs, nil
}

View File

@ -1,99 +0,0 @@
package db
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"github.com/pkg/errors"
"mcquay.me/fs"
"mcquay.me/pm"
)
const an = "var/lib/pm/available.json"
// Pull updates the available package database.
func Pull(root string) error {
db, err := load(root)
if err != nil {
return errors.Wrap(err, "loading db")
}
o := pm.Available{}
// Order here is important: the guarantee made is that any packages that
// exist in multiple remotes will be fetched by the first configured
// remote, which is why we traverse the database in reverse.
//
// TODO (sm): make this concurrent
for i := range db {
u := db[len(db)-i-1]
resp, err := http.Get(u.String() + "/available.json")
if err != nil {
return errors.Wrap(err, "http get")
}
a := pm.Available{}
if err := json.NewDecoder(resp.Body).Decode(&a); err != nil {
return errors.Wrapf(err, "decode remote available for %q", u.String())
}
a.SetRemote(u)
o.Update(a)
}
if err := saveAvailable(root, o); err != nil {
return errors.Wrap(err, "saving available db")
}
return nil
}
// ListAvailable prints all installable packages
func ListAvailable(root string, w io.Writer) error {
db, err := LoadAvailable(root)
if err != nil {
return errors.Wrap(err, "loading")
}
for m := range db.Traverse() {
fmt.Fprintf(w, "%v\t%v\t%v\n", m.Name, m.Version, m.Remote.String())
}
return nil
}
// LoadAvailable returns the collection of available packages
func LoadAvailable(root string) (pm.Available, error) {
r := pm.Available{}
dbn := filepath.Join(root, rn)
if !fs.Exists(dbn) {
return r, nil
}
f, err := os.Open(filepath.Join(root, an))
if err != nil {
return r, errors.Wrap(err, "open")
}
if err := json.NewDecoder(f).Decode(&r); err != nil {
return r, errors.Wrap(err, "decoding db")
}
return r, nil
}
func saveAvailable(root string, db pm.Available) error {
f, err := os.Create(filepath.Join(root, an))
if err != nil {
return errors.Wrap(err, "create")
}
enc := json.NewEncoder(f)
enc.SetIndent("", "\t")
if err := enc.Encode(&db); err != nil {
return errors.Wrap(err, "decoding db")
}
if err := f.Close(); err != nil {
return errors.Wrap(err, "close db")
}
return nil
}

View File

@ -1,138 +0,0 @@
package db
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"github.com/pkg/errors"
"mcquay.me/fs"
"mcquay.me/pm"
)
const in = "var/lib/pm/installed.json"
// AddInstalled adds m to the installed package database.
func AddInstalled(root string, m pm.Meta) error {
db, err := loadi(root)
if err != nil {
return errors.Wrap(err, "loading installed db")
}
db[m.Name] = m
return savei(root, db)
}
// RemoveInstalled adds m to the installed package database.
func RemoveInstalled(root string, m pm.Meta) error {
db, err := loadi(root)
if err != nil {
return errors.Wrap(err, "loading installed db")
}
delete(db, m.Name)
return savei(root, db)
}
// IsInstalled checks if m is in the installed package database.
func IsInstalled(root string, m pm.Meta) (bool, error) {
db, err := loadi(root)
if err != nil {
return false, errors.Wrap(err, "loading installed db")
}
_, r := db[m.Name]
return r, nil
}
// ListInstalled pretty prints the installed database to w.
func ListInstalled(root string, w io.Writer) error {
db, err := loadi(root)
if err != nil {
return errors.Wrap(err, "loading installed db")
}
for m := range db.Traverse() {
fmt.Fprintf(w, "%v\t%v\t%v\n", m.Name, m.Version, m.Remote.String())
}
return nil
}
// ListInstalledFiles prints the contents of a package.
func ListInstalledFiles(root string, w io.Writer, names []string) error {
for _, name := range names {
ok, err := IsInstalled(root, pm.Meta{Name: pm.Name(name)})
if err != nil {
return errors.Wrap(err, "is installed")
}
if !ok {
return fmt.Errorf("%v not installed", name)
}
}
for _, name := range names {
fn := filepath.Join(root, "var", "lib", "pm", "installed", name, "bom.sha256")
f, err := os.Open(fn)
if err != nil {
return errors.Wrapf(err, "opening %v's bom", name)
}
bom, err := pm.ParseCS(f)
if err != nil {
return errors.Wrapf(err, "parsing %v's bom", name)
}
if err := f.Close(); err != nil {
return errors.Wrapf(err, "closing %v's bom", name)
}
ks := []string{}
for k := range bom {
ks = append(ks, k)
}
sort.Strings(ks)
for _, k := range ks {
fmt.Fprintf(w, "%v\n", k)
}
}
return nil
}
func LoadInstalled(root string) (pm.Installed, error) {
return loadi(root)
}
func loadi(root string) (pm.Installed, error) {
r := pm.Installed{}
dbn := filepath.Join(root, in)
if !fs.Exists(dbn) {
return r, nil
}
f, err := os.Open(dbn)
if err != nil {
return r, errors.Wrap(err, "open")
}
if err := json.NewDecoder(f).Decode(&r); err != nil {
return r, errors.Wrap(err, "decoding db")
}
return r, nil
}
func savei(root string, db pm.Installed) error {
f, err := os.Create(filepath.Join(root, in))
if err != nil {
return errors.Wrap(err, "create")
}
enc := json.NewEncoder(f)
enc.SetIndent("", "\t")
if err := enc.Encode(&db); err != nil {
return errors.Wrap(err, "decoding db")
}
if err := f.Close(); err != nil {
return errors.Wrap(err, "close db")
}
return nil
}

View File

@ -1,150 +0,0 @@
package db
import (
"encoding/json"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"github.com/pkg/errors"
"mcquay.me/fs"
)
// DB is a slice of available URI
type DB []url.URL
const rn = "var/lib/pm/remotes.json"
// AddRemotes appends the provided uri to the list of configured remotes.
func AddRemotes(root string, uris []string) error {
db, err := load(root)
if err != nil {
return errors.Wrap(err, "loading")
}
dbm := map[string]bool{}
for _, u := range db {
dbm[u.String()] = true
}
for _, uri := range uris {
pu, err := url.Parse(uri)
if err != nil {
return errors.Wrap(err, "url parse")
}
u := strip(*pu)
if _, ok := dbm[u.String()]; ok {
return fmt.Errorf("%q already in db", u.String())
}
db = append(db, u)
}
return save(root, db)
}
// RemoveRemotes removes the given uri from the list of configured remotes.
func RemoveRemotes(root string, uris []string) error {
db, err := load(root)
if err != nil {
return errors.Wrap(err, "loading")
}
rms := map[string]bool{}
for _, uri := range uris {
pu, err := url.Parse(uri)
if err != nil {
return errors.Wrap(err, "url parse")
}
u := strip(*pu)
rms[u.String()] = true
}
o := DB{}
for _, d := range db {
if _, ok := rms[d.String()]; !ok {
o = append(o, d)
}
}
if len(o) == len(db) {
return errors.New("found no matching remotes")
}
return save(root, o)
}
// ListRemotes prints all configured remotes to w.
func ListRemotes(root string, w io.Writer) error {
db, err := load(root)
if err != nil {
return errors.Wrap(err, "loading")
}
for _, u := range db {
fmt.Fprintf(w, "%s\n", u.String())
}
return nil
}
func load(root string) (DB, error) {
r := DB{}
dbn := filepath.Join(root, rn)
if !fs.Exists(dbn) {
return r, nil
}
f, err := os.Open(filepath.Join(root, rn))
if err != nil {
return r, errors.Wrap(err, "open")
}
if err := json.NewDecoder(f).Decode(&r); err != nil {
return r, errors.Wrap(err, "decoding db")
}
return r, nil
}
func save(root string, db DB) error {
f, err := os.Create(filepath.Join(root, rn))
if err != nil {
return errors.Wrap(err, "create")
}
enc := json.NewEncoder(f)
enc.SetIndent("", "\t")
if err := enc.Encode(&db); err != nil {
return errors.Wrap(err, "decoding db")
}
if err := f.Close(); err != nil {
return errors.Wrap(err, "close db")
}
return nil
}
// strip removes all fields we don't currently need.
func strip(u url.URL) url.URL {
return url.URL{
Scheme: u.Scheme,
Host: u.Host,
Path: u.Path,
}
}
func mkdirs(root string) error {
d, _ := filepath.Split(filepath.Join(root, rn))
if !fs.Exists(d) {
if err := os.MkdirAll(d, 0700); err != nil {
return errors.Wrap(err, "mk pm dir")
}
}
return nil
}

View File

@ -1,157 +0,0 @@
package db
import (
"bytes"
"io/ioutil"
"os"
"strings"
"testing"
)
// TODO (sm): add more tests, including
// - empty add
// - removing db to empty
// - bad uris
func dirMe(t *testing.T) (string, func()) {
root, err := ioutil.TempDir("", "pm-tests-")
if err != nil {
t.Fatalf("tmpdir: %v", err)
}
if err := mkdirs(root); err != nil {
t.Fatalf("making pm dirs: %v", err)
}
return root, func() {
if err := os.RemoveAll(root); err != nil {
t.Fatalf("cleanup: %v", err)
}
}
}
func TestAdd(t *testing.T) {
root, del := dirMe(t)
defer del()
{
db, err := load(root)
if err != nil {
t.Fatalf("load: %v", err)
}
if got, want := len(db), 0; got != want {
t.Fatalf("empty db not empty: got %v, want %v", got, want)
}
}
bad := []string{
"http\ns://\nFoo|n",
}
if err := AddRemotes(root, bad); err == nil {
t.Fatalf("didn't detect bad url")
}
uris := []string{
"https://pm.mcquay.me/darwin/amd64",
}
if err := AddRemotes(root, uris); err != nil {
t.Fatalf("add: %v", err)
}
db, err := load(root)
if err != nil {
t.Fatalf("load: %v", err)
}
if got, want := len(db), len(uris); got != want {
t.Fatalf("unepected number of uris; got %v, want %v", got, want)
}
for _, u := range uris {
found := false
for _, d := range db {
if d.String() == u {
found = true
}
}
if !found {
t.Fatalf("did not find %v in the db", u)
}
}
if err := AddRemotes(root, uris); err == nil {
t.Fatalf("did not detect duplicate, and should have")
}
}
func TestRemove(t *testing.T) {
root, del := dirMe(t)
defer del()
if err := RemoveRemotes(root, nil); err == nil {
t.Fatalf("should have returned error on empty db")
}
uris := []string{
"https://pm.mcquay.me/foo",
"https://pm.mcquay.me/bar",
"https://pm.mcquay.me/baz",
}
if err := RemoveRemotes(root, uris); err == nil {
t.Fatalf("should have returned error asking to remove many uri on empty db")
}
if err := AddRemotes(root, uris); err != nil {
t.Fatalf("add: %v", err)
}
if err := RemoveRemotes(root, uris[1:2]); err != nil {
t.Fatalf("remove: %v", err)
}
db, err := load(root)
if err != nil {
t.Fatalf("load: %v", err)
}
if got, want := len(db), len(uris)-1; got != want {
t.Fatalf("unepected number of uris; got %v, want %v", got, want)
}
found := false
for _, d := range db {
if d.String() == uris[1] {
found = true
}
}
if found {
for _, v := range db {
t.Logf("%v", v.String())
}
t.Fatalf("failed to remove %v", uris[1:2])
}
}
func TestList(t *testing.T) {
root, del := dirMe(t)
defer del()
uris := []string{
"https://pm.mcquay.me/foo",
"https://pm.mcquay.me/bar",
"https://pm.mcquay.me/baz",
}
if err := AddRemotes(root, uris); err != nil {
t.Fatalf("add: %v", err)
}
buf := &bytes.Buffer{}
if err := ListRemotes(root, buf); err != nil {
t.Fatalf("list: %v", err)
}
for _, u := range uris {
if !strings.Contains(buf.String(), u) {
t.Fatalf("could not find %q in output\n%v", u, buf.String())
}
}
}

View File

@ -1,73 +0,0 @@
package pm
import (
"errors"
"fmt"
"sort"
"strings"
)
// Installed tracks installed packages.
type Installed map[Name]Meta
// Traverse returns a chan of Meta that will be sanely sorted.
func (i Installed) Traverse() <-chan Meta {
r := make(chan Meta)
go func() {
names := Names{}
for n := range i {
names = append(names, n)
}
sort.Sort(names)
for _, n := range names {
r <- i[n]
}
close(r)
}()
return r
}
// Removable calculates if the packages requested in "in" can all be removed.
func (i Installed) Removable(names []string) (Metas, error) {
inm := map[Name]bool{}
// XXX (sm): here we simply check if the package exists; eventually we'll
// have to check transitive dependencies, and deal with explicitly and
// implicitly installed packages.
found := map[Name]Meta{}
for _, name := range names {
n := Name(name)
inm[n] = true
if m, ok := i[n]; ok {
found[n] = m
}
}
if len(found) > len(inm) {
return nil, errors.New("should not have been able to find more than asked for, but did; internals are inconsistent.")
} else if len(inm) > len(found) {
// user asked for something that isn't installed.
missing := []string{}
for _, name := range names {
if _, ok := found[Name(name)]; !ok {
missing = append(missing, name)
}
}
return nil, fmt.Errorf("packages not installed: %v", strings.Join(missing, ", "))
}
if len(found) != len(inm) {
return nil, fmt.Errorf("escapes logic")
}
// XXX (sm): the ordering here will also eventually depend on transitive
// dependencies.
r := Metas{}
for _, m := range found {
r = append(r, m)
}
return r, nil
}

View File

@ -1,343 +0,0 @@
package keyring
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/armor"
"mcquay.me/fs"
)
// NewKeyPair creates and adds a new OpenPGP keypair to an existing keyring.
func NewKeyPair(root, name, email string) error {
if name == "" {
return errors.New("name cannot be empty")
}
if email == "" {
return errors.New("email cannot be empty")
}
if strings.ContainsAny(email, "()<>\x00") {
return fmt.Errorf("email %q contains invalid chars", email)
}
if err := ensureDir(root); err != nil {
return errors.Wrap(err, "can't find or create pgp dir")
}
srn, prn := getNames(root)
secs, pubs, err := getELs(srn, prn)
if err != nil {
return errors.Wrap(err, "getting existing keyrings")
}
fresh, err := openpgp.NewEntity(name, "pm", email, nil)
if err != nil {
errors.Wrap(err, "new entity")
}
pr, err := os.Create(prn)
if err != nil {
return errors.Wrap(err, "opening pubring")
}
sr, err := os.Create(srn)
if err != nil {
return errors.Wrap(err, "opening secring")
}
for _, e := range secs {
if err := e.SerializePrivate(sr, nil); err != nil {
return errors.Wrapf(err, "serializing old private key: %v", e.PrimaryKey.KeyIdString())
}
}
// order is critical here; if we don't serialize the private key of fresh
// first, the later steps fail.
if err := fresh.SerializePrivate(sr, nil); err != nil {
return errors.Wrapf(err, "serializing fresh private %v", fresh.PrimaryKey.KeyIdString())
}
if err := sr.Close(); err != nil {
return errors.Wrap(err, "closing secring")
}
for _, e := range pubs {
if err := e.Serialize(pr); err != nil {
return errors.Wrapf(err, "serializing %v", e.PrimaryKey.KeyIdString())
}
}
if err := fresh.Serialize(pr); err != nil {
return errors.Wrapf(err, "serializing %v", fresh.PrimaryKey.KeyIdString())
}
if err := pr.Close(); err != nil {
return errors.Wrap(err, "closing pubring")
}
return nil
}
// ListKeys prints keyring information to w.
func ListKeys(root string, w io.Writer) error {
if err := ensureDir(root); err != nil {
return errors.Wrap(err, "can't find or create pgp dir")
}
srn, prn := getNames(root)
secs, pubs, err := getELs(srn, prn)
if err != nil {
return errors.Wrap(err, "getting existing keyrings")
}
for _, s := range secs {
names := []string{}
for _, v := range s.Identities {
names = append(names, v.Name)
}
fmt.Fprintf(w, "sec: %+v:\t%v\n", s.PrimaryKey.KeyIdShortString(), strings.Join(names, ","))
}
for _, p := range pubs {
names := []string{}
for _, v := range p.Identities {
names = append(names, v.Name)
}
fmt.Fprintf(w, "pub: %+v:\t%v\n", p.PrimaryKey.KeyIdShortString(), strings.Join(names, ","))
}
return nil
}
// Export prints pubkey information associated with email to w.
func Export(root string, w io.Writer, email string) error {
if err := ensureDir(root); err != nil {
return errors.Wrap(err, "can't find or create pgp dir")
}
srn, prn := getNames(root)
_, pubs, err := getELs(srn, prn)
if err != nil {
return errors.Wrap(err, "getting existing keyrings")
}
e, err := findKey(pubs, email)
if err != nil {
return errors.Wrap(err, "find key")
}
aw, err := armor.Encode(w, openpgp.PublicKeyType, nil)
if err != nil {
return errors.Wrap(err, "creating armor encoder")
}
if err := e.Serialize(aw); err != nil {
return errors.Wrap(err, "serializing key")
}
if err := aw.Close(); err != nil {
return errors.Wrap(err, "closing armor encoder")
}
fmt.Fprintf(w, "\n")
return nil
}
// Import parses public key information from w and adds it to the public
// keyring.
func Import(root string, w io.Reader) error {
el, err := openpgp.ReadArmoredKeyRing(w)
if err != nil {
return errors.Wrap(err, "reading keyring")
}
if err := ensureDir(root); err != nil {
return errors.Wrap(err, "can't find or create pgp dir")
}
srn, prn := getNames(root)
_, pubs, err := getELs(srn, prn)
if err != nil {
return errors.Wrap(err, "getting existing keyrings")
}
foreign := openpgp.EntityList{}
exist := map[uint64]bool{}
for _, p := range pubs {
exist[p.PrimaryKey.KeyId] = true
}
for _, e := range el {
if _, ok := exist[e.PrimaryKey.KeyId]; !ok {
foreign = append(foreign, e)
}
}
if len(foreign) < 1 {
return errors.New("no new key material found")
}
pubs = append(pubs, foreign...)
pr, err := os.Create(prn)
if err != nil {
return errors.Wrap(err, "opening pubring")
}
for _, e := range pubs {
if err := e.Serialize(pr); err != nil {
return errors.Wrapf(err, "serializing %v", e.PrimaryKey.KeyIdString())
}
}
if err := pr.Close(); err != nil {
return errors.Wrap(err, "closing pubring")
}
return nil
}
// Sign takes an id and a reader and writes the signature for that id to sig.
func Sign(key *openpgp.Entity, in io.Reader, sig io.Writer) error {
if err := openpgp.ArmoredDetachSign(sig, key, in, nil); err != nil {
return errors.Wrap(err, "armored detach sign")
}
fmt.Fprintf(sig, "\n")
return nil
}
// Verify verifies a file's deatched signature.
func Verify(root string, file, sig io.Reader) error {
if err := ensureDir(root); err != nil {
return errors.Wrap(err, "can't find or create pgp dir")
}
srn, prn := getNames(root)
_, pubs, err := getELs(srn, prn)
if err != nil {
return errors.Wrap(err, "getting existing keyrings")
}
if _, err = openpgp.CheckArmoredDetachedSignature(pubs, file, sig); err != nil {
return errors.Wrap(err, "check sig")
}
return nil
}
// Remove removes public key information for a given id.
//
// It skips public keys that have matching secret keys, and does not effect
// private keys.
func Remove(root string, id string) error {
if err := ensureDir(root); err != nil {
return errors.Wrap(err, "can't find or create pgp dir")
}
srn, prn := getNames(root)
secs, pubs, err := getELs(srn, prn)
if err != nil {
return errors.Wrap(err, "getting existing keyrings")
}
victim, err := findKey(pubs, id)
if err != nil {
return errors.Wrapf(err, "finding key %q", id)
}
pr, err := os.Create(prn)
if err != nil {
return errors.Wrap(err, "opening pubring")
}
var rerr error
for _, p := range pubs {
if victim.PrimaryKey.KeyId == p.PrimaryKey.KeyId {
if len(secs.KeysById(victim.PrimaryKey.KeyId)) == 0 {
continue
}
rerr = fmt.Errorf("skipping pubkey with matching privkey: %v", p.PrimaryKey.KeyIdShortString())
}
if err := p.Serialize(pr); err != nil {
return errors.Wrapf(err, "serializing %v", p.PrimaryKey.KeyIdString())
}
}
if err := pr.Close(); err != nil {
return errors.Wrap(err, "closing pubring")
}
return rerr
}
func pGPDir(root string) string {
return filepath.Join(root, "var", "lib", "pm", "pgp")
}
func ensureDir(root string) error {
d := pGPDir(root)
if !fs.Exists(d) {
if err := os.MkdirAll(d, 0700); err != nil {
return errors.Wrap(err, "mk pgp dir")
}
}
return nil
}
func getNames(root string) (string, string) {
srn := filepath.Join(pGPDir(root), "secring.gpg")
prn := filepath.Join(pGPDir(root), "pubring.gpg")
return srn, prn
}
func getELs(secring, pubring string) (openpgp.EntityList, openpgp.EntityList, error) {
var sr, pr openpgp.EntityList
if fs.Exists(secring) {
f, err := os.Open(secring)
if err != nil {
return nil, nil, errors.Wrap(err, "opening secring")
}
sr, err = openpgp.ReadKeyRing(f)
if err != nil {
return nil, nil, errors.Wrap(err, "read sec key ring")
}
if err := f.Close(); err != nil {
return nil, nil, errors.Wrap(err, "closing keyring")
}
}
if fs.Exists(pubring) {
f, err := os.Open(pubring)
if err != nil {
return nil, nil, errors.Wrap(err, "opening pubring")
}
pr, err = openpgp.ReadKeyRing(f)
if err != nil {
return nil, nil, errors.Wrap(err, "read pub key ring")
}
if err := f.Close(); err != nil {
return nil, nil, errors.Wrap(err, "closing keyring")
}
}
return sr, pr, nil
}
func findKey(el openpgp.EntityList, id string) (*openpgp.Entity, error) {
var e *openpgp.Entity
if strings.Contains(id, "@") {
es := openpgp.EntityList{}
for _, p := range el {
for _, v := range p.Identities {
if id == v.UserId.Email {
es = append(es, p)
}
}
}
if len(es) == 1 {
return es[0], nil
}
if len(es) > 1 {
return nil, errors.New("too many keys matched; try searching by key id?")
}
} else {
for _, p := range el {
if id == p.PrimaryKey.KeyIdShortString() {
return p, nil
}
}
}
return e, fmt.Errorf("key %q not found", id)
}
// FindSecretEntity searches for id in the secret keyring.
func FindSecretEntity(root, id string) (*openpgp.Entity, error) {
if err := ensureDir(root); err != nil {
return nil, errors.Wrap(err, "can't find or create pgp dir")
}
srn, prn := getNames(root)
secs, _, err := getELs(srn, prn)
if err != nil {
return nil, errors.Wrap(err, "getting existing keyrings")
}
return findKey(secs, id)
}

47
meta.go
View File

@ -1,47 +0,0 @@
package pm
import (
"errors"
"fmt"
"net/url"
)
// Meta tracks metadata for a package
type Meta struct {
Name Name `json:"name"`
Version Version `json:"version"`
Description string `json:"description"`
Remote url.URL `json:"remote"`
}
// Valid validates the contents of a Meta for requires fields.
func (m Meta) Valid() (bool, error) {
if m.Name == "" {
return false, errors.New("name cannot be empty")
}
if m.Version == "" {
return false, errors.New("version cannot be empty")
}
if m.Description == "" {
return false, errors.New("description cannot be empty")
}
return true, nil
}
// Pkg returns the string name the .pkg should have on disk.
func (m Meta) Pkg() string {
return fmt.Sprintf("%s-%s.pkg", m.Name, m.Version)
}
// URL returns the http location of this package.
func (m Meta) URL() string {
return fmt.Sprintf("%s/%s", m.Remote.String(), m.Pkg())
}
func (m Meta) String() string {
return m.URL()
}
// Metas is a slice of Meta
type Metas []Meta

View File

@ -1,85 +0,0 @@
package pm
import (
"bytes"
"encoding/json"
"errors"
"testing"
)
func TestValid(t *testing.T) {
tests := []struct {
label string
m Meta
ok bool
err error
}{
{
label: "valid",
m: Meta{
Name: "heat",
Version: "1.1.0",
Description: "some description",
},
ok: true,
},
{
label: "missing name",
m: Meta{
Version: "1.1.0",
Description: "some description",
},
err: errors.New("name"),
},
{
label: "missing version",
m: Meta{
Name: "heat",
Description: "some description",
},
err: errors.New("version"),
},
{
label: "missing description",
m: Meta{
Name: "heat",
Version: "1.1.0",
},
err: errors.New("description"),
},
}
for _, test := range tests {
t.Run(test.label, func(t *testing.T) {
ok, err := test.m.Valid()
if got, want := ok, test.ok; got != want {
t.Fatalf("validity: got %v, want %v", got, want)
}
if got, want := err, test.err; (err == nil) != (test.err == nil) {
t.Fatalf("error: got %v, want %v", got, want)
}
})
}
}
func TestJsonRoundTrip(t *testing.T) {
b := Meta{
Name: "heat",
Version: "1.1.0",
Description: "make heat using cpus",
}
buf := &bytes.Buffer{}
if err := json.NewEncoder(buf).Encode(&b); err != nil {
t.Fatalf("encode: %v", err)
}
a := Meta{}
if err := json.NewDecoder(buf).Decode(&a); err != nil {
t.Fatalf("decode: %v", err)
}
if b != a {
t.Fatalf("a != b: %v != %v", a, b)
}
}

View File

@ -1,359 +0,0 @@
package pkg
import (
"archive/tar"
"bufio"
"compress/bzip2"
"crypto/sha256"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/pkg/errors"
"mcquay.me/fs"
"mcquay.me/pm"
"mcquay.me/pm/db"
"mcquay.me/pm/keyring"
)
const cache = "var/cache/pm"
const installed = "var/lib/pm/installed"
// Install fetches and installs pkgs from appropriate remotes.
func Install(root string, pkgs []string) error {
av, err := db.LoadAvailable(root)
if err != nil {
return errors.Wrap(err, "loading available db")
}
ms, err := av.Installable(pkgs)
if err != nil {
return errors.Wrap(err, "checking ability to install")
}
cacheDir := filepath.Join(root, cache)
if !fs.Exists(cacheDir) {
if err := os.MkdirAll(cacheDir, 0755); err != nil {
return errors.Wrap(err, "creating non-existent cache dir")
}
}
if !fs.IsDir(cacheDir) {
return errors.Errorf("%q is not a directory!", cacheDir)
}
installedDir := filepath.Join(root, installed)
if !fs.Exists(installedDir) {
if err := os.MkdirAll(installedDir, 0755); err != nil {
return errors.Wrap(err, "creating non-existent cache dir")
}
}
if !fs.IsDir(cacheDir) {
return errors.Errorf("%q is not a directory!", cacheDir)
}
if err := download(cacheDir, ms); err != nil {
return errors.Wrap(err, "downloading")
}
for _, m := range ms {
if err := install(root, m); err != nil {
return errors.Wrapf(err, "installing %v", m.Name)
}
}
return nil
}
func download(cache string, ms pm.Metas) error {
// TODO (sm): concurrently fetch
for _, m := range ms {
resp, err := http.Get(m.URL())
if err != nil {
return errors.Wrap(err, "http get")
}
fn := filepath.Join(cache, m.Pkg())
f, err := os.Create(fn)
if err != nil {
return errors.Wrap(err, "creating")
}
if n, err := io.Copy(f, resp.Body); err != nil {
return errors.Wrapf(err, "copy %q to disk after %d bytes", m.URL(), n)
}
if err := resp.Body.Close(); err != nil {
return errors.Wrap(err, "closing resp body")
}
}
return nil
}
func verifyManifestIntegrity(root string, m pm.Meta) error {
pn := filepath.Join(root, cache, m.Pkg())
man, err := getReadCloser(pn, "manifest.sha256")
if err != nil {
return errors.Wrap(err, "getting manifest reader")
}
sig, err := getReadCloser(pn, "manifest.sha256.asc")
if err != nil {
return errors.Wrap(err, "getting manifest reader")
}
if err := keyring.Verify(root, man, sig); err != nil {
return errors.Wrap(err, "verifying manifest")
}
if err := man.Close(); err != nil {
return errors.Wrap(err, "closing manifest reader")
}
if err := sig.Close(); err != nil {
return errors.Wrap(err, "closing manifest signature reader")
}
return nil
}
func expandPkgContents(root string, m pm.Meta) error {
pn := filepath.Join(root, cache, m.Pkg())
man, err := getReadCloser(pn, "manifest.sha256")
if err != nil {
return errors.Wrap(err, "getting manifest reader")
}
ip := filepath.Join(root, installed, string(m.Name))
if err := os.MkdirAll(ip, 0755); err != nil {
return errors.Wrapf(err, "making install dir for %q", m.Name)
}
cs := map[string]string{}
s := bufio.NewScanner(man)
for s.Scan() {
elems := strings.Split(s.Text(), "\t")
if len(elems) != 2 {
return errors.Errorf("manifest format error; got %d elements, want 2", len(elems))
}
cs[elems[1]] = elems[0]
}
if err := man.Close(); err != nil {
return errors.Wrap(err, "closing manifest reader")
}
if err := s.Err(); err != nil {
return errors.Wrap(err, "scanning manifest")
}
pf, err := os.Open(pn)
if err != nil {
return errors.Wrap(err, "opening pkg file")
}
tr := tar.NewReader(pf)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return errors.Wrap(err, "tar traversal")
}
if hdr.Name == "manifest.sha256" || hdr.Name == "manifest.sha256.asc" {
continue
}
if hdr.FileInfo().IsDir() {
if hdr.Name != "bin" {
return errors.Errorf("%v is unexpected", hdr.Name)
}
if err := os.MkdirAll(filepath.Join(ip, hdr.Name), hdr.FileInfo().Mode()); err != nil {
return errors.Wrapf(err, "mkdir for %v", hdr.Name)
}
continue
}
sha, ok := cs[hdr.Name]
if !ok {
return errors.Errorf("extra file %q found in tarfile!", hdr.Name)
}
name := filepath.Join(ip, hdr.Name)
sr := sha256.New()
var o io.WriteCloser
o = close{ioutil.Discard}
if hdr.Name != "root.tar.bz2" {
f, err := os.OpenFile(filepath.Join(ip, hdr.Name), os.O_WRONLY|os.O_CREATE, hdr.FileInfo().Mode())
if err != nil {
return errors.Wrap(err, "open file in install dir")
}
o = f
}
w := io.MultiWriter(o, sr)
if n, err := io.Copy(w, tr); err != nil {
return errors.Wrapf(err, "copying file %q after %v bytes", hdr.Name, n)
}
if sha != fmt.Sprintf("%x", sr.Sum(nil)) {
return errors.Errorf("%q checksum was incorrect", hdr.Name)
}
if err := o.Close(); err != nil {
return errors.Wrapf(err, "closing %v", name)
}
}
return nil
}
type tarSlurper struct {
f *os.File
tr *tar.Reader
}
func (ts *tarSlurper) Close() error {
return ts.f.Close()
}
func (ts *tarSlurper) Read(p []byte) (int, error) {
return ts.tr.Read(p)
}
func getReadCloser(tn, fn string) (io.ReadCloser, error) {
pf, err := os.Open(tn)
if err != nil {
return nil, errors.Wrap(err, "opening pkg file")
}
tr := tar.NewReader(pf)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, errors.Wrap(err, "tar traversal")
}
if hdr.Name == fn {
return &tarSlurper{pf, tr}, nil
}
}
return nil, errors.Errorf("%q not found", fn)
}
// close should be used to wrap ioutil.Discard to give it a noop Close method.
type close struct {
io.Writer
}
func (close) Close() error {
return nil
}
func script(root string, m pm.Meta, name string) error {
bin := filepath.Join(root, installed, string(m.Name), "bin", name)
if !fs.Exists(bin) {
return nil
}
cmd := exec.Command(bin)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func expandRoot(root string, m pm.Meta) error {
bomn := filepath.Join(root, installed, string(m.Name), "bom.sha256")
bf, err := os.Open(bomn)
if err != nil {
return errors.Wrap(err, "opening bom")
}
cs := map[string]string{}
s := bufio.NewScanner(bf)
for s.Scan() {
elems := strings.Split(s.Text(), "\t")
if len(elems) != 2 {
return errors.Errorf("manifest format error; got %d elements, want 2", len(elems))
}
cs[elems[1]] = elems[0]
}
pn := filepath.Join(root, cache, m.Pkg())
tbz, err := getReadCloser(pn, "root.tar.bz2")
if err != nil {
return errors.Wrap(err, "getting root.tar.bz2 reader")
}
tr := tar.NewReader(bzip2.NewReader(tbz))
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return errors.Wrap(err, "tar traversal")
}
if hdr.FileInfo().IsDir() {
d := filepath.Join(root, hdr.Name)
if err := os.MkdirAll(d, hdr.FileInfo().Mode()); err != nil {
return errors.Wrapf(err, "making directory %q", d)
}
continue
}
f, err := os.OpenFile(filepath.Join(root, hdr.Name), os.O_WRONLY|os.O_CREATE, hdr.FileInfo().Mode())
if err != nil {
return errors.Wrapf(err, "open output file %q", hdr.Name)
}
if n, err := io.Copy(f, tr); err != nil {
return errors.Wrapf(err, "copy file %q after %v bytes", hdr.Name, n)
}
if err := f.Close(); err != nil {
return errors.Wrapf(err, "closing %q", hdr.Name)
}
}
return nil
}
func install(root string, m pm.Meta) error {
defer func() {
cached := filepath.Join(root, cache, m.Pkg())
if !fs.Exists(cached) {
return
}
if err := os.Remove(cached); err != nil {
log.Printf("cleaning up cache: %v", err)
}
}()
already, err := db.IsInstalled(root, m)
if err != nil {
return errors.Wrapf(err, "is installed %v", m.Name)
}
if already {
return errors.Errorf("%v already installed!", m.Name)
}
if err := verifyManifestIntegrity(root, m); err != nil {
return errors.Wrap(err, "verifying pkg integrity")
}
if err := expandPkgContents(root, m); err != nil {
if err := os.RemoveAll(filepath.Join(root, installed, string(m.Name))); err != nil {
err = errors.Wrap(err, "cleaning up")
}
return errors.Wrap(err, "verifying pkg contents")
}
if err := script(root, m, "pre-install"); err != nil {
return errors.Wrap(err, "pre-install")
}
if err := expandRoot(root, m); err != nil {
return errors.Wrap(err, "root expansion")
}
if err := script(root, m, "post-install"); err != nil {
return errors.Wrap(err, "pre-install")
}
if err := db.AddInstalled(root, m); err != nil {
return errors.Wrapf(err, "adding ", m.Name)
}
return nil
}

View File

@ -1,243 +0,0 @@
package pkg
import (
"archive/tar"
"compress/bzip2"
"crypto/sha256"
"fmt"
"io"
"log"
"os"
"path/filepath"
"sort"
"strings"
"github.com/pkg/errors"
"golang.org/x/crypto/openpgp"
yaml "gopkg.in/yaml.v2"
"mcquay.me/fs"
"mcquay.me/pm"
"mcquay.me/pm/keyring"
)
var validNames map[string]bool
var crypto []string
func init() {
validNames = map[string]bool{
"root.tar.bz2": true,
"meta.yaml": true,
"bin/pre-install": true,
"bin/post-install": true,
"bin/pre-upgrade": true,
"bin/post-upgrade": true,
"bin/pre-remove": true,
"bin/post-remove": true,
}
crypto = []string{
"bom.sha256",
"manifest.sha256",
"manifest.sha256.asc",
}
}
// Create traverses the contents of dir and emits a valid pkg, signed by id
func Create(key *openpgp.Entity, dir string) error {
if !fs.Exists(dir) {
return fmt.Errorf("%q: doesn't exist", dir)
}
if !fs.IsDir(dir) {
return fmt.Errorf("%q: is not a directory", dir)
}
if err := clean(dir); err != nil {
return errors.Wrap(err, "cleaning up directory")
}
files := []string{}
found := map[string]bool{}
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if path == dir {
return nil
}
if info.IsDir() {
return nil
}
p := strings.TrimPrefix(path, dir)[1:]
if _, ok := validNames[p]; !ok {
return fmt.Errorf("unxpected filename: %q", p)
}
if strings.HasPrefix(p, "bin") && info.Mode()&0100 == 0 {
return fmt.Errorf("%q is not executable", path)
}
files = append(files, p)
found[p] = true
return nil
})
if err != nil {
return errors.Wrap(err, "checking directory contents")
}
if _, ok := found["root.tar.bz2"]; !ok {
return fmt.Errorf("did not find root.tar.bz2")
}
if _, ok := found["meta.yaml"]; !ok {
return fmt.Errorf("did not find meta.yaml")
}
mf, err := os.Open(filepath.Join(dir, "meta.yaml"))
if err != nil {
return errors.Wrap(err, "opening metadata file")
}
md := pm.Meta{}
if err := yaml.NewDecoder(mf).Decode(&md); err != nil {
return errors.Wrap(err, "decoding metadata file")
}
if _, err := md.Valid(); err != nil {
return errors.Wrap(err, "invalid metadata")
}
f, err := os.Open(filepath.Join(dir, "root.tar.bz2"))
if err != nil {
return errors.Wrap(err, "opening overlay tarball")
}
bom, err := os.Create(filepath.Join(dir, "bom.sha256"))
if err != nil {
return errors.Wrap(err, "creating bom.sha256")
}
tr := tar.NewReader(bzip2.NewReader(f))
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
errors.Wrap(err, "traversing tarball")
}
if hdr.FileInfo().IsDir() {
continue
}
s := sha256.New()
if c, err := io.Copy(s, tr); err != nil {
return errors.Wrapf(err, "copy after %d bytes", c)
}
fmt.Fprintf(bom, "%x\t%s\n", s.Sum(nil), hdr.Name)
}
if err := bom.Close(); err != nil {
return errors.Wrap(err, "closing bom")
}
files = append(files, "bom.sha256")
sort.Strings(files)
manifest, err := os.Create(filepath.Join(dir, "manifest.sha256"))
if err != nil {
return errors.Wrap(err, "creating manifest.sha256")
}
for _, fn := range files {
full := filepath.Join(dir, fn)
f, err := os.Open(full)
if err != nil {
return errors.Wrap(err, "opening file for manifest checksumming")
}
s := sha256.New()
if c, err := io.Copy(s, f); err != nil {
return errors.Wrapf(err, "copy after %d bytes", c)
}
if err := f.Close(); err != nil {
return errors.Wrapf(err, "closing %q", f.Name())
}
fmt.Fprintf(manifest, "%x\t%s\n", s.Sum(nil), fn)
}
if err := manifest.Close(); err != nil {
return errors.Wrap(err, "closing manifest")
}
sig, err := os.Create(filepath.Join(dir, "manifest.sha256.asc"))
if err != nil {
return errors.Wrap(err, "creating sig file")
}
mfi, err := os.Open(filepath.Join(dir, "manifest.sha256"))
if err != nil {
return errors.Wrap(err, "opening manifest")
}
if err := keyring.Sign(key, mfi, sig); err != nil {
return errors.Wrap(err, "signing")
}
files = append(files, "manifest.sha256")
files = append(files, "manifest.sha256.asc")
sort.Strings(files)
tn, pn := filepath.Split(dir)
tn = filepath.Join(tn, fmt.Sprintf("%v-%v.pkg", pn, md.Version))
tf, err := os.Create(tn)
if err != nil {
return errors.Wrap(err, "opening final .pkg")
}
tw := tar.NewWriter(tf)
err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if path == dir {
return nil
}
p := strings.TrimPrefix(path, dir)[1:]
hdr, err := tar.FileInfoHeader(info, "")
if err != nil {
return errors.Wrap(err, "file info header")
}
hdr.Name = p
if err := tw.WriteHeader(hdr); err != nil {
return errors.Wrapf(err, "writing tar header for %v", p)
}
// only need to do real writing for non-directories
if info.IsDir() {
return nil
}
f, err := os.Open(path)
if err != nil {
return errors.Wrap(err, "opening file for tar creation")
}
if c, err := io.Copy(tw, f); err != nil {
log.Printf("%+v", err == io.EOF)
return errors.Wrapf(err, "copy after %d bytes", c)
}
if err := f.Close(); err != nil {
return errors.Wrap(err, "closing file during tar creation")
}
return nil
})
if err != nil {
return errors.Wrap(err, "traversing the prepared directory")
}
if err := tw.Close(); err != nil {
return errors.Wrap(err, "closing tar writer")
}
if err := tf.Close(); err != nil {
return errors.Wrap(err, "closing final .pkg")
}
return nil
}
func clean(root string) error {
for _, f := range crypto {
path := filepath.Join(root, f)
if !fs.Exists(path) {
continue
}
if err := os.Remove(path); err != nil {
return errors.Wrapf(err, "removing %q", path)
}
}
return nil
}

View File

@ -1,61 +0,0 @@
package pkg
import (
"os"
"path/filepath"
"github.com/pkg/errors"
"mcquay.me/pm"
"mcquay.me/pm/db"
)
// Remove uninstalls packages.
func Remove(root string, pkgs []string) error {
iDB, err := db.LoadInstalled(root)
if err != nil {
return errors.Wrap(err, "loading available db")
}
ms, err := iDB.Removable(pkgs)
if err != nil {
return errors.Wrap(err, "checking ability to remove")
}
for _, m := range ms {
if err := script(root, m, "pre-remove"); err != nil {
return errors.Wrap(err, "pre-remove")
}
mdir := filepath.Join(root, installed, string(m.Name))
bom := filepath.Join(mdir, "bom.sha256")
bf, err := os.Open(bom)
if err != nil {
return errors.Wrapf(err, "%q: opening bom", m.Name)
}
cs, err := pm.ParseCS(bf)
if err != nil {
return errors.Wrapf(err, "%q: parsing bom", m.Name)
}
for n := range cs {
if err := os.Remove(filepath.Join(root, n)); err != nil {
return errors.Wrapf(err, "pkg %q", m.Name)
}
}
if err := script(root, m, "post-remove"); err != nil {
return errors.Wrap(err, "post-remove")
}
if err := db.RemoveInstalled(root, m); err != nil {
return errors.Wrapf(err, "removing %q", m.Name)
}
if err := os.RemoveAll(mdir); err != nil {
return errors.Wrapf(err, "%q: removing pm install dir", m.Name)
}
}
return nil
}