Files
ipsw/internal/utils/macos.go

913 lines
27 KiB
Go

package utils
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/apex/log"
"github.com/blacktop/go-plist"
"github.com/blacktop/ipsw/internal/magic"
"github.com/blacktop/ipsw/internal/utils/lsof"
semver "github.com/hashicorp/go-version"
"golang.org/x/sys/execabs"
)
// Cp copies a file from src to dest
func Cp(src, dst string) error {
from, err := os.Open(src)
if err != nil {
return err
}
defer from.Close()
to, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE, 0660)
if err != nil {
return err
}
defer to.Close()
_, err = io.Copy(to, from)
return err
}
// MkdirAndCopy recursively copies src into dst with src's file modes while also creating the outer directory
func MkdirAndCopy(src, dst string) error {
if err := os.MkdirAll(filepath.Dir(dst), 0750); err != nil {
return err
}
return CopyWithMode(src, dst, 0)
}
// Copy recursively copies src into dst with src's file modes.
// CREDIT: https://github.com/goreleaser/goreleaser/blob/main/internal/gio/copy.go
func Copy(src, dst string) error {
return CopyWithMode(src, dst, 0)
}
// CopyWithMode recursively copies src into dst with the given mode.
// The given mode applies only to files. Their parent dirs will have the same mode as their src counterparts.
func CopyWithMode(src, dst string, mode os.FileMode) error {
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("failed to copy %s to %s: %w", src, dst, err)
}
// We have the following:
// - src = "a/b"
// - dst = "dist/linuxamd64/b"
// - path = "a/b/c.txt"
// So we join "a/b" with "c.txt" and use it as the destination.
dst := filepath.Join(dst, strings.Replace(path, src, "", 1))
if info.IsDir() {
return os.MkdirAll(dst, info.Mode())
}
if info.Mode()&os.ModeSymlink != 0 {
return copySymlink(path, dst)
}
if mode != 0 {
return copyFile(path, dst, mode)
}
return copyFile(path, dst, info.Mode())
})
}
func copySymlink(src, dst string) error {
src, err := os.Readlink(src)
if err != nil {
return err
}
return os.Symlink(src, dst)
}
func copyFile(src, dst string, mode os.FileMode) error {
original, err := os.Open(src)
if err != nil {
return fmt.Errorf("failed to open '%s': %w", src, err)
}
defer original.Close()
new, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
if err != nil {
return fmt.Errorf("failed to open '%s': %w", dst, err)
}
defer new.Close()
if _, err := io.Copy(new, original); err != nil {
return fmt.Errorf("failed to copy: %w", err)
}
return nil
}
// CodeSign codesigns a given binary
func CodeSign(filePath, signature string) error {
if runtime.GOOS == "darwin" {
filePath = filepath.Clean(filePath)
cmd := exec.Command("/usr/bin/codesign", "-s", "-", "-f", "-s", signature, "-f", filePath)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%v: %s", err, out)
}
return nil
}
return fmt.Errorf("only supported on macOS")
}
// CodeSignWithEntitlements codesigns a given binary with given entitlements
func CodeSignWithEntitlements(filePath, entitlementsPath, signature string) error {
if runtime.GOOS != "darwin" {
return fmt.Errorf("only supported on macOS")
}
return Retry(2, 0, func() error {
Indent(log.Info, 2)(fmt.Sprintf("Codesigning '%s' with entitlements", filepath.Base(filePath)))
cmd := exec.Command("/usr/bin/codesign", "--entitlements", entitlementsPath, "-s", signature, "-f", filepath.Clean(filePath))
out, err := cmd.CombinedOutput()
if err == nil {
return nil
}
if strings.Contains(string(out), "AMFIUnserializeXML: syntax error") {
Indent(log.Error, 2)(fmt.Sprintf("%v: %s", err, strings.TrimSpace(string(out))))
Indent(log.Info, 2)(fmt.Sprintf("Converting entitlements file '%s' to XML1 format", entitlementsPath))
convertCmd := exec.Command("/usr/bin/plutil", "-convert", "xml1", entitlementsPath)
convertOut, convertErr := convertCmd.CombinedOutput()
if convertErr != nil {
return &StopRetryingError{Err: fmt.Errorf("%v: %s", convertErr, strings.TrimSpace(string(convertOut)))}
}
return fmt.Errorf("%v: %s", err, strings.TrimSpace(string(out)))
}
return &StopRetryingError{Err: fmt.Errorf("%v: %s", err, strings.TrimSpace(string(out)))}
})
}
// CodeSignAdHoc codesigns a given binary with ad-hoc signature
func CodeSignAdHoc(filePath string) error {
return CodeSign(filePath, "-")
}
func CodesignVerify(path string) (string, error) {
if runtime.GOOS == "darwin" {
cmd := exec.Command("/usr/bin/codesign", "--verify", "--deep", "--strict", "--verbose=4", path)
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("%v: %s", err, out)
}
return string(out), nil
}
return "", fmt.Errorf("only supported on macOS")
}
func CodesignShow(path string) (string, error) {
if runtime.GOOS == "darwin" {
cmd := exec.Command("/usr/bin/codesign", "-d", "--verbose=4", path)
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("%v: %s", err, out)
}
return string(out), nil
}
return "", fmt.Errorf("only supported on macOS")
}
// CreateSparseDiskImage creates a sparse disk image and returns it's path
func CreateSparseDiskImage(volumeName, diskPath string) (string, error) {
if runtime.GOOS == "darwin" {
cmd := exec.Command("/usr/bin/hdiutil", "create", "-size", "16g", "-fs", "HFS+", "-volname", volumeName, "-type", "SPARSE", "-plist", diskPath)
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("%v: %s", err, out)
}
var paths []string
if err := plist.NewDecoder(bytes.NewReader(out)).Decode(&paths); err != nil {
return "", fmt.Errorf("failed to parse hdiutil output plist: %v", err)
}
return paths[0], nil
}
return "", fmt.Errorf("only supported on macOS")
}
// CreateCompressedDMG creates a compressed r/o disk image containing Install macOS.app
func CreateCompressedDMG(appPath, diskimagePath string) error {
if runtime.GOOS == "darwin" {
cmd := exec.Command("/usr/bin/hdiutil", "create", "-fs", "HFS+", "-srcfolder", appPath, diskimagePath)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%v: %s", err, out)
}
return nil
}
return fmt.Errorf("only supported on macOS")
}
// CreateInstaller creates an macOS installer
func CreateInstaller(distPath, targetVol string) error {
if runtime.GOOS == "darwin" {
cmd := exec.Command("/usr/sbin/installer", "-pkg", distPath, "-target", targetVol)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "CM_BUILD=CM_BUILD")
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%v: %s", err, out)
}
if _, err := os.Stat(targetVol + "Applications"); os.IsNotExist(err) {
cmd := exec.Command("/usr/bin/ditto", targetVol+"Applications", filepath.Join(targetVol, "Applications"))
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%v: %s", err, out)
}
cmd = exec.Command("/bin/rm", "-r", targetVol+"Applications")
out, err = cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%v: %s", err, out)
}
}
return nil
}
return fmt.Errorf("only supported on macOS")
}
type IORegistryEntryChild struct {
IOBusyInterest string
IOConsoleSecurityInterest string
IOInterruptControllers []string
IOInterruptSpecifiers [][]byte
IOObjectClass string
IOObjectRetainCount int
IOPlatformSerialNumber string
IOPlatformSystemSleepPolicy []byte
IOPlatformUUID string
IOPolledInterface string
IORegistryEntryID int
IORegistryEntryName string
IOServiceBusyState int
IOServiceBusyTime int
IOServiceState int
BoardID []byte `plist:"board-id,omitempty"`
BridgeModel []byte `plist:"bridge-model,omitempty"`
ClockFrequency []byte `plist:"clock-frequency,omitempty"`
Compatible []byte `plist:"compatible,omitempty"`
Manufacturer []byte `plist:"manufacturer,omitempty"`
Model []byte `plist:"model,omitempty"`
Name []byte `plist:"name,omitempty"`
PlatformFeature []byte `plist:"platform-feature,omitempty"`
ProductName []byte `plist:"product-name,omitempty"`
SerialNumber []byte `plist:"serial-number,omitempty"`
SystemType []byte `plist:"system-type,omitempty"`
TargetType []byte `plist:"target-type,omitempty"`
Version []byte `plist:"version,omitempty"`
}
func (e IORegistryEntryChild) String() string {
return fmt.Sprintf(
"Name: %s\n"+
"Model: %s\n"+
"ProductName: %s\n"+
"Compatible: %s\n"+
"BridgeModel: %s\n"+
"BoardID: %s\n"+
"TargetType: %s\n"+
"SystemType: %d\n"+
"PlatformFeature: %s\n"+
"ClockFrequency: %#x\n"+
"Manufacturer: %s\n"+
"Version: %s\n",
string(e.Name),
string(e.Model),
string(e.ProductName),
string(e.Compatible),
string(e.BridgeModel),
string(e.BoardID),
string(e.TargetType),
e.SystemType,
string(e.PlatformFeature),
binary.BigEndian.Uint32(e.ClockFrequency),
string(e.Manufacturer),
string(e.Version),
)
}
type DeviceID struct {
IOObjectClass string
IOObjectRetainCount int
IORegistryEntryChildren []IORegistryEntryChild
IORegistryEntryID int
IORegistryEntryName string
}
// GetDeviceID returns the ioreg IOPlatformExpertDevice of the current device
func GetDeviceID() (*DeviceID, error) {
if runtime.GOOS == "darwin" {
cmd := exec.Command("/usr/sbin/ioreg", "-c", "IOPlatformExpertDevice", "-d", "2", "-a")
out, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("%v: %s", err, out)
}
var dID DeviceID
if err := plist.NewDecoder(bytes.NewReader(out)).Decode(&dID); err != nil {
return nil, fmt.Errorf("failed to parse hdiutil output plist: %v", err)
}
return &dID, nil
}
return nil, fmt.Errorf("only supported on macOS")
}
type BuildInfo struct {
ProductNames string
ProductVersion string
BuildVersion string
}
// GetBuildInfo returns the current device OS build info
func GetBuildInfo() (*BuildInfo, error) {
if runtime.GOOS == "darwin" {
var binfo BuildInfo
cmd := exec.Command("/usr/bin/sw_vers", "-productName")
out, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("%v: %s", err, out)
}
binfo.ProductNames = strings.TrimSuffix(string(out), "\n")
cmd = exec.Command("/usr/bin/sw_vers", "-productVersion")
out, err = cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("%v: %s", err, out)
}
binfo.ProductVersion = strings.TrimSuffix(string(out), "\n")
cmd = exec.Command("/usr/bin/sw_vers", "-buildVersion")
out, err = cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("%v: %s", err, out)
}
binfo.BuildVersion = strings.TrimSuffix(string(out), "\n")
return &binfo, nil
}
return nil, fmt.Errorf("only supported on macOS")
}
func GetXCodePath() (string, error) {
if runtime.GOOS == "darwin" {
cmd := exec.Command("/usr/bin/xcode-select", "-p")
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("%v: %s", err, out)
}
return strings.TrimSpace(string(out)), nil
}
return "", fmt.Errorf("only supported on macOS")
}
type xcodeVersionPlist struct {
BuildVersion string `json:"BuildVersion"`
CFBundleShortVersionString string `json:"CFBundleShortVersionString"`
CFBundleVersion string `json:"CFBundleVersion"`
ProductBuildVersion string `json:"ProductBuildVersion"`
ProjectName string `json:"ProjectName"`
SourceVersion string `json:"SourceVersion"`
}
func GetXCodeVersion(path ...string) (string, error) {
if runtime.GOOS == "darwin" {
if len(path) == 0 {
cmd := exec.Command("/usr/bin/xcodebuild", "-version")
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("%v: %s", err, out)
}
version, _, ok := strings.Cut(strings.TrimSpace(string(out)), "\n")
if !ok {
return "", fmt.Errorf("failed to parse xcodebuild version: %s", out)
}
return strings.TrimPrefix(version, "Xcode "), nil
} else {
f, err := os.Open(filepath.Join(path[0], "Contents", "version.plist"))
if err != nil {
return "", fmt.Errorf("failed to open Xcode version.plist: %v", err)
}
defer f.Close()
var version xcodeVersionPlist
if err := plist.NewDecoder(f).Decode(&version); err != nil {
return "", fmt.Errorf("failed to decode Xcode version.plist: %v", err)
}
return version.CFBundleShortVersionString, nil
}
}
return "", fmt.Errorf("only supported on macOS")
}
func GetKernelPath() (string, error) {
if runtime.GOOS == "darwin" {
cmd := exec.Command("uname", "-a")
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("%v: %s", err, out)
}
uname := strings.TrimSpace(string(out))
_, triple, ok := strings.Cut(uname, "/")
if !ok {
return "", fmt.Errorf("failed to parse uname: %s", uname)
}
tripleParts := strings.Split(triple, " ")
if len(tripleParts) != 2 {
return "", fmt.Errorf("failed to parse uname: %s", uname)
}
tparts := strings.Split(tripleParts[0], "_")
if len(tparts) != 3 {
return "", fmt.Errorf("failed to parse uname: %s", uname)
}
return fmt.Sprintf("/System/Library/Kernels/kernel.release.%s", strings.ToLower(tparts[2])), nil
}
return "", fmt.Errorf("only supported on macOS")
}
func GetKernelCollectionPath() (string, error) {
if runtime.GOOS == "darwin" {
defaultKcPath := "/System/Library/KernelCollections/BootKernelExtensions.kc"
if runtime.GOARCH == "amd64" {
return defaultKcPath, nil
}
cmd := exec.Command("sysctl", "-n", "kern.bootobjectspath")
out, err := cmd.CombinedOutput()
if err != nil {
return defaultKcPath, nil
}
return filepath.Join("/System/Volumes/Preboot", strings.TrimSpace(string(out)), "System/Library/Caches/com.apple.kernelcaches/kernelcache"), nil
}
return "", fmt.Errorf("only supported on macOS")
}
var ErrMountResourceBusy = errors.New("hdiutil: mount failed - Resource busy")
// Mount mounts a DMG with hdiutil
func Mount(image, mountPoint string) error {
if runtime.GOOS == "darwin" {
out, err := exec.Command("/usr/bin/hdiutil", "attach", "-noverify", "-mountpoint", mountPoint, image).CombinedOutput()
if err != nil {
if strings.Contains(string(out), "hdiutil: mount failed - Resource busy") {
return ErrMountResourceBusy
}
return fmt.Errorf("%v: %s", err, out)
}
} else {
// On Linux, detect filesystem type and use appropriate FUSE tool
return mountLinux(image, mountPoint)
}
return nil
}
// MountedFilesystemRoot returns the root directory of a mounted filesystem.
// apfs-fuse exposes APFS volume contents below <mountPoint>/root; hdiutil and
// hfsfuse expose the volume contents directly at <mountPoint>.
func MountedFilesystemRoot(mountPoint string) string {
mountPoint = filepath.Clean(mountPoint)
rootPath := filepath.Join(mountPoint, "root")
rootInfo, err := os.Stat(rootPath)
if err != nil || !rootInfo.IsDir() {
return mountPoint
}
systemPath := filepath.Join(mountPoint, "System")
if _, err := os.Stat(systemPath); !os.IsNotExist(err) {
return mountPoint
}
return rootPath
}
// mountLinux mounts a DMG on Linux using the appropriate FUSE tool based on filesystem type
func mountLinux(image, mountPoint string) error {
// First, try to detect the filesystem type
isAPFSFilesystem, err := magic.IsAPFS(image)
if err != nil {
log.Warnf("failed to detect APFS filesystem: %v", err)
}
isHFSPlusFilesystem, err := magic.IsHFSPlus(image)
if err != nil {
log.Warnf("failed to detect HFS+ filesystem: %v", err)
}
if isAPFSFilesystem {
log.Debugf("detected APFS filesystem, using apfs-fuse")
return mountWithAPFSFuse(image, mountPoint)
} else if isHFSPlusFilesystem {
log.Debugf("detected HFS+ filesystem, using native kernel support")
return mountWithHFSPlus(image, mountPoint)
} else {
// Fallback: try APFS first, then HFS+ if that fails
log.Debugf("filesystem type unclear, trying apfs-fuse first")
if err := mountWithAPFSFuse(image, mountPoint); err != nil {
log.Debugf("apfs-fuse failed (%v), trying native HFS+ support", err)
return mountWithHFSPlus(image, mountPoint)
}
return nil
}
}
// findApfsFuse attempts to locate apfs-fuse binary using multiple strategies
func findApfsFuse() (string, error) {
// Check environment variable override first
if envPath := os.Getenv("IPSW_APFS_FUSE_PATH"); envPath != "" {
if _, err := os.Stat(envPath); err == nil {
return envPath, nil
}
log.Warnf("IPSW_APFS_FUSE_PATH points to non-existent file: %s", envPath)
}
// Try standard PATH lookup
if path, err := execabs.LookPath("apfs-fuse"); err == nil {
return path, nil
}
// Check common installation locations (helpful for snap/confined environments)
commonPaths := []string{
"/usr/local/bin/apfs-fuse",
"/usr/bin/apfs-fuse",
"/opt/apfs-fuse/bin/apfs-fuse",
"/snap/bin/apfs-fuse",
"/home/linuxbrew/.linuxbrew/bin/apfs-fuse",
}
for _, path := range commonPaths {
if _, err := os.Stat(path); err == nil {
return path, nil
}
}
return "", fmt.Errorf("apfs-fuse not found in PATH or common locations: " +
"Install apfs-fuse or set IPSW_APFS_FUSE_PATH environment variable. " +
"(snap users: try 'sudo snap connect ipsw:system-files' if available)")
}
// mountWithAPFSFuse mounts using apfs-fuse
func mountWithAPFSFuse(image, mountPoint string) error {
apfsFusePath, err := findApfsFuse()
if err != nil {
return fmt.Errorf("failed to find apfs-fuse: %v", err)
}
out, err := exec.Command(apfsFusePath, image, mountPoint).CombinedOutput()
if err != nil {
return fmt.Errorf("failed to mount '%s' via apfs-fuse: %v: cmd output - '%s'", image, err, out)
}
return nil
}
// mountWithHFSPlus mounts using native Linux HFS+ kernel support
func mountWithHFSPlus(image, mountPoint string) error {
// Set up a loop device for the DMG file
out, err := exec.Command("losetup", "-f", "--show", image).CombinedOutput()
if err != nil {
return fmt.Errorf("failed to set up loop device for '%s': %v: cmd output - '%s'", image, err, out)
}
loopDevice := strings.TrimSpace(string(out))
log.Debugf("created loop device: %s", loopDevice)
// Mount the loop device using native HFS+ support
err = exec.Command("mount", "-t", "hfsplus", "-o", "ro", loopDevice, mountPoint).Run()
if err != nil {
// Clean up loop device on mount failure
if cleanupErr := exec.Command("losetup", "-d", loopDevice).Run(); cleanupErr != nil {
log.Warnf("failed to clean up loop device %s: %v", loopDevice, cleanupErr)
}
return fmt.Errorf("failed to mount HFS+ filesystem '%s': %v (ensure hfsplus kernel module is loaded)", loopDevice, err)
}
log.Debugf("mounted HFS+ filesystem at %s", mountPoint)
return nil
}
func MountEncrypted(image, mountPoint, password string) error {
if runtime.GOOS == "darwin" {
cmd := exec.Command("/usr/bin/hdiutil", "attach", "-noverify", "-mountpoint", mountPoint, "-stdinpass", image)
cmd.Stdin = strings.NewReader(password)
out, err := cmd.CombinedOutput()
if err != nil {
if strings.Contains(string(out), "hdiutil: mount failed - Resource busy") {
return ErrMountResourceBusy
}
return fmt.Errorf("%v: %s", err, out)
}
return nil
}
return fmt.Errorf("only supported on macOS")
}
func IsAlreadyMounted(image, mountPoint string) (string, bool, error) {
switch runtime.GOOS {
case "darwin":
img := filepath.Clean(image)
if abs, err := filepath.Abs(img); err == nil {
img = abs
}
info, err := MountInfo()
if err != nil {
return "", false, err
}
for _, i := range info.Images {
ip := filepath.Clean(i.ImagePath)
if ip == img {
for _, entry := range i.SystemEntities {
if entry.MountPoint != "" {
return entry.MountPoint, true, nil
}
}
// Image matches, but no mounted filesystem entity was reported.
// Treat as not mounted so callers can attempt to (re)attach.
break
}
}
case "linux":
if _, err := os.Stat(filepath.Join(mountPoint, "root")); !os.IsNotExist(err) {
return mountPoint, true, nil
}
}
return "", false, nil
}
func MountDMG(image string, customMountPoint string) (string, bool, error) {
image = filepath.Clean(image)
if abs, err := filepath.Abs(image); err == nil {
image = abs
}
var mountPoint string
if customMountPoint != "" {
mountPoint = customMountPoint
} else {
base := filepath.Base(image)
mountPoint = fmt.Sprintf("/tmp/%s.mount", base)
}
if runtime.GOOS == "darwin" {
// check if already mounted
if prevMountPoint, mounted, err := IsAlreadyMounted(image, mountPoint); mounted && err == nil {
if prevMountPoint != "" {
mountPoint = prevMountPoint
}
return mountPoint, true, nil
}
} else {
if err := os.MkdirAll(mountPoint, 0750); err != nil {
return "", false, fmt.Errorf("failed to create temporary mount point %s: %v", mountPoint, err)
}
}
if err := Mount(image, mountPoint); err != nil {
return "", false, fmt.Errorf("failed to mount %s: %v", image, err)
}
return mountPoint, false, nil
}
// Unmount unmounts a DMG with hdiutil
func Unmount(mountPoint string, force bool) error {
switch runtime.GOOS {
case "darwin":
var cmd *exec.Cmd
if force {
cmd = exec.Command("hdiutil", "detach", mountPoint, "-force")
} else {
cmd = exec.Command("hdiutil", "detach", mountPoint)
}
if err := cmd.Run(); err != nil {
var edetail string
if strings.Contains(err.Error(), "exit status 16") {
edetail = " (Resource busy)"
}
procs, lerr := lsof.MountPoint(mountPoint)
if lerr == nil {
edetail += "\n Processes using mount point:"
for _, proc := range procs {
edetail += fmt.Sprintf("\n %s", proc)
}
edetail += "\n"
}
return fmt.Errorf("failed to unmount %s: %v%s", mountPoint, err, edetail)
}
case "linux":
// First unmount the filesystem
cmd := exec.Command("umount", mountPoint)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to unmount %s: %v", mountPoint, err)
}
// Note: Loop devices created for HFS+ mounting are automatically cleaned up
// by the kernel when the filesystem is unmounted, so no explicit cleanup needed
}
return nil
}
type systemEntry struct {
ContentHint string `plist:"content-hint,omitempty" xml:"content-hint,omitempty"`
DevEntry string `plist:"dev-entry,omitempty" xml:"dev-entry,omitempty"`
MountPoint string `plist:"mount-point,omitempty" xml:"mount-point,omitempty"`
}
type image struct {
Autodiskmount bool `plist:"autodiskmount,omitempty" xml:"autodiskmount,omitempty"`
BlockCount int64 `plist:"blockcount,omitempty" xml:"blockcount,omitempty"`
BlockSize int64 `plist:"blocksize,omitempty" xml:"blocksize,omitempty"`
DiskImages2 bool `plist:"diskimages2,omitempty" xml:"diskimages2,omitempty"`
HdidPID int64 `plist:"hdid-pid,omitempty" xml:"hdid-pid,omitempty"`
IconPath string `plist:"icon-path,omitempty" xml:"icon-path,omitempty"`
ImageEncrypted bool `plist:"image-encrypted,omitempty" xml:"image-encrypted,omitempty"`
ImagePath string `plist:"image-path,omitempty" xml:"image-path,omitempty"`
ImageType string `plist:"image-type,omitempty" xml:"image-type,omitempty"`
OwnerUID int64 `plist:"owner-uid,omitempty" xml:"owner-uid,omitempty"`
Removable bool `plist:"removable,omitempty" xml:"removable,omitempty"`
SystemEntities []systemEntry `plist:"system-entities,omitempty" xml:"system-entities,omitempty"`
}
type HdiUtilInfo struct {
Framework string `plist:"framework,omitempty" xml:"framework,omitempty"`
Revision string `plist:"revision,omitempty" xml:"revision,omitempty"`
Vendor string `plist:"vendor,omitempty" xml:"vendor,omitempty"`
Images []image `plist:"images,omitempty" xml:"images,omitempty"`
}
func (i HdiUtilInfo) Mount(mount string) *image {
for _, img := range i.Images {
for _, entry := range img.SystemEntities {
if strings.Contains(entry.MountPoint, mount) {
return &img
}
}
}
return nil
}
func MountInfo() (*HdiUtilInfo, error) {
if runtime.GOOS == "darwin" {
cmd := exec.Command("hdiutil", "info", "-plist")
out, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("%v: %s", err, out)
}
var info HdiUtilInfo
if err := plist.NewDecoder(bytes.NewReader(out)).Decode(&info); err != nil {
return nil, fmt.Errorf("failed to decode hdiutil info plist: %v", err)
}
return &info, nil
}
return nil, fmt.Errorf("only supported on macOS")
}
func PkgUtilExpand(src, dst string) (string, error) {
if runtime.GOOS == "darwin" {
// cmd := exec.Command("pkgutil", "--expand-full", name, filepath.Join(os.TempDir(), "macosupd"))
outDir := filepath.Join(dst, "macosupd")
cmd := exec.Command("pkgutil", "--expand", src, outDir)
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("%v: %s", err, out)
}
return outDir, nil
}
return "", fmt.Errorf("only supported on macOS")
}
func InstallXCodeSimRuntime(path string) error {
if runtime.GOOS == "darwin" {
cmd := exec.Command("xcrun", "simctl", "runtime", "add", path)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%v: %s", err, out)
}
return nil
}
return fmt.Errorf("only supported on macOS")
}
func InstallKDK(path string) error {
if runtime.GOOS == "darwin" {
cmd := exec.Command("hdiutil", "attach", path)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%v: %s", err, out)
}
cmd = exec.Command("sudo", "installer", "-pkg", "/Volumes/Kernel Debug Kit/KernelDebugKit.pkg", "-target", "/")
out, err = cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%v: %s", err, out)
}
cmd = exec.Command("hdiutil", "detach", "/Volumes/Kernel Debug Kit")
out, err = cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%v: %s", err, out)
}
return nil
}
return fmt.Errorf("only supported on macOS")
}
type KMUConfig struct {
Suffix string
Arch string
KDK string
Name string
Kernel string
Exclude []string
}
func KmutilCreate(conf *KMUConfig) (err error) {
if runtime.GOOS == "darwin" {
binfo, err := GetBuildInfo()
if err != nil {
return fmt.Errorf("failed to get build info: %v", err)
}
ver, err := semver.NewVersion(binfo.ProductVersion)
if err != nil {
return fmt.Errorf("failed to parse product version: %v", err)
}
args := []string{"create", "--verbose", "--new", "boot"}
if conf.Suffix != "" {
args = append(args, "--variant-suffix", conf.Suffix)
}
if conf.Arch != "" {
args = append(args, "--arch", conf.Arch)
}
if ver.GreaterThan(semver.Must(semver.NewVersion("13.0"))) {
if conf.KDK == "" {
conf.KDK = fmt.Sprintf("/Library/Developer/KDKs/KDK_%s_%s.kdk", binfo.ProductVersion, binfo.BuildVersion)
}
args = append(args, "--kdk", conf.KDK)
}
if conf.Kernel == "" {
conf.Kernel, err = GetKernelPath()
if err != nil {
return fmt.Errorf("failed to get kernel path: %v", err)
}
}
args = append(args, "--kernel", conf.Kernel)
if conf.Name != "" {
if err := os.MkdirAll(filepath.Dir(conf.Name), 0750); err != nil {
return fmt.Errorf("failed to create directory %s: %v", filepath.Dir(conf.Name), err)
}
args = append(args, "--boot-path", conf.Name)
}
if len(conf.Exclude) > 0 {
args = append(args, "--explicit-only")
args = append(args, conf.Exclude...)
}
cmd := exec.Command("kmutil", args...)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%v: %s", err, out)
}
fmt.Println(string(out))
return nil
}
return fmt.Errorf("only supported on macOS")
}