Skip to content

Commit

Permalink
Add MacOS installer to packaging
Browse files Browse the repository at this point in the history
This adds the creation of a .dmg files when building packages on MacOS (the
build host must be darwin with XCode installed). The .dmg file contains an
installer (.pkg) and a rudimentary Uninstall.app.  When code signing is enabled
every part of the .dmg is signed. This is the structure of the package.

- beat.dmg
  - beat.pkg
    - internal-beat.pkg
    - BeatPrefPane.pkg
  - Uninstall.app

The following environment variables control the code signing behavior.

- APPLE_SIGNING_ENABLED - Must be set to true to enable signing. Defaults to
  false.
- APPLE_SIGNING_IDENTITY_INSTALLER - filter for selecting the signing identity
  for installers. It's effectively used as
  `security find-identity -v | grep $APPLE_SIGNING_IDENTITY_INSTALLER` to
  select a single certificate.
- APPLE_SIGNING_IDENTITY_APP - filter for selecting the signing identity
  for apps.

Co-authored-by:  Adrian Serrano <adrisr83@gmail.com>
  • Loading branch information
andrewkroh and adriansr committed Jul 3, 2018
1 parent 94fe58a commit 2a58850
Show file tree
Hide file tree
Showing 35 changed files with 1,330 additions and 255 deletions.
1 change: 1 addition & 0 deletions CHANGELOG-developer.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ The list below covers the major changes between 6.3.0 and master only.

- Libbeat provides a global registry for beats developer that allow to register and retrieve plugin. {pull}7392[7392]
- Added more options to control required and optional fields in schema.Apply(), error returned is a plain nil if no error happened {pull}7335[7335]
- Packaging on MacOS now produces a .dmg file containing an installer (.pkg) and uninstaller for the Beat. {pull}7481[7481]
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,9 @@ snapshot:
.PHONY: release
release: beats-dashboards
@$(foreach var,$(BEATS),$(MAKE) -C $(var) release || exit 1;)
@$(foreach var,$(BEATS),mkdir -p build/distributions/$(var) && mv -f $(var)/build/distributions/* build/distributions/$(var)/ || exit 1;)
@$(foreach var,$(BEATS), \
test -d $(var)/build/distributions && test -n "$$(ls $(var)/build/distributions)" || exit 0; \
mkdir -p build/distributions/$(var) && mv -f $(var)/build/distributions/* build/distributions/$(var)/ || exit 1;)

# Builds a snapshot release. The Go version defined in .go-version will be
# installed and used for the build.
Expand Down
7 changes: 5 additions & 2 deletions auditbeat/magefile.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,16 @@ func customizePackaging() {
)

for _, args := range mage.Packages {
switch args.Types[0] {
pkgType := args.Types[0]
switch pkgType {
case mage.TarGz, mage.Zip:
args.Spec.ReplaceFile("{{.BeatName}}.yml", shortConfig)
args.Spec.ReplaceFile("{{.BeatName}}.reference.yml", referenceConfig)
default:
case mage.Deb, mage.RPM, mage.DMG:
args.Spec.ReplaceFile("/etc/{{.BeatName}}/{{.BeatName}}.yml", shortConfig)
args.Spec.ReplaceFile("/etc/{{.BeatName}}/{{.BeatName}}.reference.yml", referenceConfig)
default:
panic(errors.Errorf("unhandled package type: %v", pkgType))
}
}
}
Expand Down
38 changes: 37 additions & 1 deletion dev-tools/mage/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
Expand All @@ -44,6 +45,7 @@ import (
"time"
"unicode"

"github.com/magefile/mage/mg"
"github.com/magefile/mage/sh"
"github.com/magefile/mage/target"
"github.com/magefile/mage/types"
Expand Down Expand Up @@ -600,13 +602,47 @@ func CreateSHA512File(file string) error {
return ioutil.WriteFile(file+".sha512", []byte(out), 0644)
}

// Mage executes mage targets in the specified directory.
func Mage(dir string, targets ...string) error {
c := exec.Command("mage", targets...)
c.Dir = dir
if mg.Verbose() {
c.Env = append(os.Environ(), "MAGEFILE_VERBOSE=1")
}
c.Stdout = os.Stdout
c.Stderr = os.Stderr
c.Stdin = os.Stdin
log.Println("exec:", strings.Join(c.Args, " "))
err := c.Run()
return err
}

// IsUpToDate returns true iff dst exists and is older based on modtime than all
// of the sources.
func IsUpToDate(dst string, sources ...string) bool {
if len(sources) == 0 {
panic("No sources passed to IsUpToDate")
}
execute, err := target.Path(dst, sources...)

var files []string
for _, s := range sources {
filepath.Walk(s, func(path string, info os.FileInfo, err error) error {
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}

if info.Mode().IsRegular() {
files = append(files, path)
}

return nil
})
}

execute, err := target.Path(dst, files...)
return err == nil && !execute
}

Expand Down
286 changes: 286 additions & 0 deletions dev-tools/mage/dmgbuilder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package mage

import (
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/magefile/mage/mg"
"github.com/magefile/mage/sh"
"github.com/pkg/errors"
)

type dmgBuilder struct {
PackageSpec

SigningInfo *AppleSigningInfo
Identifier string
PreferencePaneDir string
PreferencePanePkgFile string
InternalBeatPkg string
BeatPkg string

beatsDir string
dmgDir string

// Build tools.
pkgbuild func(args ...string) error
productbuild func(args ...string) error
spctl func(args ...string) error
codesign func(args ...string) error
hdiutil func(args ...string) error
}

func newDMGBuilder(spec PackageSpec) (*dmgBuilder, error) {
for _, cmd := range []string{"pkgbuild", "productbuild", "spctl", "codesign", "hdiutil"} {
if _, err := exec.LookPath(cmd); err != nil {
return nil, errors.Wrapf(err, "required tool '%v' for DMG packaging not found on PATH", cmd)
}
}

beatsDir, err := ElasticBeatsDir()
if err != nil {
return nil, err
}

preferencePaneDir := filepath.Join(beatsDir, "dev-tools/packer/platforms/darwin/preference-pane")
preferencePanePkgFile := filepath.Join(preferencePaneDir, "build/BeatsPrefPane.pkg")
beatIdentifier, ok := spec.evalContext["identifier"].(string)
if !ok {
return nil, errors.Errorf("identifier not specified for DMG packaging")
}

spec.OutputFile, err = spec.Expand(defaultBinaryName)
if err != nil {
return nil, err
}

info, err := GetAppleSigningInfo()
if err != nil {
return nil, err
}

return &dmgBuilder{
PackageSpec: spec,
SigningInfo: info,
Identifier: beatIdentifier,
PreferencePaneDir: preferencePaneDir,
PreferencePanePkgFile: preferencePanePkgFile,

beatsDir: beatsDir,
dmgDir: filepath.Join(spec.packageDir, "dmg"),

pkgbuild: sh.RunCmd("pkgbuild"),
productbuild: sh.RunCmd("productbuild"),
spctl: sh.RunCmd("spctl", "-a", "-t"),
codesign: sh.RunCmd("codesign"),
hdiutil: sh.RunCmd("hdiutil"),
}, nil
}

// Create .pkg for preference pane.
func (b *dmgBuilder) buildPreferencePane() error {
return errors.Wrap(Mage(b.PreferencePaneDir), "failed to build Beats preference pane")
}

func (b *dmgBuilder) buildBeatPkg() error {
beatPkgRoot := filepath.Join(b.packageDir, "beat-pkg-root")
if err := os.RemoveAll(beatPkgRoot); err != nil {
return errors.Wrap(err, "failed to clean beat-pkg-root")
}

// Copy files into the packaging root and set their mode.
for _, f := range b.Files {
target := filepath.Join(beatPkgRoot, f.Target)
if err := Copy(f.Source, target); err != nil {
return err
}

info, err := os.Stat(target)
if err != nil {
return err
}

if info.Mode().IsRegular() && info.Mode().Perm() != f.Mode {
if err = os.Chmod(target, f.Mode); err != nil {
return err
}
}
}

b.InternalBeatPkg = filepath.Join(b.packageDir, "pkgs", "internal-"+b.OutputFile+".pkg")

args := []string{
"--root", beatPkgRoot,
"--scripts", filepath.Join(b.packageDir, "scripts"),
"--identifier", b.Identifier,
"--version", b.MustExpand("{{.Version}}{{if .Snapshot}}-SNAPSHOT{{end}}"),
}
if b.SigningInfo.Sign {
args = append(args, "--sign", b.SigningInfo.Installer.ID, "--timestamp")
}
args = append(args, createDir(b.InternalBeatPkg))
if err := b.pkgbuild(args...); err != nil {
return err
}

return nil
}

func (b *dmgBuilder) buildProductPkg() error {
var (
distributionPlist = filepath.Join(b.packageDir, "distributions.plist")
resourcesDir = filepath.Join(b.packageDir, "resources")
)

b.MustExpandFile(
filepath.Join(b.beatsDir, "dev-tools/mage/templates/darwin/distribution.plist.tmpl"),
distributionPlist)
b.MustExpandFile(
filepath.Join(b.beatsDir, "dev-tools/mage/templates/darwin/README.html.tmpl"),
filepath.Join(resourcesDir, "README.html"))
for t, pf := range b.Files {
if strings.HasSuffix(t, "LICENSE.txt") {
Copy(pf.Source, filepath.Join(resourcesDir, "LICENSE.txt"))
break
}
}
b.MustExpandFile(
filepath.Join(b.beatsDir, "dev-tools/mage/templates/darwin/README.html.tmpl"),
filepath.Join(resourcesDir, "README.html"))

if err := os.RemoveAll(b.dmgDir); err != nil {
return err
}
b.BeatPkg = filepath.Join(b.dmgDir, b.OutputFile+".pkg")

// Create .pkg containing the previous two .pkg files.
args := []string{
"--distribution", distributionPlist,
"--resources", resourcesDir,
"--package-path", filepath.Dir(b.InternalBeatPkg),
"--package-path", filepath.Dir(b.PreferencePanePkgFile),
"--component-compression", "auto",
}
if b.SigningInfo.Sign {
args = append(args, "--sign", b.SigningInfo.Installer.ID, "--timestamp")
}
args = append(args, createDir(b.BeatPkg))
if err := b.productbuild(args...); err != nil {
return err
}

if b.SigningInfo.Sign {
if err := b.spctl("install", b.BeatPkg); err != nil {
return err
}
}

return nil
}

func (b *dmgBuilder) buildUninstallApp() error {
const (
uninstallerIcons = "Uninstall.app/Contents/Resources/uninstaller.icns"
uninstallScript = "Uninstall.app/Contents/MacOS/uninstall.sh"
infoPlist = "Uninstall.app/Contents/Info.plist"
)

inputDir := filepath.Join(b.beatsDir, "dev-tools/mage/templates/darwin/dmg")

Copy(
filepath.Join(inputDir, uninstallerIcons),
filepath.Join(b.dmgDir, uninstallerIcons),
)
b.MustExpandFile(
filepath.Join(inputDir, infoPlist+".tmpl"),
filepath.Join(b.dmgDir, infoPlist),
)
b.MustExpandFile(
filepath.Join(inputDir, uninstallScript+".tmpl"),
filepath.Join(b.dmgDir, uninstallScript),
)
if err := os.Chmod(filepath.Join(b.dmgDir, uninstallScript), 0755); err != nil {
return err
}

if b.SigningInfo.Sign {
uninstallApp := filepath.Join(b.dmgDir, "Uninstall.app")
if err := b.codesign("-s", b.SigningInfo.App.ID, "--timestamp", uninstallApp); err != nil {
return err
}

if err := b.spctl("exec", uninstallApp); err != nil {
return err
}
}

return nil
}

// Create a .dmg file containing both the Uninstall.app and .pkg file.
func (b *dmgBuilder) buildDMG() error {
dmgFile := filepath.Join(distributionsDir, DMG.AddFileExtension(b.OutputFile))

args := []string{
"create",
"-volname", b.MustExpand("{{.BeatName | title}} {{.Version}}{{if .Snapshot}}-SNAPSHOT{{end}}"),
"-srcfolder", b.dmgDir,
"-ov",
createDir(dmgFile),
}
if err := b.hdiutil(args...); err != nil {
return err
}

// Sign the .dmg.
if b.SigningInfo.Sign {
if err := b.codesign("-s", b.SigningInfo.App.ID, "--timestamp", dmgFile); err != nil {
return err
}

if err := b.spctl("install", dmgFile); err != nil {
return err
}
}

return errors.Wrap(CreateSHA512File(dmgFile), "failed to create .sha512 file")
}

func (b *dmgBuilder) Build() error {
// Mark this function as a dep so that is is only invoked once.
mg.Deps(b.buildPreferencePane)

var err error
if err = b.buildBeatPkg(); err != nil {
return errors.Wrap(err, "failed to build internal beat pkg")
}
if err = b.buildProductPkg(); err != nil {
return errors.Wrap(err, "failed to build beat product pkg (pref pane + beat)")
}
if err = b.buildUninstallApp(); err != nil {
return errors.Wrap(err, "failed to build Uninstall.app")
}
if err = b.buildDMG(); err != nil {
return errors.Wrap(err, "failed to build beat dmg")
}
return nil
}
Loading

0 comments on commit 2a58850

Please # to comment.