diff --git a/Makefile b/Makefile index e7ed399..9554857 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ export CGO_ENABLED=0 -export GOPATH?=$(PWD)/../../../../ +export GOPATH?=$(shell go env GOPATH) export DESTDIR?=$(GOPATH)/bin export GOBIN?=$(DESTDIR) diff --git a/config.go b/config.go index 31f5d35..d639b9c 100644 --- a/config.go +++ b/config.go @@ -6,8 +6,10 @@ package debpkg import ( "fmt" - "github.com/xor-gate/debpkg/internal/config" "io/ioutil" + "strings" + + "github.com/xor-gate/debpkg/internal/config" ) // Config loads settings from a depkg.yml specfile @@ -38,17 +40,29 @@ func (deb *DebPkg) Config(filename string) error { deb.SetShortDescription(cfg.Description.Short) deb.SetDescription(cfg.Description.Long) deb.SetBuiltUsing(cfg.BuiltUsing) + deb.SetDepends(cfg.Depends) + deb.SetRecommends(cfg.Recommends) + deb.SetSuggests(cfg.Suggests) + deb.SetConflicts(cfg.Conflicts) + deb.SetProvides(cfg.Provides) + deb.SetReplaces(cfg.Replaces) for _, file := range cfg.Files { - err := deb.AddFile(file.Src, file.Dest) - if err != nil { - return fmt.Errorf("error adding file %s: %v", file.Src, err) + if len(file.Src) > 0 { + if err := deb.AddFile(file.Src, file.Dest); err != nil { + return fmt.Errorf("error adding file %s: %v", file.Src, err) + } + } else if len(file.Content) > 0 { + if err := deb.AddFileString(file.Content, file.Dest); err != nil { + return fmt.Errorf("error adding file by string: %v", err) + } + } else { + return fmt.Errorf("need either 'content' or a 'src' to add a file") } } for _, dir := range cfg.Directories { - err := deb.AddDirectory(dir) - if err != nil { + if err := deb.AddDirectory(dir); err != nil { return fmt.Errorf("error adding directory %s: %v", dir, err) } } @@ -60,5 +74,36 @@ func (deb *DebPkg) Config(filename string) error { } } + if len(cfg.ControlExtra.Preinst) > 0 { + if strings.ContainsAny(cfg.ControlExtra.Preinst, "\n") { + deb.AddControlExtraString("preinst", cfg.ControlExtra.Preinst) + } else { + deb.AddControlExtra("preinst", cfg.ControlExtra.Preinst) + } + } + + if len(cfg.ControlExtra.Postinst) > 0 { + if strings.ContainsAny(cfg.ControlExtra.Postinst, "\n") { + deb.AddControlExtraString("postinst", cfg.ControlExtra.Postinst) + } else { + deb.AddControlExtra("postinst", cfg.ControlExtra.Postinst) + } + } + + if len(cfg.ControlExtra.Prerm) > 0 { + if strings.ContainsAny(cfg.ControlExtra.Prerm, "\n") { + deb.AddControlExtraString("prerm", cfg.ControlExtra.Prerm) + } else { + deb.AddControlExtra("prerm", cfg.ControlExtra.Prerm) + } + } + + if len(cfg.ControlExtra.Postrm) > 0 { + if strings.ContainsAny(cfg.ControlExtra.Postrm, "\n") { + deb.AddControlExtraString("postrm", cfg.ControlExtra.Postrm) + } else { + deb.AddControlExtra("postrm", cfg.ControlExtra.Postrm) + } + } return nil } diff --git a/config_test.go b/config_test.go index 13650fc..eb13b7c 100644 --- a/config_test.go +++ b/config_test.go @@ -5,10 +5,11 @@ package debpkg import ( - "github.com/stretchr/testify/assert" - "github.com/xor-gate/debpkg/internal/test" "runtime" "testing" + + "github.com/stretchr/testify/assert" + "github.com/xor-gate/debpkg/internal/test" ) // TestExampleConfig verifies if the config example in the root is correctly loaded @@ -21,6 +22,12 @@ maintainer_email: deb@pkg.com homepage: https://github.com/xor-gate/debpkg section: devel priority: standard +depends: lsb-release +recommends: nano +suggests: curl +conflicts: pico +provides: editor +replaces: vim built_using: golang description: short: This is a short description @@ -36,10 +43,19 @@ files: - file: debpkg_test.go - file: README.md dest: {{.DATAROOTDIR}}/foobar/README.md + - dest: /bin/hello + content: > + #!/bin/bash + echo "hello" directories: - ./internal emptydirs: - /var/cache/foobar +control_extra: + postrm: Makefile + prerm: Makefile + postinst: Makefile + preinst: Makefile ` filepath, err := test.WriteTempFile("debpkg.yml", configFile) assert.Nil(t, err) @@ -68,6 +84,52 @@ emptydirs: assert.Nil(t, testWrite(t, deb)) } +func TestExampleConfig2(t *testing.T) { + const configFile = `name: foo-bar +version: 1.2.3 +architecture: amd64 +maintainer: Mr. Foo Bar +maintainer_email: foo@bar.org +homepage: https://www.debian.org +section: net +priority: important +control_extra: + postrm: > + #!/bin/bash + echo "post rm!!" + prerm: > + #!/bin/bash + echo "pre rm!!" + postinst: > + #!/bin/bash + echo "post inst!!" + preinst: > + #!/bin/bash + echo "pre inst!!" +` + filepath, err := test.WriteTempFile("debpkg.yml", configFile) + assert.Nil(t, err) + + deb := New() + defer deb.Close() + + assert.Nil(t, deb.Config(filepath)) + assert.Equal(t, "1.2.3", deb.control.info.version.full, + "Unexpected deb.control.info.version.full") + assert.Equal(t, "Mr. Foo Bar", deb.control.info.maintainer, + "Unexpected deb.control.info.maintainer") + assert.Equal(t, "foo@bar.org", deb.control.info.maintainerEmail, + "Unexpected deb.control.info.maintainerEmail") + assert.Equal(t, "https://www.debian.org", deb.control.info.homepage, + "Unexpected deb.control.info.homepage") + assert.Equal(t, "net", deb.control.info.section, + "unexpected section") + assert.Equal(t, PriorityImportant, deb.control.info.priority, + "unexpected priority") + + assert.Nil(t, testWrite(t, deb)) +} + func TestDefaultConfig(t *testing.T) { filepath, err := test.WriteTempFile("emptyfile.yml", "") assert.Nil(t, err) @@ -107,3 +169,27 @@ func TestNonExistingConfig(t *testing.T) { assert.NotNil(t, deb.Config("/non/existent/config/file")) } + +func TestInvalidYAML(t *testing.T) { + deb := New() + defer deb.Close() + + const configFile = `name: debpkg + foo: bar + ` + filepath, err := test.WriteTempFile("debpkg2.yml", configFile) + assert.Nil(t, err) + assert.NotNil(t, deb.Config(filepath)) +} + +func TestInvalidTemplateVar(t *testing.T) { + deb := New() + defer deb.Close() + + const configFile = `name: debpkg +foo: {{.bar}} + ` + filepath, err := test.WriteTempFile("debpkg3.yml", configFile) + assert.Nil(t, err) + assert.NotNil(t, deb.Config(filepath)) +} diff --git a/control.go b/control.go index 6ae0ec5..3f6dc67 100644 --- a/control.go +++ b/control.go @@ -7,6 +7,7 @@ package debpkg import ( "fmt" "io" + "io/ioutil" "math" "strings" @@ -34,10 +35,12 @@ type controlInfo struct { maintainer string maintainerEmail string homepage string + depends string + recommends string suggests string conflicts string - replaces string provides string + replaces string section string priority Priority descrShort string // Short package description @@ -106,6 +109,18 @@ func (deb *DebPkg) SetMaintainerEmail(email string) { deb.control.info.maintainerEmail = email } +// SetDepends sets the package dependencies. E.g: "lsb-release" +// See: https://www.debian.org/doc/debian-policy/ch-relationships.html#s-binarydeps +func (deb *DebPkg) SetDepends(depends string) { + deb.control.info.depends = depends +} + +// SetRecommends sets the package recommendations. E.g: "aptitude" +// See: https://www.debian.org/doc/debian-policy/ch-relationships.html#s-binarydeps +func (deb *DebPkg) SetRecommends(recommends string) { + deb.control.info.recommends = recommends +} + // SetSuggests sets the package suggestions. E.g: "aptitude" // See: https://www.debian.org/doc/debian-policy/ch-relationships.html#s-binarydeps func (deb *DebPkg) SetSuggests(suggests string) { @@ -124,6 +139,12 @@ func (deb *DebPkg) SetProvides(provides string) { deb.control.info.provides = provides } +// SetReplaces sets the names of packages which will be replaced. E.g: "pico" +// See: https://www.debian.org/doc/debian-policy/ch-relationships.html +func (deb *DebPkg) SetReplaces(replaces string) { + deb.control.info.replaces = replaces +} + // SetPriority (recommended). Default set to debpkg.PriorityUnset // See: https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Priority // And: https://www.debian.org/doc/debian-policy/ch-archive.html#s-priorities @@ -138,12 +159,6 @@ func (deb *DebPkg) SetSection(section string) { deb.control.info.section = section } -// SetReplaces sets the names of packages which will be replaced. E.g: "pico" -// See: -func (deb *DebPkg) SetReplaces(replaces string) { - deb.control.info.replaces = replaces -} - // SetHomepage sets the homepage URL of the package. E.g: "https://github.com/foo/bar" func (deb *DebPkg) SetHomepage(url string) { deb.control.info.homepage = url @@ -193,6 +208,7 @@ func (deb *DebPkg) SetBuiltUsing(info string) { // AddControlExtraString is the same as AddControlExtra except it uses a string input func (deb *DebPkg) AddControlExtraString(name, s string) error { + s = strings.Replace(s, "\r\n", "\n", -1) return deb.control.tgz.AddFileFromBuffer(name, []byte(s)) } @@ -200,7 +216,11 @@ func (deb *DebPkg) AddControlExtraString(name, s string) error { // for preinst, postinst, postrm, prerm: https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html // And: https://www.debian.org/doc/manuals/maint-guide/dother.en.html#maintscripts func (deb *DebPkg) AddControlExtra(name, filename string) error { - return deb.control.tgz.AddFile(filename, name) + b, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + return deb.AddControlExtraString(name, string(b)) } // AddConffile adds a file to the conffiles so it is treated as configuration files. Configuration files are not @@ -276,6 +296,25 @@ func (c *control) String(installedSize uint64) string { o += fmt.Sprintf("Built-Using: %s\n", c.info.builtUsing) } + if c.info.depends != "" { + o += fmt.Sprintf("Depends: %s\n", c.info.depends) + } + if c.info.recommends != "" { + o += fmt.Sprintf("Recommends: %s\n", c.info.recommends) + } + if c.info.suggests != "" { + o += fmt.Sprintf("Suggests: %s\n", c.info.suggests) + } + if c.info.conflicts != "" { + o += fmt.Sprintf("Conflicts: %s\n", c.info.conflicts) + } + if c.info.provides != "" { + o += fmt.Sprintf("Provides: %s\n", c.info.provides) + } + if c.info.replaces != "" { + o += fmt.Sprintf("Replaces: %s\n", c.info.replaces) + } + o += fmt.Sprintf("Description: %s\n", c.info.descrShort) o += fmt.Sprintf("%s", c.info.descr) diff --git a/data.go b/data.go index c368773..6c512a9 100644 --- a/data.go +++ b/data.go @@ -5,13 +5,15 @@ package debpkg import ( + "bytes" "crypto/md5" "fmt" - "github.com/xor-gate/debpkg/internal/targzip" "io" "os" "path/filepath" "strings" + + "github.com/xor-gate/debpkg/internal/targzip" ) type data struct { @@ -22,6 +24,10 @@ type data struct { func (d *data) addDirectory(dirpath string) error { dirpath = filepath.Clean(dirpath) + if os.PathSeparator != '/' { + dirpath = strings.Replace(dirpath, string(os.PathSeparator), "/", -1) + } + d.addParentDirectories(dirpath) for _, addedDir := range d.dirs { if addedDir == dirpath { return nil @@ -38,37 +44,38 @@ func (d *data) addDirectory(dirpath string) error { return nil } -func (d *data) addEmptyDirectory(dir string) error { - dirname := strings.Replace(dir, "\\", "/", -1) +func (d *data) addParentDirectories(filename string) { + dirname := filepath.Dir(filename) + if dirname == "." { + return + } + if os.PathSeparator != '/' { + dirname = strings.Replace(dirname, string(os.PathSeparator), "/", -1) + } dirs := strings.Split(dirname, "/") - var current string + current := "/" for _, dir := range dirs { if len(dir) > 0 { current += dir + "/" - err := d.addDirectory(current) - if err != nil { - return err - } + d.addDirectory(current) } } - return nil } -func (d *data) addDirectoriesForFile(filename string) { - dirname := filepath.Dir(filename) - if dirname != "." { - if os.PathSeparator != '/' { - dirname = strings.Replace(dirname, string(os.PathSeparator), "/", -1) - } - dirs := strings.Split(dirname, "/") - var current string - for _, dir := range dirs { - if len(dir) > 0 { - current += dir + "/" - d.addDirectory(current) - } - } +func (d *data) addFileString(contents string, dest string) error { + d.addParentDirectories(dest) + + if err := d.tgz.AddFileFromBuffer(dest, []byte(contents)); err != nil { + return err } + + md5, err := computeMd5(bytes.NewBufferString(contents)) + if err != nil { + return err + } + + d.md5sums += fmt.Sprintf("%x %s\n", md5, dest) + return nil } func (d *data) addFile(filename string, dest ...string) error { @@ -80,7 +87,7 @@ func (d *data) addFile(filename string, dest ...string) error { destfilename = filename } - d.addDirectoriesForFile(destfilename) + d.addParentDirectories(destfilename) // if err := d.tgz.AddFile(filename, dest...); err != nil { diff --git a/data_test.go b/data_test.go new file mode 100644 index 0000000..0050fd2 --- /dev/null +++ b/data_test.go @@ -0,0 +1,71 @@ +package debpkg + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/xor-gate/debpkg/internal/targzip" +) + +func newData(t *testing.T) *data { + tgz, err := targzip.NewTempFile(os.TempDir()) + assert.Nil(t, err) + return &data{tgz: tgz} +} + +func TestDataAddDirectory(t *testing.T) { + d := newData(t) + + err := d.addDirectory("/my/foo/directory/") + assert.Nil(t, err) + assert.Equal(t, []string{"/my", "/my/foo", "/my/foo/directory"}, d.dirs) + assert.Nil(t, d.tgz.Close()) + os.Remove(d.tgz.Name()) +} + +func TestDataAddDirectoryError(t *testing.T) { + d := newData(t) + assert.Nil(t, d.tgz.Close()) + os.Remove(d.tgz.Name()) + err := d.addDirectory("/doesnt/matter") + assert.NotNil(t, err) +} + +func TestDataAddDirectoryCwd(t *testing.T) { + d := newData(t) + err := d.addDirectory(".") + assert.Nil(t, err) + assert.Empty(t, d.dirs) + assert.Nil(t, d.tgz.Close()) + os.Remove(d.tgz.Name()) +} + +func TestDataAddFileString(t *testing.T) { + d := newData(t) + err := d.addFileString("test", "/foo") + assert.Nil(t, err) + assert.Empty(t, d.dirs) + assert.Equal(t, "098f6bcd4621d373cade4e832627b4f6 /foo\n", d.md5sums) + + assert.Nil(t, d.tgz.Close()) + os.Remove(d.tgz.Name()) +} + +func TestDataAddFileStringError(t *testing.T) { + d := newData(t) + assert.Nil(t, d.tgz.Close()) + os.Remove(d.tgz.Name()) + err := d.addFileString("test", "/foo") + assert.NotNil(t, err) + assert.Empty(t, d.md5sums) +} + +func TestDataAddFileError(t *testing.T) { + d := newData(t) + assert.Nil(t, d.tgz.Close()) + os.Remove(d.tgz.Name()) + err := d.addFileString("data_test.go", "/foo/bar.go") + assert.NotNil(t, err) + assert.Empty(t, d.md5sums) +} diff --git a/deb.go b/deb.go index 442188b..d20e463 100644 --- a/deb.go +++ b/deb.go @@ -6,9 +6,10 @@ package debpkg import ( "fmt" - "github.com/xor-gate/debpkg/internal/targzip" "os" "path/filepath" + + "github.com/xor-gate/debpkg/internal/targzip" ) // DebPkg holds data for a single debian package @@ -122,12 +123,20 @@ func (deb *DebPkg) AddFile(filename string, dest ...string) error { return deb.setError(deb.data.addFile(filename, dest...)) } +// AddFileString adds a file to the package with the provided content +func (deb *DebPkg) AddFileString(contents, dest string) error { + if deb.err != nil { + return deb.err + } + return deb.setError(deb.data.addFileString(contents, dest)) +} + // AddEmptyDirectory adds a empty directory to the package func (deb *DebPkg) AddEmptyDirectory(dir string) error { if deb.err != nil { return deb.err } - return deb.setError(deb.data.addEmptyDirectory(dir)) + return deb.setError(deb.data.addDirectory(dir)) } // AddDirectory adds a directory recursive to the package diff --git a/debpkg_test.go b/debpkg_test.go index 6eb49b8..65769dd 100644 --- a/debpkg_test.go +++ b/debpkg_test.go @@ -6,11 +6,12 @@ package debpkg import ( "fmt" - "github.com/stretchr/testify/assert" - "github.com/xor-gate/debpkg/internal/test" "go/build" "os/exec" "testing" + + "github.com/stretchr/testify/assert" + "github.com/xor-gate/debpkg/internal/test" ) // testWrite writes the deb package to a temporary file and verifies with native dpkg tool when available @@ -70,6 +71,8 @@ func TestWrite(t *testing.T) { deb.SetVcsBrowser("https://github.com/xor-gate/secdl") deb.AddFile("debpkg.go") + deb.AddFile("debpkg_test.go", "/foo/awesome/test.go") + deb.AddFileString("this is a real file", "/real/file.txt") assert.Nil(t, testWrite(t, deb)) diff --git a/internal/config/config.go b/internal/config/config.go index f5a632b..e144eed 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,8 +6,9 @@ package config import ( "fmt" - "gopkg.in/yaml.v2" "runtime" + + "gopkg.in/yaml.v2" ) // PkgSpecFile represents a single debian package @@ -19,6 +20,12 @@ type PkgSpecFile struct { MaintainerEmail string `yaml:"maintainer_email"` Homepage string `yaml:"homepage"` Section string `yaml:"section"` + Depends string `yaml:"depends"` + Recommends string `yaml:"recommends"` + Suggests string `yaml:"suggests"` + Conflicts string `yaml:"conflicts"` + Provides string `yaml:"provides"` + Replaces string `yaml:"replaces"` Priority string `yaml:"priority"` BuiltUsing string `yaml:"built_using"` Description struct { @@ -26,11 +33,18 @@ type PkgSpecFile struct { Long string `yaml:"long"` } Files []struct { - Src string `yaml:"file"` - Dest string `yaml:"dest"` + Src string `yaml:"file"` + Dest string `yaml:"dest"` + Content string `yaml:"content"` } `yaml:",flow"` Directories []string `yaml:",flow"` EmptyDirectories []string `yaml:"emptydirs,flow"` + ControlExtra struct { + Preinst string `yaml:"preinst"` + Postinst string `yaml:"postinst"` + Prerm string `yaml:"prerm"` + Postrm string `yaml:"postrm"` + } `yaml:"control_extra"` } // PkgSpecFileUnmarshal loads the configuration data into a PkgSpecFile structure diff --git a/tempdir.go b/tempdir.go index 540d8d6..1187c54 100644 --- a/tempdir.go +++ b/tempdir.go @@ -11,7 +11,7 @@ import ( var tempDir = os.TempDir() // default temporary directory is os.TempDir // SetTempDir sets the directory for temporary files. When the directory doesn't -// exist it is automaticlly created (but not removed). +// exist it is automatically created (but not removed). func SetTempDir(dir string) error { if dir == "" { dir = os.TempDir()