Documentation
¶
Overview ¶
Package hdiutil provides a Go wrapper around the macOS hdiutil command-line tool for creating, manipulating, and signing DMG disk images.
Supported formats and filesystems ¶
The following compressed image formats are supported via [Config.ImageFormat]:
- UDZO — zlib compression (level 9). This is the default and the most widely compatible format.
- UDBZ — bzip2 compression (level 9). Better compression ratio than UDZO at the cost of slower creation and extraction.
- ULFO — lzfse compression. Apple's modern codec; fast with good ratios, but only supported on macOS 10.11+.
- ULMO — lzma compression. Highest compression ratio, slowest speed.
The following filesystem types are supported via [Config.FileSystem]:
- HFS+ — the default; includes tuned allocation parameters (-fsargs -c c=64,a=16,e=16).
- APFS — Apple File System. Cannot be combined with [Config.SandboxSafe].
Configuration ¶
A Config struct holds all settings for image creation. It can be built programmatically or deserialized from JSON with Config.FromJSON. Configs can also be serialized with Config.ToJSON for round-tripping through pipelines or storage.
Config.Validate must be called (either directly or implicitly through Runner.Setup) before the lazy option functions ([Config.FilesystemOpts], [Config.ImageFormatOpts], [Config.VolumeSizeOpts], [Config.VolumeNameOpt]) become usable. Calling them before validation panics.
Required fields:
- [Config.SourceDir] — directory whose contents are copied into the DMG.
- [Config.OutputPath] — destination path; must end in ".dmg".
Optional fields with defaults:
- [Config.VolumeName] — defaults to the output filename without extension (e.g. "MyApp.dmg" → "MyApp").
- [Config.VolumeSizeMb] — when zero, hdiutil sizes the image automatically.
- [Config.ImageFormat] — defaults to "UDZO".
- [Config.FileSystem] — defaults to "HFS+".
Runner lifecycle ¶
New creates a Runner from a Config. The Runner must go through a fixed sequence of steps; calling methods out of order returns an error (typically ErrNeedInit).
cfg := &hdiutil.Config{
SourceDir: "path/to/source",
OutputPath: "output.dmg",
VolumeName: "MyVolume",
}
runner := hdiutil.New(cfg)
defer runner.Cleanup()
// 1. Validate config, create temp directory.
if err := runner.Setup(); err != nil {
log.Fatal(err)
}
// 2. Create a writable temporary image populated from SourceDir.
if err := runner.Start(); err != nil {
log.Fatal(err)
}
// 3. (Optional) Mount the image, modify contents, mark bootable, unmount.
if err := runner.AttachDiskImage(); err != nil {
log.Fatal(err)
}
// ... copy additional files into runner.MountDir, customise .DS_Store, etc.
_ = runner.Bless() // mark as bootable (no-op unless Config.Bless is set)
_ = runner.DetachDiskImage() // fixes permissions and unmounts
// 4. Convert the writable image to the final compressed format.
if err := runner.FinalizeDMG(); err != nil {
log.Fatal(err)
}
// 5. (Optional) Sign and notarize.
if err := runner.Codesign(); err != nil { // no-op when SigningIdentity is empty
log.Fatal(err)
}
if err := runner.Notarize(); err != nil { // no-op when NotarizeCredentials is empty
log.Fatal(err)
}
Runner.Cleanup removes the temporary working directory and is safe to call multiple times.
Sandbox-safe images ¶
Setting [Config.SandboxSafe] uses a two-step process (hdiutil makehybrid + convert) that produces images openable by sandboxed macOS applications. APFS cannot be used in this mode; attempting it returns ErrSandboxAPFS. The Runner.Bless step is also skipped for sandbox-safe images.
Code signing and notarization ¶
When [Config.SigningIdentity] is set, Runner.Codesign signs the final DMG and verifies the signature with --deep --strict. When [Config.NotarizeCredentials] is set to a keychain profile name, Runner.Notarize submits the DMG via xcrun notarytool and staples the ticket with xcrun stapler. Both methods are no-ops when their respective config fields are empty.
Verbosity ¶
[Config.HDIUtilVerbosity] controls the flags passed to hdiutil:
- 0 — no flag (default).
- 1 — -quiet.
- 2 — -verbose.
- 3+ — -debug.
Negative values are treated as 0.
Logging ¶
Internal log messages are discarded by default. Call SetLogWriter with os.Stderr (or any io.Writer) to enable them.
Dry-run mode ¶
Passing the Simulate option to New logs every external command without executing it, which is useful for previewing the hdiutil invocations that would be made. Simulate mode can also be toggled at runtime via Runner.SetSimulate.
Input sanitization ¶
Config.Validate rejects values that could lead to OS command argument injection:
- Null bytes in any string field (SourceDir, OutputPath, VolumeName, SigningIdentity, NotarizeCredentials).
- Paths (SourceDir, OutputPath) that start with a dash after filepath.Clean, which could be misinterpreted as flags by external commands.
Error handling ¶
Sentinel errors are defined for every category of failure and can be matched with errors.Is:
- ErrUnsafeArg — config value contains null bytes or unsafe characters.
- ErrInvSourceDir — empty or missing source directory.
- ErrImageFileExt — output path does not end in ".dmg".
- ErrInvFormatOpt — unsupported image format.
- ErrInvFilesystemOpt — unsupported filesystem.
- ErrVolumeSize — negative volume size.
- ErrSandboxAPFS — sandbox-safe mode with APFS.
- ErrNeedInit — Runner.Setup was not called.
- ErrCreateDir — failed to create temporary directory.
- ErrMountImage — attach/mount failed.
- ErrCodesignFailed — signing or verification failed.
- ErrNotarizeFailed — notarization or stapling failed.
Testing ¶
The CommandExecutor interface and the WithExecutor functional option allow injecting a mock executor into New, so tests can verify command arguments and simulate failures without invoking real binaries. CommandExecutor uses typed methods (Hdiutil, Codesign, Xcrun, Chmod, Bless) rather than a generic Run(name, args...) to ensure that only known commands can be executed and that static analysis tools see literal command names in each exec.Command call.
Index ¶
- Variables
- func SetLogWriter(w io.Writer)
- type CommandExecutor
- type Config
- type OptFn
- type Option
- type Runner
- func (r *Runner) AttachDiskImage() error
- func (r *Runner) Bless() error
- func (r *Runner) Cleanup()
- func (r *Runner) Codesign() error
- func (r *Runner) DetachDiskImage() error
- func (r *Runner) FinalizeDMG() error
- func (r *Runner) Notarize() error
- func (r *Runner) SetSimulate(simulate bool)
- func (r *Runner) Setup() error
- func (r *Runner) Start() error
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrInvSourceDir indicates the source directory is empty or invalid. ErrInvSourceDir = errors.New("invalid source directory") // ErrVolumeSize indicates that a negative volume size. ErrVolumeSize = errors.New("volume size must be >= 0") // ErrInvFormatOpt indicates an unsupported image format was specified. ErrInvFormatOpt = errors.New("invalid image format") // ErrInvFilesystemOpt indicates an unsupported filesystem type was specified. ErrInvFilesystemOpt = errors.New("invalid image filesystem") // ErrCreateDir indicates a failure to create a temporary working directory. ErrCreateDir = errors.New("couldn't create directory") // ErrImageFileExt indicates the output path doesn't have a .dmg extension. ErrImageFileExt = errors.New("output file must have a .dmg extension") // ErrMountImage indicates failure to attach/mount the disk image. ErrMountImage = errors.New("couldn't attach disk image") // ErrCodesignFailed indicates the codesign command failed or signature verification failed. ErrCodesignFailed = errors.New("codesign command failed") // ErrNotarizeFailed indicates Apple notarization or stapling failed. ErrNotarizeFailed = errors.New("notarization failed") // ErrSandboxAPFS indicates an attempt to create a sandbox-safe APFS image, which is unsupported. ErrSandboxAPFS = errors.New("creating an APFS disk image that is sandbox safe is not supported") // ErrNeedInit indicates Runner.Setup was not called before attempting operations. ErrNeedInit = errors.New("runner not properly initialized, call Setup() first") // ErrUnsafeArg indicates a config value contains characters unsafe for command arguments. ErrUnsafeArg = errors.New("argument contains unsafe characters") )
Error variables for common failure conditions during DMG creation.
var HdiutilExe string
Functions ¶
func SetLogWriter ¶
SetLogWriter configures the output writer for verbose logging. By default, verbose logging is discarded. Pass os.Stdout or os.Stderr to enable logging output.
Example ¶
package main
import (
"bytes"
"fmt"
"os"
"al.essio.dev/pkg/hdiutil"
)
func main() {
var buf bytes.Buffer
hdiutil.SetLogWriter(&buf)
defer hdiutil.SetLogWriter(os.Stderr)
cfg := &hdiutil.Config{
SourceDir: "./dist",
OutputPath: "MyApp.dmg",
}
runner := hdiutil.New(cfg, hdiutil.Simulate())
defer runner.Cleanup()
_ = runner.Setup()
_ = runner.Start()
// Log output was captured in buf.
fmt.Println(buf.Len() > 0)
}
Output: true
Types ¶
type CommandExecutor ¶
type CommandExecutor interface {
Hdiutil(args ...string) error
HdiutilOutput(args ...string) (string, error)
Codesign(args ...string) error
Xcrun(args ...string) error
XcrunOutput(args ...string) (string, error)
Chmod(args ...string) error
Bless(args ...string) error
}
CommandExecutor defines the interface for executing external commands. Each method corresponds to a specific allowed command, ensuring that only known binaries can be invoked and satisfying static analysis requirements.
type Config ¶
type Config struct {
// VolumeName is the name of the mounted volume. If empty, it defaults to the output filename without extension.
VolumeName string `json:"volume_name,omitempty"`
// VolumeSizeMb specifies the volume size in megabytes. If zero, hdiutil determines the size automatically.
VolumeSizeMb int64 `json:"volume_size_mb,omitempty"`
// SandboxSafe enables sandbox-safe mode. Cannot be used with APFS filesystem.
SandboxSafe bool `json:"sandbox_safe,omitempty"`
// Bless marks the volume as bootable.
Bless bool `json:"bless,omitempty"`
// FileSystem specifies the filesystem type (e.g., "HFS+", "APFS"). Defaults to "HFS+".
FileSystem string `json:"filesystem,omitempty"`
// SigningIdentity specifies the signing identity to use.
SigningIdentity string `json:"signing_identity,omitempty"`
// NotarizeCredentials contains credentials for Apple notarization.
NotarizeCredentials string `json:"notarize_credentials,omitempty"`
// ImageFormat specifies the DMG format (e.g., "UDZO", "UDBZ", "ULFO", "ULMO"). Defaults to "UDZO".
ImageFormat string `json:"image_format,omitempty"`
// HDIUtilVerbosity controls the verbosity level of hdiutil output.
HDIUtilVerbosity int `json:"hdiutil_verbosity,omitempty"`
// OutputPath is the destination path for the created DMG file. Must have .dmg extension.
OutputPath string `json:"output_path,omitempty"`
// SourceDir is the directory containing files to include in the DMG.
SourceDir string `json:"source_dir,omitempty"`
// FilesystemOpts returns the hdiutil arguments for the configured filesystem.
// Only available after calling Validate.
FilesystemOpts OptFn[[]string] `json:"-"`
// ImageFormatOpts returns the hdiutil arguments for the configured image format.
// Only available after calling Validate.
ImageFormatOpts OptFn[[]string] `json:"-"`
// VolumeSizeOpts returns the hdiutil arguments for the configured volume size.
// Only available after calling Validate.
VolumeSizeOpts OptFn[[]string] `json:"-"`
// VolumeNameOpt returns the resolved volume name.
// Only available after calling Validate.
VolumeNameOpt OptFn[string] `json:"-"`
// contains filtered or unexported fields
}
Config holds the configuration for creating a DMG disk image.
func (*Config) FromJSON ¶
FromJSON populates the Config from a JSON reader.
Example ¶
package main
import (
"fmt"
"strings"
"al.essio.dev/pkg/hdiutil"
)
func main() {
jsonStr := `{
"source_dir": "./dist",
"output_path": "MyApp.dmg",
"volume_name": "My App",
"image_format": "ULFO"
}`
cfg := &hdiutil.Config{}
if err := cfg.FromJSON(strings.NewReader(jsonStr)); err != nil {
fmt.Println(err)
return
}
fmt.Println(cfg.SourceDir)
fmt.Println(cfg.OutputPath)
fmt.Println(cfg.VolumeName)
fmt.Println(cfg.ImageFormat)
}
Output: ./dist MyApp.dmg My App ULFO
func (*Config) ToJSON ¶
ToJSON writes the Config to a JSON writer.
Example ¶
package main
import (
"bytes"
"fmt"
"al.essio.dev/pkg/hdiutil"
)
func main() {
cfg := &hdiutil.Config{
SourceDir: "./dist",
OutputPath: "MyApp.dmg",
VolumeName: "My App",
FileSystem: "HFS+",
}
var buf bytes.Buffer
if err := cfg.ToJSON(&buf); err != nil {
fmt.Println(err)
return
}
fmt.Print(buf.String())
}
Output: { "volume_name": "My App", "filesystem": "HFS+", "output_path": "MyApp.dmg", "source_dir": "./dist" }
func (*Config) Validate ¶
Validate checks the configuration for errors and initializes the option functions. It must be called before using FilesystemOpts, ImageFormatOpts, VolumeSizeOpts, or VolumeNameOpt. Returns an error if:
- Any string field contains a null byte
- SourceDir or OutputPath starts with a dash (argument injection)
- SourceDir is empty
- OutputPath does not have a .dmg extension
- ImageFormat is invalid
- FileSystem is invalid
- SandboxSafe is enabled with APFS filesystem
Example ¶
package main
import (
"fmt"
"al.essio.dev/pkg/hdiutil"
)
func main() {
cfg := hdiutil.Config{
SourceDir: "./dist",
OutputPath: "MyApp.dmg",
VolumeName: "My App",
}
err := cfg.Validate()
fmt.Println(err)
}
Output: <nil>
Example (InvalidFormat) ¶
package main
import (
"errors"
"fmt"
"al.essio.dev/pkg/hdiutil"
)
func main() {
cfg := hdiutil.Config{
SourceDir: "./dist",
OutputPath: "MyApp.dmg",
ImageFormat: "INVALID",
}
err := cfg.Validate()
fmt.Println(errors.Is(err, hdiutil.ErrInvFormatOpt))
}
Output: true
Example (MissingExtension) ¶
package main
import (
"errors"
"fmt"
"al.essio.dev/pkg/hdiutil"
)
func main() {
cfg := hdiutil.Config{
SourceDir: "./dist",
OutputPath: "MyApp.iso",
}
err := cfg.Validate()
fmt.Println(errors.Is(err, hdiutil.ErrImageFileExt))
}
Output: true
Example (SandboxSafeAPFS) ¶
package main
import (
"errors"
"fmt"
"al.essio.dev/pkg/hdiutil"
)
func main() {
cfg := hdiutil.Config{
SourceDir: "./dist",
OutputPath: "MyApp.dmg",
SandboxSafe: true,
FileSystem: "APFS",
}
err := cfg.Validate()
fmt.Println(errors.Is(err, hdiutil.ErrSandboxAPFS))
}
Output: true
Example (UnsafeArg) ¶
package main
import (
"errors"
"fmt"
"al.essio.dev/pkg/hdiutil"
)
func main() {
cfg := hdiutil.Config{
SourceDir: "src\x00evil",
OutputPath: "test.dmg",
}
err := cfg.Validate()
fmt.Println(errors.Is(err, hdiutil.ErrUnsafeArg))
}
Output: true
Example (WithFormat) ¶
package main
import (
"fmt"
"al.essio.dev/pkg/hdiutil"
)
func main() {
cfg := hdiutil.Config{
SourceDir: "./dist",
OutputPath: "MyApp.dmg",
ImageFormat: "UDBZ",
FileSystem: "APFS",
}
err := cfg.Validate()
fmt.Println(err)
}
Output: <nil>
type OptFn ¶
OptFn is a function type that returns a value of type T when called. It is used to lazily compute configuration options after validation.
type Option ¶
type Option func(*Runner)
Option is a functional option for configuring a Runner.
func Simulate ¶ added in v0.2.0
func Simulate() Option
Simulate returns an Option that enables simulate mode on a Runner. When enabled, the Runner skips executing external commands and operates in dry-run mode.
Example ¶
package main
import (
"fmt"
"al.essio.dev/pkg/hdiutil"
)
func main() {
cfg := &hdiutil.Config{
SourceDir: "./dist",
OutputPath: "MyApp.dmg",
VolumeName: "My App",
}
runner := hdiutil.New(cfg, hdiutil.Simulate())
defer runner.Cleanup()
if err := runner.Setup(); err != nil {
fmt.Println(err)
return
}
// All commands are logged but not executed.
if err := runner.Start(); err != nil {
fmt.Println(err)
return
}
fmt.Println("ok")
}
Output: ok
func WithExecutor ¶
func WithExecutor(e CommandExecutor) Option
WithExecutor returns an Option that sets the Runner's CommandExecutor to the provided executor. Useful for injecting a mock or custom executor (for testing or alternative command implementations).
Example ¶
mock := &noopExecutor{}
cfg := &hdiutil.Config{
SourceDir: "./dist",
OutputPath: "MyApp.dmg",
VolumeName: "My App",
}
runner := hdiutil.New(cfg, hdiutil.WithExecutor(mock))
defer runner.Cleanup()
if err := runner.Setup(); err != nil {
fmt.Println(err)
return
}
if err := runner.Start(); err != nil {
fmt.Println(err)
return
}
fmt.Println("ok")
Output: ok
type Runner ¶
type Runner struct {
*Config
// contains filtered or unexported fields
}
Runner orchestrates the DMG creation process, including image creation, mounting, file copying, code signing, and notarization.
func New ¶
New creates a new Runner with the provided configuration. New creates a Runner configured with the provided Config and applies any functional options. It initializes the Runner with a default realCommandExecutor. The returned Runner is not initialized for operation; call Setup on it before use.
Example ¶
package main
import (
"fmt"
"al.essio.dev/pkg/hdiutil"
)
func main() {
cfg := &hdiutil.Config{
SourceDir: "./dist",
OutputPath: "MyApp.dmg",
VolumeName: "My App",
}
runner := hdiutil.New(cfg, hdiutil.Simulate())
defer runner.Cleanup()
if err := runner.Setup(); err != nil {
fmt.Println(err)
return
}
if err := runner.Start(); err != nil {
fmt.Println(err)
return
}
fmt.Println("ok")
}
Output: ok
Example (FullWorkflow) ¶
package main
import (
"fmt"
"al.essio.dev/pkg/hdiutil"
)
func main() {
cfg := &hdiutil.Config{
SourceDir: "./dist",
OutputPath: "MyApp.dmg",
VolumeName: "My App",
ImageFormat: "UDBZ",
FileSystem: "HFS+",
}
runner := hdiutil.New(cfg, hdiutil.Simulate())
defer runner.Cleanup()
if err := runner.Setup(); err != nil {
fmt.Println(err)
return
}
if err := runner.Start(); err != nil {
fmt.Println(err)
return
}
if err := runner.FinalizeDMG(); err != nil {
fmt.Println(err)
return
}
// Codesign and Notarize are no-ops without credentials.
if err := runner.Codesign(); err != nil {
fmt.Println(err)
return
}
if err := runner.Notarize(); err != nil {
fmt.Println(err)
return
}
fmt.Println("ok")
}
Output: ok
Example (SandboxSafe) ¶
package main
import (
"fmt"
"al.essio.dev/pkg/hdiutil"
)
func main() {
cfg := &hdiutil.Config{
SourceDir: "./dist",
OutputPath: "MyApp.dmg",
SandboxSafe: true,
}
runner := hdiutil.New(cfg, hdiutil.Simulate())
defer runner.Cleanup()
if err := runner.Setup(); err != nil {
fmt.Println(err)
return
}
if err := runner.Start(); err != nil {
fmt.Println(err)
return
}
fmt.Println("ok")
}
Output: ok
func (*Runner) AttachDiskImage ¶
AttachDiskImage mounts the temporary disk image and stores the mount point. The image is attached with -nobrowse (hidden from Finder) and -noverify flags. Returns ErrMountImage if it fails or the mount point cannot be determined.
func (*Runner) Bless ¶
Bless marks the mounted volume as bootable using the bless command. This operation is skipped if Config.Bless is false or if SandboxSafe mode is enabled. Bless is typically used for bootable installer images.