core API

core

package

API reference for the core package.

S
struct

DiskManager

DiskManager exposes functions to interact with the system’s disks
and partitions (e.g. mount, unmount, get partitions, etc.)

core/disk-manager.go:28-28
type DiskManager struct

Methods

GetPartitionByLabel finds a partition by searching for its label. If no partition can be found with the given label, returns error.

Parameters

label string

Returns

error
func (*DiskManager) GetPartitionByLabel(label string) (Partition, error)
{
	PrintVerboseInfo("DiskManager.GetPartitionByLabel", "retrieving partitions")

	partitions, err := d.GetPartitions("")
	if err != nil {
		PrintVerboseErr("DiskManager.GetPartitionByLabel", 0, err)
		return Partition{}, err
	}

	for _, part := range partitions {
		if part.Label == label {
			PrintVerboseInfo("DiskManager.GetPartitionByLabel", "Partition with UUID", part.Uuid, "has label", label)
			return part, nil
		}
	}

	errMsg := fmt.Errorf("could not find partition with label %s", label)
	PrintVerboseErr("DiskManager.GetPartitionByLabel", 1, errMsg)
	return Partition{}, errMsg
}
GetPartitions
Method

getPartitions gets a disk's partitions. If device is an empty string, gets all partitions from all disks

Parameters

device string

Returns

error
func (*DiskManager) GetPartitions(device string) ([]Partition, error)
{
	PrintVerboseInfo("DiskManager.getPartitions", "running...")

	output, err := exec.Command("lsblk", "-J", "-o", "NAME,FSTYPE,LABEL,MOUNTPOINT,UUID").Output()
	if err != nil {
		PrintVerboseErr("DiskManager.getPartitions", 0, err)
		return nil, err
	}

	var partitions struct {
		BlockDevices []struct {
			Name     string     `json:"name"`
			Type     string     `json:"type"`
			Children []Children `json:"children"`
		} `json:"blockdevices"`
	}

	if err := json.Unmarshal(output, &partitions); err != nil {
		PrintVerboseErr("DiskManager.getPartitions", 1, err)
		return nil, err
	}

	var result []Partition
	for _, blockDevice := range partitions.BlockDevices {
		if device != "" && blockDevice.Name != device {
			continue
		}

		iterChildren(&blockDevice.Children, &result)
	}

	PrintVerboseInfo("DiskManager.getPartitions", "successfully got partitions for disk", device)

	return result, nil
}
S
struct

Partition

Partition represents either a standard partition or a device-mapper
partition, such as an LVM volume

core/disk-manager.go:32-60
type Partition struct

Methods

Mount
Method

Mount mounts a partition to a directory, returning an error if any occurs

Parameters

destination string

Returns

error
func (*Partition) Mount(destination string) error
{
	PrintVerboseInfo("Partition.Mount", "running...")

	if _, err := os.Stat(destination); os.IsNotExist(err) {
		if err := os.MkdirAll(destination, 0755); err != nil {
			PrintVerboseErr("Partition.Mount", 0, err)
			return err
		}
	}

	devicePath := "/dev/"
	if p.IsDevMapper() {
		devicePath += "mapper/"
	}
	devicePath += p.Device

	err := syscall.Mount(devicePath, destination, p.FsType, 0, "")
	if err != nil {
		PrintVerboseErr("Partition.Mount", 1, err)
		return err
	}

	p.MountPoint = destination
	PrintVerboseInfo("Partition.Mount", "successfully mounted", devicePath, "to", destination)
	return nil
}
Unmount
Method

Unmount unmounts a partition

Returns

error
func (*Partition) Unmount() error
{
	PrintVerboseInfo("Partition.Unmount", "running...")

	if p.MountPoint == "" {
		PrintVerboseErr("Partition.Unmount", 0, errors.New("no mount point"))
		return errors.New("no mount point")
	}

	err := syscall.Unmount(p.MountPoint, 0)
	if err != nil {
		PrintVerboseErr("Partition.Unmount", 1, err)
		return err
	}

	PrintVerboseInfo("Partition.Unmount", "successfully unmounted", p.MountPoint)
	p.MountPoint = ""

	return nil
}
IsDevMapper
Method

Returns whether the partition is a device-mapper virtual partition

Returns

bool
func (*Partition) IsDevMapper() bool
{
	return p.Parent != nil
}
IsEncrypted
Method

IsEncrypted returns whether the partition is encrypted

Returns

bool
func (*Partition) IsEncrypted() bool
{
	return strings.HasPrefix(p.FsType, "crypto_")
}

Fields

Name Type Description
Label string
MountPoint string
MountOptions string
Uuid string
FsType string
Device string
Parent *Partition
S
struct

Children

The children a block device or partition may have

core/disk-manager.go:63-72
type Children struct

Fields

Name Type Description
MountPoint string json:"mountpoint"
FsType string json:"fstype"
Label string json:"label"
Uuid string json:"uuid"
LogicalName string json:"name"
Size string json:"size"
MountOptions string json:"mountopts"
Children []Children json:"children"
F
function

NewDiskManager

NewDiskManager creates and returns a pointer to a new DiskManager instance
from which you can interact with the system’s disks and partitions

Returns

core/disk-manager.go:76-78
func NewDiskManager() *DiskManager

{
	return &DiskManager{}
}
F
function

iterChildren

iterChildren iterates through the children of a device or partition
recursively

Parameters

childs
result
core/disk-manager.go:105-127
func iterChildren(childs *[]Children, result *[]Partition)

{
	for _, child := range *childs {
		*result = append(*result, Partition{
			Label:        child.Label,
			MountPoint:   child.MountPoint,
			MountOptions: child.MountOptions,
			Uuid:         child.Uuid,
			FsType:       child.FsType,
			Device:       child.LogicalName,
		})

		currentPartitions := len(*result)
		iterChildren(&child.Children, result)
		detectedPartitions := len(*result) - currentPartitions

		// Populate children's reference to parent
		for i := currentPartitions; i < len(*result); i++ {
			if (*result)[i].Parent == nil {
				(*result)[i].Parent = &(*result)[len(*result)-detectedPartitions-1]
			}
		}
	}
}
S
struct

ABImage

The ABImage is the representation of an OCI image used by ABRoot, it
contains the digest, the timestamp and the image name. If you need to
investigate the current ABImage on an ABRoot system, you can find it
at /abimage.abr

core/image.go:30-34
type ABImage struct

Methods

WriteTo
Method

WriteTo writes the json to a destination path, if the suffix is not empty, it will be appended to the filename

Parameters

dest string

Returns

error
func (*ABImage) WriteTo(dest string) error
{
	PrintVerboseInfo("ABImage.WriteTo", "running...")

	if _, err := os.Stat(dest); os.IsNotExist(err) {
		err = os.MkdirAll(dest, 0755)
		if err != nil {
			PrintVerboseErr("ABImage.WriteTo", 0, err)
			return err
		}
	}

	imagePath := filepath.Join(dest, "abimage.abr")

	abimage, err := json.Marshal(a)
	if err != nil {
		PrintVerboseErr("ABImage.WriteTo", 1, err)
		return err
	}

	err = os.WriteFile(imagePath, abimage, 0644)
	if err != nil {
		PrintVerboseErr("ABImage.WriteTo", 2, err)
		return err
	}

	PrintVerboseInfo("ABImage.WriteTo", "successfully wrote abimage.abr to "+imagePath)

	return nil
}

Fields

Name Type Description
Digest digest.Digest json:"digest"
Timestamp time.Time json:"timestamp"
Image string json:"image"
F
function

NewABImage

NewABImage creates a new ABImage instance and returns a pointer to it,
if the digest is empty, it returns an error

Parameters

digest
image
string

Returns

error
core/image.go:38-48
func NewABImage(digest digest.Digest, image string) (*ABImage, error)

{
	if digest == "" {
		return nil, fmt.Errorf("NewABImage: digest is empty")
	}

	return &ABImage{
		Digest:    digest,
		Timestamp: time.Now(),
		Image:     image,
	}, nil
}
F
function

NewABImageFromRoot

NewABImageFromRoot returns the current ABImage by parsing /abimage.abr, if
it fails, it returns an error (e.g. if the file doesn’t exist).
Note for distro maintainers: if the /abimage.abr is not present, it could
mean that the user is running an older version of ABRoot (pre v2) or the
root state is corrupted. In the latter case, generating a new ABImage should
fix the issue, Digest and Timestamp can be random, but Image should reflect
an existing image on the configured Docker registry. Anyway, support on this
is not guaranteed, so please don’t open issues about this.

Returns

error
core/image.go:58-76
func NewABImageFromRoot() (*ABImage, error)

{
	PrintVerboseInfo("NewABImageFromRoot", "running...")

	abimage, err := os.ReadFile("/abimage.abr")
	if err != nil {
		PrintVerboseErr("NewABImageFromRoot", 0, err)
		return nil, err
	}

	var a ABImage
	err = json.Unmarshal(abimage, &a)
	if err != nil {
		PrintVerboseErr("NewABImageFromRoot", 1, err)
		return nil, err
	}

	PrintVerboseInfo("NewABImageFromRoot", "found abimage.abr: "+a.Digest)
	return &a, nil
}
S
struct

NotEnoughSpaceError

core/oci.go:36-36
type NotEnoughSpaceError struct

Methods

Error
Method

Returns

string
func (*NotEnoughSpaceError) Error() string
{
	return "not enough space in disk"
}
F
function

padString

Parameters

str
string
size
int

Returns

string
core/oci.go:58-64
func padString(str string, size int) string

{
	if len(str) < size {
		return str + strings.Repeat(" ", size-len(str))
	} else {
		return str
	}
}
F
function

OciPullImage

OciExportRootFs pulls an image from a registry

Parameters

imageName
string

Returns

error
core/oci.go:67-89
func OciPullImage(imageName string) error

{
	pt, err := prometheus.NewPrometheus(
		"/var/lib/abroot/storage",
		"overlay",
		settings.Cnf.MaxParallelDownloads,
	)
	if err != nil {
		PrintVerboseErr("OciPullImage", 0, err)
		return err
	}

	if strings.HasPrefix(imageName, "localhost/") {
		return nil
	}

	err = pullImageWithProgressbar(pt, "remoteimage", imageName)
	if err != nil {
		PrintVerboseErr("OciPullImage", 1, err)
		return err
	}

	return nil
}
F
function

OciExportRootFs

OciExportRootFs generates a rootfs from an image recipe file

Parameters

buildImageName
string
imageRecipe
transDir
string
dest
string

Returns

error
core/oci.go:92-178
func OciExportRootFs(buildImageName string, imageRecipe *ImageRecipe, transDir string, dest string) error

{
	PrintVerboseInfo("OciExportRootFs", "running...")

	pt, err := prometheus.NewPrometheus(
		"/var/lib/abroot/storage",
		"overlay",
		settings.Cnf.MaxParallelDownloads,
	)
	if err != nil {
		PrintVerboseErr("OciExportRootFs", 0, err)
		return err
	}

	imageRecipePath := filepath.Join(transDir, "imageRecipe")

	if transDir == dest {
		err := errors.New("transDir and dest cannot be the same")
		PrintVerboseErr("OciExportRootFs", 1, err)
		return err
	}

	// create dest if it doesn't exist
	err = os.MkdirAll(dest, 0o755)
	if err != nil {
		PrintVerboseErr("OciExportRootFs", 3, err)
		return err
	}

	// cleanup transDir
	err = os.RemoveAll(transDir)
	if err != nil {
		PrintVerboseErr("OciExportRootFs", 4, err)
		return err
	}
	err = os.MkdirAll(transDir, 0o755)
	if err != nil {
		PrintVerboseErr("OciExportRootFs", 5, err)
		return err
	}

	// write imageRecipe
	err = imageRecipe.Write(imageRecipePath)
	if err != nil {
		PrintVerboseErr("OciExportRootFs", 6, err)
		return err
	}

	// build image
	imageBuild, err := pt.BuildContainerFile(imageRecipePath, buildImageName)
	if err != nil {
		PrintVerboseErr("OciExportRootFs", 7, err)
		return err
	}

	// This is safe because BuildContainerFile layers on top of the base image
	// So this won't delete the actual layers, only the image reference
	_, _ = pt.Store.DeleteImage(imageRecipe.From, true)

	// mount image
	mountDir, err := pt.MountImage(imageBuild.TopLayer)
	if err != nil {
		PrintVerboseErr("OciExportRootFs", 8, err)
		return err
	}

	err = checkImageSize(mountDir, dest)
	if err != nil {
		PrintVerboseErr("OciExportRootFs", 8.5, err)
		return err
	}

	// copy mount dir contents to dest
	err = rsyncCmd(mountDir+"/", dest, []string{"--delete", "--delete-before", "--checksum"}, false)
	if err != nil {
		PrintVerboseErr("OciExportRootFs", 9, err)
		return err
	}

	// unmount image
	_, err = pt.UnMountImage(imageBuild.TopLayer, true)
	if err != nil {
		PrintVerboseErr("OciExportRootFs", 10, err)
		return err
	}

	return nil
}
F
function

checkImageSize

returns nil if there’s enough space in the filesystem for the image
returns NotEnoughSpaceError if there is not enough space
returns other error if the sizes were not calculated correctly

Parameters

imageDir
string
filesystemMount
string

Returns

error
core/oci.go:183-220
func checkImageSize(imageDir string, filesystemMount string) error

{
	imageDirStat, err := os.Stat(imageDir)
	if err != nil {
		PrintVerboseErr("OciExportRootFs", 8.1, err)
		return err
	}

	var imageDirSize int64
	if imageDirStat.IsDir() {
		imageDirSize, err = getDirSize(imageDir)
		if err != nil {
			PrintVerboseErr("OciExportRootFs", 8.2, err)
			return err
		}
	} else {
		imageDirSize = imageDirStat.Size()
	}

	var stat syscall.Statfs_t
	err = syscall.Statfs(filesystemMount, &stat)
	if err != nil {
		PrintVerboseErr("OciExportRootFs", 8.3, err)
		return err
	}

	availableSpace := stat.Blocks * uint64(stat.Bsize)
	if settings.Cnf.ThinProvisioning {
		availableSpace /= 2
	}

	if uint64(imageDirSize) > availableSpace {
		err := &NotEnoughSpaceError{}
		PrintVerboseErr("OciExportRootFs", 8.4, err)
		return err
	}

	return nil
}
F
function

pullImageWithProgressbar

pullImageWithProgressbar pulls the image specified in the provided recipe
and reports the download progress using pterm progressbars. Each blob has
its own bar, similar to how docker and podman report downloads in their
respective CLIs

Parameters

name
string
imageName
string

Returns

error
core/oci.go:226-278
func pullImageWithProgressbar(pt *prometheus.Prometheus, name string, imageName string) error

{
	PrintVerboseInfo("pullImageWithProgressbar", "running...")

	progressCh := make(chan types.ProgressProperties)
	manifestCh := make(chan prometheus.OciManifest)
	errorCh := make(chan error)

	defer close(progressCh)
	defer close(manifestCh)
	defer close(errorCh)

	err := pt.PullImageAsync(imageName, name, progressCh, manifestCh, errorCh)
	if err != nil {
		PrintVerboseErr("pullImageWithProgressbar", 0, err)
		return err
	}

	multi := pterm.DefaultMultiPrinter
	bars := map[string]*pterm.ProgressbarPrinter{}

	multi.Start()

	barFmt := "%s [%s/%s]"
	for {
		select {
		case report := <-progressCh:
			digest := report.Artifact.Digest.Encoded()
			if pb, ok := bars[digest]; ok {
				progressBytes := humanize.Bytes(uint64(report.Offset))
				totalBytes := humanize.Bytes(uint64(report.Artifact.Size))

				pb.Add(int(report.Offset) - pb.Current)

				title := fmt.Sprintf(barFmt, digest[:12], progressBytes, totalBytes)
				pb.UpdateTitle(padString(title, 28))
			} else {
				totalBytes := humanize.Bytes(uint64(report.Artifact.Size))

				title := fmt.Sprintf(barFmt, digest[:12], "0", totalBytes)
				newPb, err := Progressbar.WithTotal(int(report.Artifact.Size)).WithWriter(multi.NewWriter()).Start(padString(title, 28))
				if err != nil {
					PrintVerboseErr("pullImageWithProgressbar", 1, err)
					return err
				}

				bars[digest] = newPb
			}
		case <-manifestCh:
			multi.Stop()
			return nil
		}
	}
}
F
function

FindImageWithLabel

FindImageWithLabel returns the name of the first image containinig the provided key-value pair
or an empty string if none was found
FindImageWithLabel returns the name of the first image containing the
provided key-value pair or an empty string if none was found

Parameters

key
string
value
string

Returns

string
error
core/oci.go:284-318
func FindImageWithLabel(key, value string) (string, error)

{
	PrintVerboseInfo("FindImageWithLabel", "running...")

	pt, err := prometheus.NewPrometheus(
		"/var/lib/abroot/storage",
		"overlay",
		settings.Cnf.MaxParallelDownloads,
	)
	if err != nil {
		PrintVerboseErr("FindImageWithLabel", 0, err)
		return "", err
	}

	images, err := pt.Store.Images()
	if err != nil {
		PrintVerboseErr("FindImageWithLabel", 1, err)
		return "", err
	}

	for _, img := range images {
		// This is the only way I could find to get the labels form an image
		builder, err := buildah.ImportBuilderFromImage(context.Background(), pt.Store, buildah.ImportFromImageOptions{Image: img.ID})
		if err != nil {
			PrintVerboseErr("FindImageWithLabel", 2, err)
			return "", err
		}

		val, ok := builder.Labels()[key]
		if ok && val == value {
			return img.Names[0], nil
		}
	}

	return "", nil
}
F
function

RetrieveImageForRoot

RetrieveImageForRoot retrieves the image created for the provided root
based on the label. Note for distro maintainers: labels must follow those
defined in the ABRoot config file

Parameters

root
string

Returns

string
error
core/oci.go:323-333
func RetrieveImageForRoot(root string) (string, error)

{
	PrintVerboseInfo("RetrieveImageForRoot", "running...")

	image, err := FindImageWithLabel("ABRoot.root", root)
	if err != nil {
		PrintVerboseErr("RetrieveImageForRoot", 0, err)
		return "", err
	}

	return image, nil
}
F
function

DeleteAllButLatestImage

DeleteAllButLatestImage deletes all images

Returns

error
core/oci.go:336-374
func DeleteAllButLatestImage() error

{
	pt, err := prometheus.NewPrometheus(
		"/var/lib/abroot/storage",
		"overlay",
		settings.Cnf.MaxParallelDownloads,
	)
	if err != nil {
		PrintVerboseErr("DeleteAllImagesButLatest", 1, err)
		return err
	}

	allImages, err := pt.Store.Images()
	if err != nil {
		PrintVerboseErr("DeleteAllImagesButLatest", 2, err)
		return fmt.Errorf("could not retrieve all images: %w", err)
	}

	if len(allImages) == 0 {
		return nil
	}

	var latestImage *storage.Image
	for _, image := range allImages {
		if latestImage == nil || image.Created.After(latestImage.Created) {
			latestImage = &image
		}
	}

	for _, image := range allImages {
		if image.ID != latestImage.ID {
			_, err := pt.Store.DeleteImage(image.ID, true)
			if err != nil {
				PrintVerboseErr("DeleteAllImagesButLatest", 3, "failed to remove image: ", err)
			}
		}
	}

	return nil
}
F
function

HasUpdate

HasUpdate checks if the image/tag from the registry has a different digest
it returns the new digest and a boolean indicating if an update is available

Parameters

oldDigest

Returns

bool
error
core/oci.go:378-407
func HasUpdate(oldDigest digest.Digest) (digest.Digest, bool, error)

{
	PrintVerboseInfo("OCI.HasUpdate", "Checking for updates ...")

	pt, err := prometheus.NewPrometheus(
		"/var/lib/abroot/storage",
		"overlay",
		settings.Cnf.MaxParallelDownloads,
	)
	if err != nil {
		PrintVerboseErr("OCI.HasUpdate", 0, err)
		return "", false, err
	}

	imageName := fmt.Sprintf("%s/%s:%s", settings.Cnf.Registry, settings.Cnf.Name, settings.Cnf.Tag)
	PrintVerboseInfo("OCI.HasUpdate", "checking image: ", imageName)

	_, newDigest, err := pt.PullManifestOnly(imageName)
	if err != nil {
		PrintVerboseErr("OCI.HasUpdate", 1, err)
		return "", false, err
	}

	if newDigest == oldDigest {
		PrintVerboseInfo("OCI.HasUpdate", "no update available")
		return "", false, nil
	}

	PrintVerboseInfo("OCI.HasUpdate", "update available. Old digest: ", oldDigest, ", new digest: ", newDigest)
	return newDigest, true, nil
}
S
struct

Chroot

Chroot represents a chroot instance, which can be used to run commands
inside a chroot environment

core/chroot.go:26-31
type Chroot struct

Methods

Close
Method

Close unmounts all the bind mounts and closes the chroot environment

Returns

error
func (*Chroot) Close() error
{
	PrintVerboseInfo("Chroot.Close", "running...")

	err := syscall.Unmount(filepath.Join(c.root, "/dev/pts"), 0)
	if err != nil {
		PrintVerboseErr("Chroot.Close", 0, err)
		return err
	}

	mountList := ReservedMounts
	if c.etcMounted {
		mountList = append(mountList, "/etc")
	}
	mountList = append(mountList, "")

	for _, mount := range mountList {
		if mount == "/dev/pts" {
			continue
		}

		mountDir := filepath.Join(c.root, mount)
		PrintVerboseInfo("Chroot.Close", "unmounting", mountDir)
		err := syscall.Unmount(mountDir, 0)
		if err != nil {
			PrintVerboseErr("Chroot.Close", 1, err)
			return err
		}
	}

	PrintVerboseInfo("Chroot.Close", "successfully closed.")
	return nil
}
Execute
Method

Execute runs a command in the chroot environment, the command is a string and the arguments are a list of strings. If an error occurs it is returned.

Parameters

cmd string

Returns

error
func (*Chroot) Execute(cmd string) error
{
	PrintVerboseInfo("Chroot.Execute", "running...")

	PrintVerboseInfo("Chroot.Execute", "running command:", cmd)
	e := exec.Command("chroot", c.root, "/bin/sh", "-c", cmd)
	e.Stdout = os.Stdout
	e.Stderr = os.Stderr
	e.Stdin = os.Stdin
	err := e.Run()
	if err != nil {
		PrintVerboseErr("Chroot.Execute", 0, err)
		return err
	}

	PrintVerboseInfo("Chroot.Execute", "successfully ran.")
	return nil
}
ExecuteCmds
Method

ExecuteCmds runs a list of commands in the chroot environment, stops at the first error

Parameters

cmds []string

Returns

error
func (*Chroot) ExecuteCmds(cmds []string) error
{
	PrintVerboseInfo("Chroot.ExecuteCmds", "running...")

	for _, cmd := range cmds {
		err := c.Execute(cmd)
		if err != nil {
			PrintVerboseErr("Chroot.ExecuteCmds", 0, err)
			return err
		}
	}

	PrintVerboseInfo("Chroot.ExecuteCmds", "successfully ran.")
	return nil
}

Fields

Name Type Description
root string
rootUuid string
rootDevice string
etcMounted bool
F
function

NewChroot

NewChroot creates a new chroot environment from the given root path and
returns its Chroot instance or an error if something went wrong

Parameters

root
string
rootUuid
string
rootDevice
string
mountUserEtc
bool
userEtcPath
string

Returns

error
core/chroot.go:46-90
func NewChroot(root string, rootUuid string, rootDevice string, mountUserEtc bool, userEtcPath string) (*Chroot, error)

{
	PrintVerboseInfo("NewChroot", "running...")

	root = strings.ReplaceAll(root, "//", "/")

	if _, err := os.Stat(root); os.IsNotExist(err) {
		PrintVerboseErr("NewChroot", 0, err)
		return nil, err
	}

	chroot := &Chroot{
		root:       root,
		rootUuid:   rootUuid,
		rootDevice: rootDevice,
		etcMounted: mountUserEtc,
	}

	// workaround for grub-mkconfig, not able to find the device
	// inside a chroot environment
	err := chroot.Execute("mount --bind / /")
	if err != nil {
		PrintVerboseErr("NewChroot", 1, err)
		return nil, err
	}

	for _, mount := range ReservedMounts {
		PrintVerboseInfo("NewChroot", "mounting", mount)
		err := syscall.Mount(mount, filepath.Join(root, mount), "", syscall.MS_BIND, "")
		if err != nil {
			PrintVerboseErr("NewChroot", 2, err)
			return nil, err
		}
	}

	if mountUserEtc {
		err = syscall.Mount("overlay", filepath.Join(root, "etc"), "overlay", syscall.MS_RDONLY, "lowerdir="+userEtcPath+":"+filepath.Join(root, "/etc"))
		if err != nil {
			PrintVerboseErr("NewChroot", 3, "failed to mount user etc:", err)
			return nil, err
		}
	}

	PrintVerboseInfo("NewChroot", "successfully created.")
	return chroot, nil
}
F
function

RepairRootIntegrity

Parameters

rootPath
string

Returns

err
error
core/integrity.go:54-68
func RepairRootIntegrity(rootPath string) (err error)

{
	fixupOlderSystems(rootPath)

	err = repairLinks(rootPath)
	if err != nil {
		return err
	}

	err = repairPaths(rootPath)
	if err != nil {
		return err
	}

	return nil
}
F
function

repairPaths

Parameters

rootPath
string

Returns

err
error
core/integrity.go:109-117
func repairPaths(rootPath string) (err error)

{
	for _, path := range pathsToRepair {
		err = repairPath(filepath.Join(rootPath, path))
		if err != nil {
			return err
		}
	}
	return nil
}
F
function

repairPath

Parameters

path
string

Returns

err
error
core/integrity.go:119-138
func repairPath(path string) (err error)

{
	if info, err := os.Lstat(path); err == nil && info.IsDir() {
		return nil
	}

	err = os.Remove(path)
	if err != nil && !os.IsNotExist(err) {
		PrintVerboseErr("repairPath", 1, "Can't remove ", path, " : ", err)
		return err
	}

	PrintVerboseInfo("repairPath", "Repairing ", path)
	err = os.MkdirAll(path, 0o755)
	if err != nil {
		PrintVerboseErr("repairPath", 2, "Can't create ", path, " : ", err)
		return err
	}

	return nil
}
F
function

fixupOlderSystems

this is here to keep compatibility with older systems

Parameters

rootPath
string
core/integrity.go:141-160
func fixupOlderSystems(rootPath string)

{
	paths := []string{
		"mnt",
		"root",
	}

	for _, path := range paths {
		legacyPath := filepath.Join(rootPath, path)
		newPath := filepath.Join("/var", path)

		if _, err := os.Lstat(newPath); errors.Is(err, os.ErrNotExist) {
			err = exec.Command("mv", legacyPath, newPath).Run()
			if err != nil {
				PrintVerboseErr("fixupOlderSystems", 1, "could not move ", legacyPath, " to ", newPath, " : ", err)
				// if moving failed it probably means that it migrated successfully in the past
				// so it's safe to ignore errors
			}
		}
	}
}
F
function

init

init initializes the log file and sets up logging

core/logging.go:32-70
func init()

{
	PrintVerboseInfo("NewLogFile", "running...")

	// Incremental value to append to log file name
	incremental := 0

	// Check for existing log files
	logFiles, err := filepath.Glob("/var/log/abroot.log.*")
	if err != nil {
		// If there are no log files, start with incremental 1
		incremental = 1
	} else {
		allIncrementals := []int{}
		// Extract incremental values from existing log file names
		for _, logFile := range logFiles {
			_, err := fmt.Sscanf(logFile, "/var/log/abroot.log.%d", &incremental)
			if err != nil {
				continue
			}
			allIncrementals = append(allIncrementals, incremental)
		}
		// Set incremental to the next available value
		if len(allIncrementals) == 0 {
			incremental = 1
		} else {
			incremental = allIncrementals[len(allIncrementals)-1] + 1
		}
	}

	// Open or create the log file
	logFile, err = os.OpenFile(
		fmt.Sprintf("/var/log/abroot.log.%d", incremental),
		os.O_RDWR|os.O_CREATE|os.O_APPEND,
		0666,
	)
	if err != nil {
		PrintVerboseErrNoLog("NewLogFile", 0, "failed to open log file", err)
	}
}
F
function

IsVerbose

IsVerbose checks if verbose mode is enabled

Returns

bool
core/logging.go:73-77
func IsVerbose() bool

{
	flag := cmdr.FlagValBool("verbose")
	_, arg := os.LookupEnv("ABROOT_VERBOSE")
	return flag || arg
}
F
function

formatMessage

formatMessage formats log messages based on prefix, level, and depth

Parameters

prefix
string
level
string
depth
float32
args
...interface{}

Returns

string
core/logging.go:80-89
func formatMessage(prefix, level string, depth float32, args ...interface{}) string

{
	if prefix == "" && level == "" && depth == -1 {
		return fmt.Sprint(args...)
	}

	if depth > -1 {
		level = fmt.Sprintf("%s(%f)", level, depth)
	}
	return fmt.Sprintf("%s:%s:%s", prefix, level, fmt.Sprint(args...))
}
F
function

printFormattedMessage

printFormattedMessage prints formatted log messages to Stdout

Parameters

formattedMsg
string
core/logging.go:92-94
func printFormattedMessage(formattedMsg string)

{
	printLog.Printf("%s\n", formattedMsg)
}
F
function

logToFileIfEnabled

logToFileIfEnabled logs messages to the file if logging is enabled

Parameters

formattedMsg
string
core/logging.go:97-101
func logToFileIfEnabled(formattedMsg string)

{
	if logFile != nil {
		LogToFile(formattedMsg)
	}
}
F
function

PrintVerboseNoLog

PrintVerboseNoLog prints verbose messages without logging to the file

Parameters

prefix
string
level
string
depth
float32
args
...interface{}
core/logging.go:104-109
func PrintVerboseNoLog(prefix, level string, depth float32, args ...interface{})

{
	if IsVerbose() {
		formattedMsg := formatMessage(prefix, level, depth, args...)
		printFormattedMessage(formattedMsg)
	}
}
F
function

PrintVerbose

PrintVerbose prints verbose messages and logs to the file if enabled

Parameters

prefix
string
level
string
depth
float32
args
...interface{}
core/logging.go:112-116
func PrintVerbose(prefix, level string, depth float32, args ...interface{})

{
	PrintVerboseNoLog(prefix, level, depth, args...)

	logToFileIfEnabled(formatMessage(prefix, level, depth, args...))
}
F
function

PrintVerboseSimpleNoLog

PrintVerboseSimpleNoLog prints simple verbose messages without logging to the file

Parameters

args
...interface{}
core/logging.go:119-121
func PrintVerboseSimpleNoLog(args ...interface{})

{
	PrintVerboseNoLog("", "", -1, args...)
}
F
function

PrintVerboseSimple

PrintVerboseSimple prints simple verbose messages and logs to the file if enabled

Parameters

args
...interface{}
core/logging.go:124-126
func PrintVerboseSimple(args ...interface{})

{
	PrintVerbose("", "", -1, args...)
}
F
function

PrintVerboseErrNoLog

PrintVerboseErrNoLog prints verbose error messages without logging to the file

Parameters

prefix
string
depth
float32
args
...interface{}
core/logging.go:129-131
func PrintVerboseErrNoLog(prefix string, depth float32, args ...interface{})

{
	PrintVerboseNoLog(prefix, "err", depth, args...)
}
F
function

PrintVerboseErr

PrintVerboseErr prints verbose error messages and logs to the file if enabled

Parameters

prefix
string
depth
float32
args
...interface{}
core/logging.go:134-136
func PrintVerboseErr(prefix string, depth float32, args ...interface{})

{
	PrintVerbose(prefix, "err", depth, args...)
}
F
function

PrintVerboseWarnNoLog

PrintVerboseWarnNoLog prints verbose warning messages without logging to the file

Parameters

prefix
string
depth
float32
args
...interface{}
core/logging.go:139-141
func PrintVerboseWarnNoLog(prefix string, depth float32, args ...interface{})

{
	PrintVerboseNoLog(prefix, "warn", depth, args...)
}
F
function

PrintVerboseWarn

PrintVerboseWarn prints verbose warning messages and logs to the file if enabled

Parameters

prefix
string
depth
float32
args
...interface{}
core/logging.go:144-146
func PrintVerboseWarn(prefix string, depth float32, args ...interface{})

{
	PrintVerbose(prefix, "warn", depth, args...)
}
F
function

PrintVerboseInfoNoLog

PrintVerboseInfoNoLog prints verbose info messages without logging to the file

Parameters

prefix
string
args
...interface{}
core/logging.go:149-151
func PrintVerboseInfoNoLog(prefix string, args ...interface{})

{
	PrintVerboseNoLog(prefix, "info", -1, args...)
}
F
function

PrintVerboseInfo

PrintVerboseInfo prints verbose info messages and logs to the file if enabled

Parameters

prefix
string
args
...interface{}
core/logging.go:154-156
func PrintVerboseInfo(prefix string, args ...interface{})

{
	PrintVerbose(prefix, "info", -1, args...)
}
F
function

LogToFile

LogToFile writes messages to the log file

Parameters

msg
string
args
...interface{}

Returns

error
core/logging.go:159-170
func LogToFile(msg string, args ...interface{}) error

{
	if logFile != nil {
		_, err := fmt.Fprintf(
			logFile,
			"%s: %s\n",
			time.Now().Format("2006-01-02 1 15:04:05"),
			fmt.Sprintf(msg, args...),
		)
		return err
	}
	return nil
}
F
function

GetLogFile

GetLogFile returns the log file handle

Returns

core/logging.go:173-175
func GetLogFile() *os.File

{
	return logFile
}
F
function

BaseImagePackageDiff

BaseImagePackageDiff retrieves the added, removed, upgraded and downgraded
base packages (the ones bundled with the image).

Parameters

currentDigest
newDigest

Returns

upgraded
downgraded
removed
err
error
core/package-diff.go:31-84
func BaseImagePackageDiff(currentDigest, newDigest digest.Digest) (added, upgraded, downgraded, removed []diff.PackageDiff, err error)

{
	PrintVerboseInfo("PackageDiff.BaseImagePackageDiff", "running...")

	imageComponents := strings.Split(settings.Cnf.Name, "/")
	imageName := imageComponents[len(imageComponents)-1]
	reqUrl := fmt.Sprintf("%s/images/%s/diff", settings.Cnf.DifferURL, imageName)
	body := fmt.Sprintf("{\"old_digest\": \"%s\", \"new_digest\": \"%s\"}", currentDigest, newDigest)

	PrintVerboseInfo("PackageDiff.BaseImagePackageDiff", "Requesting base image diff to", reqUrl, "with body", body)

	request, err := http.NewRequest(http.MethodGet, reqUrl, strings.NewReader(body))
	if err != nil {
		PrintVerboseErr("PackageDiff.BaseImagePackageDiff", 0, err)
		return
	}
	defer request.Body.Close()

	resp, err := http.DefaultClient.Do(request)
	if err != nil {
		PrintVerboseErr("PackageDiff.BaseImagePackageDiff", 1, err)
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		PrintVerboseErr("PackageDiff.BaseImagePackageDiff", 2, fmt.Errorf("received non-OK status %s", resp.Status))
		return
	}

	contents, err := io.ReadAll(resp.Body)
	if err != nil {
		PrintVerboseErr("PackageDiff.BaseImagePackageDiff", 3, err)
		return
	}

	pkgDiff := struct {
		Added, Upgraded, Downgraded, Removed []diff.PackageDiff
	}{}
	err = json.Unmarshal(contents, &pkgDiff)
	if err != nil {
		PrintVerboseErr("PackageDiff.BaseImagePackageDiff", 4, err)
		return
	}

	added = pkgDiff.Added
	upgraded = pkgDiff.Upgraded
	downgraded = pkgDiff.Downgraded
	removed = pkgDiff.Removed

	return
}
F
function

OverlayPackageDiff

OverlayPackageDiff retrieves the added, removed, upgraded and downgraded
overlay packages (the ones added manually via abroot pkg add).

Returns

upgraded
downgraded
removed
err
error
core/package-diff.go:88-132
func OverlayPackageDiff() (added, upgraded, downgraded, removed []diff.PackageDiff, err error)

{
	PrintVerboseInfo("OverlayPackageDiff", "running...")

	pkgM, err := NewPackageManager(false)
	if err != nil {
		PrintVerboseErr("OverlayPackageDiff", 0, err)
		return
	}

	addedPkgs, err := pkgM.GetAddPackages()
	if err != nil {
		PrintVerboseErr("PackageDiff.OverlayPackageDiff", 0, err)
		return
	}

	localAddedVersions := dpkg.DpkgBatchGetPackageVersion(addedPkgs)
	localAdded := map[string]string{}
	for i := 0; i < len(addedPkgs); i++ {
		if localAddedVersions[i] != "" {
			localAdded[addedPkgs[i]] = localAddedVersions[i]
		}
	}

	remoteAdded := map[string]string{}
	var pkgInfo map[string]interface{}
	for pkgName := range localAdded {
		pkgInfo, err = GetRepoContentsForPkg(pkgName)
		if err != nil {
			PrintVerboseErr("PackageDiff.OverlayPackageDiff", 1, err)
			return
		}
		version, ok := pkgInfo["version"].(string)
		if !ok {
			err = fmt.Errorf("unexpected value when retrieving upstream version of '%s'", pkgName)
			return
		}
		remoteAdded[pkgName] = version
	}

	added, upgraded, downgraded, removed = diff.DiffPackages(localAdded, remoteAdded)
	return
}
S
struct

ABRootManager

ABRootManager exposes methods to manage ABRoot partitions, this includes
getting the present and future partitions, the boot partition, the init
volume (when using LVM Thin-Provisioning), and the other partition. If you
need to operate on an ABRoot partition, you should use this struct, each
partition is a pointer to a Partition struct, which contains methods to
operate on the partition itself

core/root.go:28-34
type ABRootManager struct

Methods

GetPartitions
Method

GetPartitions gets the root partitions from the current device

Returns

error
func (*ABRootManager) GetPartitions() error
{
	PrintVerboseInfo("ABRootManager.GetRootPartitions", "running...")

	diskM := NewDiskManager()
	rootLabels := []string{settings.Cnf.PartLabelA, settings.Cnf.PartLabelB}
	for _, label := range rootLabels {
		partition, err := diskM.GetPartitionByLabel(label)
		if err != nil {
			PrintVerboseErr("ABRootManager.GetRootPartitions", 0, err)
			return err
		}

		identifier, err := a.IdentifyPartition(partition)
		if err != nil {
			PrintVerboseErr("ABRootManager.GetRootPartitions", 1, err)
			return err
		}

		isCurrent := a.IsCurrent(partition)
		a.Partitions = append(a.Partitions, ABRootPartition{
			Label:        partition.Label,
			IdentifiedAs: identifier,
			Partition:    partition,
			MountPoint:   partition.MountPoint,
			MountOptions: partition.MountOptions,
			Uuid:         partition.Uuid,
			FsType:       partition.FsType,
			Current:      isCurrent,
		})
	}

	partition, err := diskM.GetPartitionByLabel(settings.Cnf.PartLabelVar)
	if err != nil {
		PrintVerboseErr("ABRootManager.GetRootPartitions", 2, err)
		return err
	}
	a.VarPartition = partition

	PrintVerboseInfo("ABRootManager.GetRootPartitions", "successfully got root partitions")

	return nil
}
IsCurrent
Method

IsCurrent checks if a partition is the current one

Parameters

partition Partition

Returns

bool
func (*ABRootManager) IsCurrent(partition Partition) bool
{
	PrintVerboseInfo("ABRootManager.IsCurrent", "running...")

	if partition.MountPoint == "/" {
		PrintVerboseInfo("ABRootManager.IsCurrent", "partition is current")
		return true
	}

	PrintVerboseInfo("ABRootManager.IsCurrent", "partition is not current")
	return false
}

IdentifyPartition identifies a partition

Parameters

partition Partition

Returns

identifiedAs string
err error
func (*ABRootManager) IdentifyPartition(partition Partition) (identifiedAs string, err error)
{
	PrintVerboseInfo("ABRootManager.IdentifyPartition", "running...")

	if partition.Label == settings.Cnf.PartLabelA || partition.Label == settings.Cnf.PartLabelB {
		if partition.MountPoint == "/" {
			PrintVerboseInfo("ABRootManager.IdentifyPartition", "partition is present")
			return "present", nil
		}

		PrintVerboseInfo("ABRootManager.IdentifyPartition", "partition is future")
		return "future", nil
	}

	err = errors.New("partition is not managed by ABRoot")
	PrintVerboseErr("ABRootManager.IdentifyPartition", 0, err)
	return "", err
}
GetPresent
Method

GetPresent gets the present partition

Returns

partition ABRootPartition
err error
func (*ABRootManager) GetPresent() (partition ABRootPartition, err error)
{
	PrintVerboseInfo("ABRootManager.GetPresent", "running...")

	for _, partition := range a.Partitions {
		if partition.IdentifiedAs == "present" {
			PrintVerboseInfo("ABRootManager.GetPresent", "successfully got present partition")
			return partition, nil
		}
	}

	err = errors.New("present partition not found")
	PrintVerboseErr("ABRootManager.GetPresent", 0, err)
	return ABRootPartition{}, err
}
GetFuture
Method

GetFuture gets the future partition

Returns

partition ABRootPartition
err error
func (*ABRootManager) GetFuture() (partition ABRootPartition, err error)
{
	PrintVerboseInfo("ABRootManager.GetFuture", "running...")

	for _, partition := range a.Partitions {
		if partition.IdentifiedAs == "future" {
			PrintVerboseInfo("ABRootManager.GetFuture", "successfully got future partition")
			return partition, nil
		}
	}

	err = errors.New("future partition not found")
	PrintVerboseErr("ABRootManager.GetFuture", 0, err)
	return ABRootPartition{}, err
}
GetOther
Method

GetOther gets the other partition

Returns

partition ABRootPartition
err error
func (*ABRootManager) GetOther() (partition ABRootPartition, err error)
{
	PrintVerboseInfo("ABRootManager.GetOther", "running...")

	present, err := a.GetPresent()
	if err != nil {
		PrintVerboseErr("ABRootManager.GetOther", 0, err)
		return ABRootPartition{}, err
	}

	if present.Label == settings.Cnf.PartLabelA {
		PrintVerboseInfo("ABRootManager.GetOther", "successfully got other partition")
		return a.GetPartition(settings.Cnf.PartLabelB)
	}

	PrintVerboseInfo("ABRootManager.GetOther", "successfully got other partition")
	return a.GetPartition(settings.Cnf.PartLabelA)
}
GetPartition
Method

GetPartition gets a partition by label

Parameters

label string

Returns

partition ABRootPartition
err error
func (*ABRootManager) GetPartition(label string) (partition ABRootPartition, err error)
{
	PrintVerboseInfo("ABRootManager.GetPartition", "running...")

	for _, partition := range a.Partitions {
		if partition.Label == label {
			PrintVerboseInfo("ABRootManager.GetPartition", "successfully got partition")
			return partition, nil
		}
	}

	err = errors.New("partition not found")
	PrintVerboseErr("ABRootManager.GetPartition", 0, err)
	return ABRootPartition{}, err
}
GetBoot
Method

GetBoot gets the boot partition from the current device

Returns

partition Partition
err error
func (*ABRootManager) GetBoot() (partition Partition, err error)
{
	PrintVerboseInfo("ABRootManager.GetBoot", "running...")

	diskM := NewDiskManager()
	part, err := diskM.GetPartitionByLabel(settings.Cnf.PartLabelBoot)
	if err != nil {
		err = errors.New("boot partition not found")
		PrintVerboseErr("ABRootManager.GetBoot", 0, err)

		return Partition{}, err
	}

	PrintVerboseInfo("ABRootManager.GetBoot", "successfully got boot partition")
	return part, nil
}
GetInit
Method

GetInit gets the init volume when using LVM Thin-Provisioning

Returns

partition Partition
err error
func (*ABRootManager) GetInit() (partition Partition, err error)
{
	PrintVerboseInfo("ABRootManager.GetInit", "running...")

	// Make sure Thin-Provisioning is properly configured
	if !settings.Cnf.ThinProvisioning || settings.Cnf.ThinInitVolume == "" {
		return Partition{}, errors.New("ABRootManager.GetInit: error: system is not configured for thin-provisioning")
	}

	diskM := NewDiskManager()
	part, err := diskM.GetPartitionByLabel(settings.Cnf.ThinInitVolume)
	if err != nil {
		err = errors.New("init volume not found")
		PrintVerboseErr("ABRootManager.GetInit", 0, err)

		return Partition{}, err
	}

	PrintVerboseInfo("ABRootManager.GetInit", "successfully got init volume")
	return part, nil
}

Fields

Name Type Description
Partitions []ABRootPartition
VarPartition Partition
S
struct

ABRootPartition

ABRootPartition represents a partition managed by ABRoot

core/root.go:37-46
type ABRootPartition struct

Fields

Name Type Description
Label string
IdentifiedAs string
Partition Partition
MountPoint string
MountOptions string
Uuid string
FsType string
Current bool
F
function

NewABRootManager

NewABRootManager creates a new ABRootManager

Returns

core/root.go:49-56
func NewABRootManager() *ABRootManager

{
	PrintVerboseInfo("NewABRootManager", "running...")

	a := &ABRootManager{}
	a.GetPartitions()

	return a
}
F
function

rsyncCmd

rsyncCmd executes the rsync command with the requested options.
If silent is true, rsync progress will not appear in stdout.

Parameters

src
string
dst
string
opts
[]string
silent
bool

Returns

error
core/rsync.go:31-97
func rsyncCmd(src, dst string, opts []string, silent bool) error

{
	args := []string{"-avxHAX"}
	args = append(args, opts...)
	args = append(args, src)
	args = append(args, dst)

	cmd := exec.Command("rsync", args...)
	stdout, _ := cmd.StdoutPipe()

	var totalFiles int

	if !silent {
		countCmdOut, _ := exec.Command(
			"/bin/sh",
			"-c",
			fmt.Sprintf("echo -n $(($(rsync --dry-run %s | wc -l) - 4))", strings.Join(args, " ")),
		).Output()
		totalFiles, _ = strconv.Atoi(string(countCmdOut))
	}

	reader := bufio.NewReader(stdout)

	err := cmd.Start()
	if err != nil {
		return err
	}

	if !silent {
		verbose := IsVerbose()

		p, _ := cmdr.ProgressBar.WithTotal(totalFiles).WithTitle("Sync in progress").WithMaxWidth(120).Start()
		maxLineLen := cmdr.TerminalWidth() / 4

		for i := 0; i < p.Total; i++ {
			line, _ := reader.ReadString('\n')
			line = strings.TrimSpace(line)

			if verbose {
				cmdr.Info.Println(line + " synced")
			}

			if len(line) > maxLineLen {
				startingLen := len(line) - maxLineLen + 1
				line = "<" + line[startingLen:]
			} else {
				padding := maxLineLen - len(line)
				line += strings.Repeat(" ", padding)
			}

			p.UpdateTitle("Syncing " + line)
			p.Increment()
		}
	} else {
		stdout.Close()
	}

	err = cmd.Wait()
	if err != nil {
		// exit status 24 is a warning, not an error, we don't care about it
		// since rsync is going to be removed in the OCI version
		if !strings.Contains(err.Error(), "exit status 24") {
			return err
		}
	}

	return nil
}
F
function

rsyncDryRun

rsyncDryRun executes the rsync command with the –dry-run option.

Parameters

src
string
dst
string
excluded
[]string

Returns

error
core/rsync.go:100-110
func rsyncDryRun(src, dst string, excluded []string) error

{
	opts := []string{"--dry-run"}

	if len(excluded) > 0 {
		for _, exclude := range excluded {
			opts = append(opts, "--exclude="+exclude)
		}
	}

	return rsyncCmd(src, dst, opts, false)
}
F
function

AtomicRsync

AtomicRsync executes the rsync command in an atomic-like manner.
It does so by dry-running the rsync, and if it succeeds, it runs
the rsync again performing changes.
If the keepUnwanted option
is set to true, it will omit the –delete option, so that the already
existing and unwanted files will not be deleted.
To ensure the changes are applied atomically, we rsync on a _new directory first,
and use atomicSwap to replace the _new with the dst directory.

Parameters

src
string
dst
string
transitionalPath
string
finalPath
string
excluded
[]string
keepUnwanted
bool

Returns

error
core/rsync.go:120-165
func AtomicRsync(src, dst string, transitionalPath string, finalPath string, excluded []string, keepUnwanted bool) error

{
	PrintVerboseInfo("AtomicRsync", "Running...")
	if _, err := os.Stat(transitionalPath); os.IsNotExist(err) {
		err = os.Mkdir(transitionalPath, 0755)
		if err != nil {
			PrintVerboseErr("AtomicRsync", 0, err)
			return err
		}
	}

	PrintVerboseInfo("AtomicRsync", "Starting dry run process...")
	err := rsyncDryRun(src, transitionalPath, excluded)
	if err != nil {
		return err
	}

	opts := []string{"--link-dest", dst, "--exclude", finalPath, "--exclude", transitionalPath}

	if len(excluded) > 0 {
		for _, exclude := range excluded {
			opts = append(opts, "--exclude", exclude)
		}
	}

	if !keepUnwanted {
		opts = append(opts, "--delete")
	}

	PrintVerboseInfo("AtomicRsync", "Starting rsync process...")

	err = rsyncCmd(src, transitionalPath, opts, true)
	if err != nil {
		return err
	}

	PrintVerboseInfo("AtomicRsync", "Starting atomic swap process...")

	err = AtomicSwap(transitionalPath, finalPath)
	if err != nil {
		return err
	}

	PrintVerboseInfo("AtomicRsync", "Removing transitional path...")

	return os.RemoveAll(transitionalPath)
}
S
struct

PCSpecs

core/specs.go:25-29
type PCSpecs struct

Fields

Name Type Description
CPU string
GPU []string
Memory string
S
struct

GPUInfo

core/specs.go:31-34
type GPUInfo struct

Fields

Name Type Description
Address string
Description string
F
function

getCPUInfo

Returns

string
error
core/specs.go:36-45
func getCPUInfo() (string, error)

{
	info, err := cpu.Info()
	if err != nil {
		return "", err
	}
	if len(info) == 0 {
		return "", fmt.Errorf("CPU information not found")
	}
	return info[0].ModelName, nil
}
F
function

parseGPUInfo

Parameters

line
string

Returns

string
error
core/specs.go:47-59
func parseGPUInfo(line string) (string, error)

{
	parts := strings.SplitN(line, " ", 3)
	if len(parts) < 3 {
		return "", fmt.Errorf("GPU information not found")
	}

	parts = strings.SplitN(parts[2], ":", 2)
	if len(parts) < 2 {
		return "", fmt.Errorf("GPU information not found")
	}

	return strings.TrimSpace(parts[1]), nil
}
F
function

getGPUInfo

Returns

[]string
error
core/specs.go:61-83
func getGPUInfo() ([]string, error)

{
	cmd := exec.Command("lspci")
	output, err := cmd.CombinedOutput()
	if err != nil {
		fmt.Println("Error getting GPU info:", err)
		return nil, err
	}

	lines := strings.Split(string(output), "\n")

	var gpus []string
	for _, line := range lines {
		if strings.Contains(line, "VGA compatible controller") {
			gpu, err := parseGPUInfo(line)
			if err != nil {
				continue
			}
			gpus = append(gpus, gpu)
		}
	}

	return gpus, nil
}
F
function

getMemoryInfo

Returns

string
error
core/specs.go:85-91
func getMemoryInfo() (string, error)

{
	vm, err := mem.VirtualMemory()
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%d MB", vm.Total/1024/1024), nil
}
F
function

GetPCSpecs

Returns

core/specs.go:93-103
func GetPCSpecs() PCSpecs

{
	cpu, _ := getCPUInfo()
	gpu, _ := getGPUInfo()
	memory, _ := getMemoryInfo()

	return PCSpecs{
		CPU:    cpu,
		GPU:    gpu,
		Memory: memory,
	}
}
F
function

init

core/kargs.go:29-33
func init()

{
	if os.Getenv("ABROOT_KARGS_PATH") != "" {
		KargsPath = os.Getenv("ABROOT_KARGS_PATH")
	}
}
F
function

kargsCreateIfMissing

kargsCreateIfMissing creates the kargs file if it doesn’t exist

Returns

error
core/kargs.go:36-50
func kargsCreateIfMissing() error

{
	PrintVerboseInfo("kargsCreateIfMissing", "running...")

	if _, err := os.Stat(KargsPath); os.IsNotExist(err) {
		PrintVerboseInfo("kargsCreateIfMissing", "creating kargs file...")
		err = os.WriteFile(KargsPath, []byte(DefaultKargs), 0644)
		if err != nil {
			PrintVerboseErr("kargsCreateIfMissing", 0, err)
			return err
		}
	}

	PrintVerboseInfo("kargsCreateIfMissing", "done")
	return nil
}
F
function

KargsWrite

KargsWrite makes a backup of the current kargs file and then
writes the new content to it

Parameters

content
string

Returns

error
core/kargs.go:54-83
func KargsWrite(content string) error

{
	PrintVerboseInfo("KargsWrite", "running...")

	err := kargsCreateIfMissing()
	if err != nil {
		PrintVerboseErr("KargsWrite", 0, err)
		return err
	}

	validated, err := KargsFormat(content)
	if err != nil {
		PrintVerboseErr("KargsWrite", 1, err)
		return err
	}

	err = KargsBackup()
	if err != nil {
		PrintVerboseErr("KargsWrite", 2, err)
		return err
	}

	err = os.WriteFile(KargsPath, []byte(validated), 0644)
	if err != nil {
		PrintVerboseErr("KargsWrite", 3, err)
		return err
	}

	PrintVerboseInfo("KargsWrite", "done")
	return nil
}
F
function

KargsBackup

KargsBackup makes a backup of the current kargs file

Returns

error
core/kargs.go:86-103
func KargsBackup() error

{
	PrintVerboseInfo("KargsBackup", "running...")

	content, err := KargsRead()
	if err != nil {
		PrintVerboseErr("KargsBackup", 0, err)
		return err
	}

	err = os.WriteFile(KargsPath+".bak", []byte(content), 0644)
	if err != nil {
		PrintVerboseErr("KargsBackup", 1, err)
		return err
	}

	PrintVerboseInfo("KargsBackup", "done")
	return nil
}
F
function

KargsRead

KargsRead reads the content of the kargs file

Returns

string
error
core/kargs.go:106-123
func KargsRead() (string, error)

{
	PrintVerboseInfo("KargsRead", "running...")

	err := kargsCreateIfMissing()
	if err != nil {
		PrintVerboseErr("KargsRead", 0, err)
		return "", err
	}

	content, err := os.ReadFile(KargsPath)
	if err != nil {
		PrintVerboseErr("KargsRead", 1, err)
		return "", err
	}

	PrintVerboseInfo("KargsRead", "done")
	return string(content), nil
}
F
function

KargsFormat

KargsFormat formats the contents of the kargs file, ensuring that
there are no duplicate entries, multiple spaces or trailing newline

Parameters

content
string

Returns

string
error
core/kargs.go:127-157
func KargsFormat(content string) (string, error)

{
	PrintVerboseInfo("KargsValidate", "running...")

	kargs := []string{}

	lines := strings.Split(content, "\n")
	for _, line := range lines {
		if line == "" {
			continue
		}

		lineArgs := strings.Split(line, " ")
		for _, larg := range lineArgs {
			// Check for duplicates
			isDuplicate := false
			for _, ka := range kargs {
				if ka == larg {
					isDuplicate = true
					break
				}
			}

			if !isDuplicate {
				kargs = append(kargs, larg)
			}
		}
	}

	PrintVerboseInfo("KargsValidate", "done")
	return strings.Join(kargs, " "), nil
}
F
function

KargsEdit

KargsEdit copies the kargs file to a temporary file and opens it in the
user’s preferred editor by querying the $EDITOR environment variable.
Once closed, its contents are written back to the main kargs file.
This function returns a boolean parameter indicating whether any changes
were made to the kargs file.

Returns

bool
error
core/kargs.go:164-224
func KargsEdit() (bool, error)

{
	PrintVerboseInfo("KargsEdit", "running...")

	editor := os.Getenv("EDITOR")
	if editor == "" {
		editor = "nano"
	}

	err := kargsCreateIfMissing()
	if err != nil {
		PrintVerboseErr("KargsEdit", 0, err)
		return false, err
	}

	// Open a temporary file, so editors installed via apx can also be used
	PrintVerboseInfo("KargsEdit", "Copying kargs file to /tmp")
	err = CopyFile(KargsPath, KargsTmpFile)
	if err != nil {
		PrintVerboseErr("KargsEdit", 1, err)
		return false, err
	}

	// Call $EDITOR on temp file
	PrintVerboseInfo("KargsEdit", "Opening", KargsTmpFile, "in", editor)
	cmd := exec.Command(editor, KargsTmpFile)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err = cmd.Run()
	if err != nil {
		PrintVerboseErr("KargsEdit", 2, err)
		return false, err
	}

	content, err := os.ReadFile(KargsTmpFile)
	if err != nil {
		PrintVerboseErr("KargsEdit", 3, err)
		return false, err
	}

	// Check whether there were any changes
	ogContent, err := os.ReadFile(KargsPath)
	if err != nil {
		PrintVerboseErr("KargsEdit", 4, err)
		return false, err
	}
	if string(ogContent) == string(content) {
		PrintVerboseInfo("KargsEdit", "No changes were made to kargs, skipping save.")
		return false, nil
	}

	PrintVerboseInfo("KargsEdit", "Writing contents of", KargsTmpFile, "to the original location")
	err = KargsWrite(string(content))
	if err != nil {
		PrintVerboseErr("KargsEdit", 5, err)
		return false, err
	}

	PrintVerboseInfo("KargsEdit", "Done")
	return true, nil
}
S
struct

PackageManager

PackageManager struct

core/packages.go:32-36
type PackageManager struct

Methods

Add
Method

Add adds a package to the packages.add file

Parameters

pkg string

Returns

error
func (*PackageManager) Add(pkg string) error
{
	PrintVerboseInfo("PackageManager.Add", "running...")

	// Check for package manager status and user agreement
	err := p.CheckStatus()
	if err != nil {
		PrintVerboseErr("PackageManager.Add", 0, err)
		return err
	}

	// Check if package was removed before
	packageWasRemoved := false
	removedIndex := -1
	pkgsRemove, err := p.GetRemovePackages()
	if err != nil {
		PrintVerboseErr("PackageManager.Add", 2.1, err)
		return err
	}
	for i, rp := range pkgsRemove {
		if rp == pkg {
			packageWasRemoved = true
			removedIndex = i
			break
		}
	}

	// packages that have been removed by the user aren't always in the repo
	if !packageWasRemoved {
		// Check if package exists in repo
		for _, _pkg := range strings.Split(pkg, " ") {
			err := p.ExistsInRepo(_pkg)
			if err != nil {
				PrintVerboseErr("PackageManager.Add", 0, err)
				return err
			}
		}
	}

	// If package was removed by the user, simply remove it from packages.remove
	if packageWasRemoved {
		pkgsRemove = append(pkgsRemove[:removedIndex], pkgsRemove[removedIndex+1:]...)
		PrintVerboseInfo("PackageManager.Add", "unsetting manually removed package")
		return p.writeRemovePackages(pkgsRemove)
	}

	// Abort if package is already added
	pkgsAdd, err := p.GetAddPackages()
	if err != nil {
		PrintVerboseErr("PackageManager.Add", 3, err)
		return err
	}
	for _, p := range pkgsAdd {
		if p == pkg {
			PrintVerboseInfo("PackageManager.Add", "package already added")
			return nil
		}
	}

	pkgsAdd = append(pkgsAdd, pkg)

	PrintVerboseInfo("PackageManager.Add", "writing packages.add")
	return p.writeAddPackages(pkgsAdd)
}
Remove
Method

Remove either removes a manually added package from packages.add or adds a package to be deleted into packages.remove

Parameters

pkg string

Returns

error
func (*PackageManager) Remove(pkg string) error
{
	PrintVerboseInfo("PackageManager.Remove", "running...")

	// Check for package manager status and user agreement
	err := p.CheckStatus()
	if err != nil {
		PrintVerboseErr("PackageManager.Remove", 0, err)
		return err
	}

	// Check if package exists in packages.add
	pkgsAddList, err := p.GetAddPackagesString(" ")
	if err != nil {
		PrintVerboseErr("PackageManager.Remove", 1, err)
		return err
	}
	if !strings.Contains(pkgsAddList, pkg) {

		// Check if package exists in repo
		// FIXME: this should also check if the package is installed in
		// different systems, not just debian-based ditros.. Since this is a
		// distro specific feature, I'm leaving it as is for now.
		err = p.ExistsOnSystem(pkg)
		if err != nil {
			PrintVerboseErr("PackageManager.Remove", 1, err)
			return err
		}
	}

	// If package was added by the user, simply remove it from packages.add
	pkgsAdd, err := p.GetAddPackages()
	if err != nil {
		PrintVerboseErr("PackageManager.Remove", 4, err)
		return err
	}
	for i, ap := range pkgsAdd {
		if ap == pkg {
			pkgsAdd = append(pkgsAdd[:i], pkgsAdd[i+1:]...)
			PrintVerboseInfo("PackageManager.Remove", "removing manually added package")
			return p.writeAddPackages(pkgsAdd)
		}
	}

	// Abort if package is already removed
	pkgsRemove, err := p.GetRemovePackages()
	if err != nil {
		PrintVerboseErr("PackageManager.Remove", 5, err)
		return err
	}
	for _, p := range pkgsRemove {
		if p == pkg {
			PrintVerboseInfo("PackageManager.Remove", "package already removed")
			return nil
		}
	}

	pkgsRemove = append(pkgsRemove, pkg)

	// Otherwise, add package to packages.remove
	PrintVerboseInfo("PackageManager.Remove", "writing packages.remove")
	return p.writeRemovePackages(pkgsRemove)
}

GetAddPackages returns the packages in the packages.add file

Returns

[]string
error
func (*PackageManager) GetAddPackages() ([]string, error)
{
	PrintVerboseInfo("PackageManager.GetAddPackages", "running...")
	return p.getPackages(PackagesAddFile)
}

GetRemovePackages returns the packages in the packages.remove file

Returns

[]string
error
func (*PackageManager) GetRemovePackages() ([]string, error)
{
	PrintVerboseInfo("PackageManager.GetRemovePackages", "running...")
	return p.getPackages(PackagesRemoveFile)
}

GetUnstagedPackages returns the package changes that are yet to be applied

Parameters

rootPath string

Returns

toBeAdded []string
toBeRemoved []string
err error
func (*PackageManager) GetUnstagedPackages(rootPath string) (toBeAdded, toBeRemoved []string, err error)
{
	PrintVerboseInfo("PackageManager.GetUnstagedPackages", "running...")
	alreadyAdded, alreadyRemoved, err := p.GetCurrentlyInstalledPackages(rootPath)
	if err != nil {
		PrintVerboseErr("PackageManager.GetUnstagedPackages", 1, err)
		return toBeAdded, toBeRemoved, err
	}

	shouldBeAdded, err := p.GetAddPackages()
	if err != nil {
		PrintVerboseErr("PackageManager.GetUnstagedPackages", 2, err)
		return toBeAdded, toBeRemoved, err
	}

	shouldBeRemoved, err := p.GetRemovePackages()
	if err != nil {
		PrintVerboseErr("PackageManager.GetUnstagedPackages", 2, err)
		return toBeAdded, toBeRemoved, err
	}

	addNew, removeAdded := sliceDifference(shouldBeAdded, alreadyAdded)
	removeNew, addRemoved := sliceDifference(shouldBeRemoved, alreadyRemoved)

	toBeAdded = append(addNew, addRemoved...)
	toBeRemoved = append(removeNew, removeAdded...)

	return toBeAdded, toBeRemoved, nil
}

GetAddPackagesString returns the packages in the packages.add file as a string

Parameters

sep string

Returns

string
error
func (*PackageManager) GetAddPackagesString(sep string) (string, error)
{
	PrintVerboseInfo("PackageManager.GetAddPackagesString", "running...")
	pkgs, err := p.GetAddPackages()
	if err != nil {
		PrintVerboseErr("PackageManager.GetAddPackagesString", 0, err)
		return "", err
	}

	PrintVerboseInfo("PackageManager.GetAddPackagesString", "done")
	return strings.Join(pkgs, sep), nil
}

GetRemovePackagesString returns the packages in the packages.remove file as a string

Parameters

sep string

Returns

string
error
func (*PackageManager) GetRemovePackagesString(sep string) (string, error)
{
	PrintVerboseInfo("PackageManager.GetRemovePackagesString", "running...")
	pkgs, err := p.GetRemovePackages()
	if err != nil {
		PrintVerboseErr("PackageManager.GetRemovePackagesString", 0, err)
		return "", err
	}

	PrintVerboseInfo("PackageManager.GetRemovePackagesString", "done")
	return strings.Join(pkgs, sep), nil
}
getPackages
Method

Parameters

file string

Returns

[]string
error
func (*PackageManager) getPackages(file string) ([]string, error)
{
	PrintVerboseInfo("PackageManager.getPackages", "running...")

	pkgs := []string{}
	f, err := os.Open(filepath.Join(p.baseDir, file))
	if err != nil {
		PrintVerboseErr("PackageManager.getPackages", 0, err)
		return pkgs, err
	}
	defer f.Close()

	b, err := io.ReadAll(f)
	if err != nil {
		PrintVerboseErr("PackageManager.getPackages", 1, err)
		return pkgs, err
	}

	for _, pkg := range strings.Split(string(b), "\n") {
		pkgTrimmed := strings.TrimSpace(pkg)
		if pkgTrimmed != "" {
			pkgs = append(pkgs, pkgTrimmed)
		}
	}

	PrintVerboseInfo("PackageManager.getPackages", "returning packages")
	return pkgs, nil
}

Parameters

pkgs []string

Returns

error
func (*PackageManager) writeAddPackages(pkgs []string) error
{
	PrintVerboseInfo("PackageManager.writeAddPackages", "running...")
	return p.writePackages(PackagesAddFile, pkgs)
}

Parameters

pkgs []string

Returns

error
func (*PackageManager) writeRemovePackages(pkgs []string) error
{
	PrintVerboseInfo("PackageManager.writeRemovePackages", "running...")
	return p.writePackages(PackagesRemoveFile, pkgs)
}
writePackages
Method

Parameters

file string
pkgs []string

Returns

error
func (*PackageManager) writePackages(file string, pkgs []string) error
{
	PrintVerboseInfo("PackageManager.writePackages", "running...")

	f, err := os.Create(filepath.Join(p.baseDir, file))
	if err != nil {
		PrintVerboseErr("PackageManager.writePackages", 0, err)
		return err
	}
	defer f.Close()

	for _, pkg := range pkgs {
		if pkg == "" {
			continue
		}

		_, err = fmt.Fprintf(f, "%s\n", pkg)
		if err != nil {
			PrintVerboseErr("PackageManager.writePackages", 1, err)
			return err
		}
	}

	PrintVerboseInfo("PackageManager.writePackages", "packages written")
	return nil
}

Returns

commands []string
err error
func (*PackageManager) createPackageCommands() (commands []string, err error)
{
	addPkgs, err := p.GetAddPackagesString(" ")
	if err != nil {
		PrintVerboseErr("PackageManager.processUpgradePackages", 0, err)
		return
	}

	removePkgs, err := p.GetRemovePackagesString(" ")
	if err != nil {
		PrintVerboseErr("PackageManager.processUpgradePackages", 1, err)
		return
	}

	if len(addPkgs) == 0 && len(removePkgs) == 0 {
		PrintVerboseInfo("PackageManager.processUpgradePackages", "no packages to install or remove")
		return []string{}, nil
	}

	if removePkgs != "" {
		finalRemovePkgs := fmt.Sprintf("%s %s", settings.Cnf.IPkgMngRm, removePkgs)
		commands = append(commands, finalRemovePkgs)
	}

	if addPkgs != "" {
		finalAddPkgs := fmt.Sprintf("%s %s", settings.Cnf.IPkgMngAdd, addPkgs)
		commands = append(commands, finalAddPkgs)
	}

	return commands, nil
}
GetFinalCmd
Method

Returns

string
error
func (*PackageManager) GetFinalCmd() (string, error)
{
	PrintVerboseInfo("PackageManager.GetFinalCmd", "running...")

	commands, err := p.createPackageCommands()
	if err != nil {
		return "", err
	}

	if len(commands) == 0 {
		return "", nil
	}

	preExec := settings.Cnf.IPkgMngPre
	postExec := settings.Cnf.IPkgMngPost
	if preExec != "" {
		commands = append([]string{preExec}, commands...)
	}
	if postExec != "" {
		commands = append(commands, postExec)
	}

	cmd := strings.Join(commands, " && ")

	PrintVerboseInfo("PackageManager.GetFinalCmd", "returning cmd: "+cmd)
	return cmd, nil
}
getSummary
Method

Returns

string
error
func (*PackageManager) getSummary() (string, error)
{
	if p.CheckStatus() != nil {
		return "", nil
	}

	addPkgs, err := p.GetAddPackages()
	if err != nil {
		if errors.Is(err, &os.PathError{}) {
			addPkgs = []string{}
		} else {
			return "", err
		}
	}
	removePkgs, err := p.GetRemovePackages()
	if err != nil {
		if errors.Is(err, &os.PathError{}) {
			removePkgs = []string{}
		} else {
			return "", err
		}
	}

	// GetPackages returns slices with one empty element if there are no packages
	if len(addPkgs) == 1 && addPkgs[0] == "" {
		addPkgs = []string{}
	}
	if len(removePkgs) == 1 && removePkgs[0] == "" {
		removePkgs = []string{}
	}

	summary := ""

	for _, pkg := range addPkgs {
		summary += "+ " + pkg + "\n"
	}
	for _, pkg := range removePkgs {
		summary += "- " + pkg + "\n"
	}

	return summary, nil
}

WriteSummaryToFile writes added and removed packages to the root specified by rootPath added packages get the + prefix, while removed packages get the - prefix

Parameters

rootPath string

Returns

error
func (*PackageManager) WriteSummaryToRoot(rootPath string) error
{
	summaryFilePath := filepath.Join(rootPath, summaryFileLocation)

	summary, err := p.getSummary()
	if err != nil {
		return err
	}
	if summary == "" {
		return nil
	}
	summaryFile, err := os.Create(summaryFilePath)
	if err != nil {
		return err
	}
	defer summaryFile.Close()
	err = summaryFile.Chmod(0o644)
	if err != nil {
		return err
	}
	_, err = summaryFile.WriteString(summary)
	if err != nil {
		return err
	}

	return nil
}

GetCurrentlyInstalledPackages returns the currently installed packages

Parameters

rootPath string

Returns

added []string
removed []string
err error
func (*PackageManager) GetCurrentlyInstalledPackages(rootPath string) (added []string, removed []string, err error)
{
	summaryFilePath := filepath.Join(rootPath, summaryFileLocation)

	if _, err := os.Stat(summaryFilePath); errors.Is(err, os.ErrNotExist) {
		return added, removed, nil
	}

	content, err := os.ReadFile(summaryFilePath)
	if err != nil {
		PrintVerboseErr("PackageManager.GetCurrentlyInstalledPackages", 1, err)
		return added, removed, err
	}

	for _, line := range strings.Split(string(content), "\n") {
		addedPkg, isAdded := strings.CutPrefix(line, "+ ")
		if isAdded {
			added = append(added, addedPkg)
			continue
		}
		removedPkg, isRemoved := strings.CutPrefix(line, "- ")
		if isRemoved {
			removed = append(removed, removedPkg)
			continue
		}
		PrintVerboseWarn("PackageManager.GetCurrentlyInstalledPackages", 1, "line "+line+" is not a valid package string")
	}

	return added, removed, nil
}
ExistsInRepo
Method

Parameters

pkg string

Returns

error
func (*PackageManager) ExistsInRepo(pkg string) error
{
	PrintVerboseInfo("PackageManager.ExistsInRepo", "running...")

	ok, err := assertPkgMngApiSetUp()
	if err != nil {
		return err
	}
	if !ok {
		return nil
	}

	url := strings.Replace(settings.Cnf.IPkgMngApi, "{packageName}", pkg, 1)
	PrintVerboseInfo("PackageManager.ExistsInRepo", "checking if package exists in repo: "+url)

	resp, err := http.Get(url)
	if err != nil {
		PrintVerboseErr("PackageManager.ExistsInRepo", 0, err)
		return err
	}

	if resp.StatusCode != 200 {
		PrintVerboseInfo("PackageManager.ExistsInRepo", "package does not exist in repo")
		return fmt.Errorf("package does not exist in repo: %s", pkg)
	}

	PrintVerboseInfo("PackageManager.ExistsInRepo", "package exists in repo")
	return nil
}

Parameters

pkg string

Returns

error
func (*PackageManager) ExistsOnSystem(pkg string) error
{
	PrintVerboseInfo("PackageManager.ExistsOnSystem", "running...")

	PrintVerboseInfo("PackageManager.ExistsInRepo", "checking if package "+pkg+" exists on system: ")

	packageListFile, err := os.ReadFile("/var/lib/dpkg/status")
	if err != nil {
		PrintVerboseErr("PackageManager.ExistsOnSystem", 0, err)
		return p.ExistsInRepo(pkg)
	}

	if !strings.Contains(string(packageListFile), "Package: "+pkg) {
		PrintVerboseInfo("PackageManager.ExistsInRepo", "package does not exist on system")
		return fmt.Errorf("package does not exist on system: %s", pkg)
	}

	PrintVerboseInfo("PackageManager.ExistsInRepo", "package exists on system")
	return nil
}

AcceptUserAgreement sets the package manager status to enabled

Returns

error
func (*PackageManager) AcceptUserAgreement() error
{
	PrintVerboseInfo("PackageManager.AcceptUserAgreement", "running...")

	if p.Status != PKG_MNG_REQ_AGREEMENT {
		PrintVerboseInfo("PackageManager.AcceptUserAgreement", "package manager is not in agreement mode")
		return nil
	}

	err := os.WriteFile(
		PkgManagerUserAgreementFile,
		[]byte(time.Now().String()),
		0o644,
	)
	if err != nil {
		PrintVerboseErr("PackageManager.AcceptUserAgreement", 0, err)
		return err
	}

	return nil
}

GetUserAgreementStatus returns if the user has accepted the package manager agreement or not

Returns

bool
func (*PackageManager) GetUserAgreementStatus() bool
{
	PrintVerboseInfo("PackageManager.GetUserAgreementStatus", "running...")

	if p.Status != PKG_MNG_REQ_AGREEMENT {
		PrintVerboseInfo("PackageManager.GetUserAgreementStatus", "package manager is not in agreement mode")
		return true
	}

	_, err := os.Stat(PkgManagerUserAgreementFile)
	if err != nil {
		PrintVerboseInfo("PackageManager.GetUserAgreementStatus", "user has not accepted the agreement")
		return false
	}

	PrintVerboseInfo("PackageManager.GetUserAgreementStatus", "user has accepted the agreement")
	return true
}
CheckStatus
Method

CheckStatus checks if the package manager is enabled or not

Returns

error
func (*PackageManager) CheckStatus() error
{
	PrintVerboseInfo("PackageManager.CheckStatus", "running...")

	// Check if package manager is enabled
	if p.Status == PKG_MNG_DISABLED {
		PrintVerboseInfo("PackageManager.CheckStatus", "package manager is disabled")
		return nil
	}

	// Check if user has accepted the package manager agreement
	if p.Status == PKG_MNG_REQ_AGREEMENT {
		if !p.GetUserAgreementStatus() {
			PrintVerboseInfo("PackageManager.CheckStatus", "package manager agreement not accepted")
			err := errors.New("package manager agreement not accepted")
			return err
		}
	}

	PrintVerboseInfo("PackageManager.CheckStatus", "package manager is enabled")
	return nil
}

Fields

Name Type Description
dryRun bool
baseDir string
Status ABRootPkgManagerStatus
T
type

ABRootPkgManagerStatus

ABRootPkgManagerStatus represents the status of the package manager
in the ABRoot configuration file

core/packages.go:62-62
type ABRootPkgManagerStatus int
F
function

NewPackageManager

NewPackageManager returns a new PackageManager struct

Parameters

dryRun
bool

Returns

core/packages.go:65-118
func NewPackageManager(dryRun bool) (*PackageManager, error)

{
	PrintVerboseInfo("PackageManager.NewPackageManager", "running...")

	baseDir := PackagesBaseDir
	if dryRun {
		baseDir = DryRunPackagesBaseDir
	}

	err := os.MkdirAll(baseDir, 0o755)
	if err != nil {
		PrintVerboseErr("PackageManager.NewPackageManager", 0, err)
		return nil, err
	}

	_, err = os.Stat(filepath.Join(baseDir, PackagesAddFile))
	if err != nil {
		err = os.WriteFile(
			filepath.Join(baseDir, PackagesAddFile),
			[]byte(""),
			0o644,
		)
		if err != nil {
			PrintVerboseErr("PackageManager.NewPackageManager", 1, err)
			return nil, err
		}
	}

	_, err = os.Stat(filepath.Join(baseDir, PackagesRemoveFile))
	if err != nil {
		err = os.WriteFile(
			filepath.Join(baseDir, PackagesRemoveFile),
			[]byte(""),
			0o644,
		)
		if err != nil {
			PrintVerboseErr("PackageManager.NewPackageManager", 2, err)
			return nil, err
		}
	}

	// here we convert settings.Cnf.IPkgMngStatus to an ABRootPkgManagerStatus
	// for easier understanding in the code
	var status ABRootPkgManagerStatus
	switch settings.Cnf.IPkgMngStatus {
	case PKG_MNG_REQ_AGREEMENT:
		status = PKG_MNG_REQ_AGREEMENT
	case PKG_MNG_ENABLED:
		status = PKG_MNG_ENABLED
	default:
		status = PKG_MNG_DISABLED
	}

	return &PackageManager{dryRun, baseDir, status}, nil
}
F
function

assertPkgMngApiSetUp

assertPkgMngApiSetUp checks whether the repo API is properly configured.
If a configuration exists but is malformed, returns an error.

Returns

bool
error
core/packages.go:547-564
func assertPkgMngApiSetUp() (bool, error)

{
	if settings.Cnf.IPkgMngApi == "" {
		PrintVerboseInfo("PackageManager.assertPkgMngApiSetUp", "no API url set, will not check if package exists. This could lead to errors")
		return false, nil
	}

	_, err := url.ParseRequestURI(settings.Cnf.IPkgMngApi)
	if err != nil {
		return false, fmt.Errorf("PackageManager.assertPkgMngApiSetUp: Value set as API url (%s) is not a valid URL", settings.Cnf.IPkgMngApi)
	}

	if !strings.Contains(settings.Cnf.IPkgMngApi, "{packageName}") {
		return false, fmt.Errorf("PackageManager.assertPkgMngApiSetUp: API url does not contain {packageName} placeholder. ABRoot is probably misconfigured, please report the issue to the maintainers of the distribution")
	}

	PrintVerboseInfo("PackageManager.assertPkgMngApiSetUp", "Repo is set up properly")
	return true, nil
}
F
function

GetRepoContentsForPkg

GetRepoContentsForPkg retrieves package information from the repository API

Parameters

pkg
string

Returns

map[string]interface{}
error
core/packages.go:616-650
func GetRepoContentsForPkg(pkg string) (map[string]interface{}, error)

{
	PrintVerboseInfo("PackageManager.GetRepoContentsForPkg", "running...")

	ok, err := assertPkgMngApiSetUp()
	if err != nil {
		return map[string]interface{}{}, err
	}
	if !ok {
		return map[string]interface{}{}, errors.New("PackageManager.GetRepoContentsForPkg: no API url set, cannot query package information")
	}

	url := strings.Replace(settings.Cnf.IPkgMngApi, "{packageName}", pkg, 1)
	PrintVerboseInfo("PackageManager.GetRepoContentsForPkg", "fetching package information in: "+url)

	resp, err := http.Get(url)
	if err != nil {
		PrintVerboseErr("PackageManager.GetRepoContentsForPkg", 0, err)
		return map[string]interface{}{}, err
	}

	contents, err := io.ReadAll(resp.Body)
	if err != nil {
		PrintVerboseErr("PackageManager.GetRepoContentsForPkg", 1, err)
		return map[string]interface{}{}, err
	}

	pkgInfo := map[string]interface{}{}
	err = json.Unmarshal(contents, &pkgInfo)
	if err != nil {
		PrintVerboseErr("PackageManager.GetRepoContentsForPkg", 2, err)
		return map[string]interface{}{}, err
	}

	return pkgInfo, nil
}
F
function

sliceDifference

Parameters

a
[]string
b
[]string

Returns

onlyInA
[]string
onlyInB
[]string
core/packages.go:717-741
func sliceDifference(a, b []string) (onlyInA, onlyInB []string)

{
	inA := make(map[string]struct{})
	inB := make(map[string]struct{})

	for _, item := range a {
		inA[item] = struct{}{}
	}
	for _, item := range b {
		inB[item] = struct{}{}
	}

	for _, item := range a {
		if _, found := inB[item]; !found {
			onlyInA = append(onlyInA, item)
		}
	}

	for _, item := range b {
		if _, found := inA[item]; !found {
			onlyInB = append(onlyInB, item)
		}
	}

	return onlyInA, onlyInB
}
F
function

AtomicSwap

atomicSwap allows swapping 2 files or directories in-place and atomically,
using the renameat2 syscall. This should be used instead of os.Rename,
which is not atomic at all

Parameters

src
string
dst
string

Returns

error
core/atomic-io.go:26-49
func AtomicSwap(src, dst string) error

{
	PrintVerboseInfo("AtomicSwap", "running...")

	orig, err := os.Open(src)
	if err != nil {
		PrintVerboseErr("AtomicSwap", 0, err)
		return err
	}

	newfile, err := os.Open(dst)
	if err != nil {
		PrintVerboseErr("AtomicSwap", 1, err)
		return err
	}

	err = unix.Renameat2(int(orig.Fd()), src, int(newfile.Fd()), dst, unix.RENAME_EXCHANGE)
	if err != nil {
		PrintVerboseErr("AtomicSwap", 2, err)
		return err
	}

	PrintVerboseInfo("AtomicSwap", "done")
	return nil
}
F
function

MergeDiff

MergeDiff merges the diff lines between the first and second files into
the destination file. If any errors occur, they are returned.

Parameters

firstFile
string
secondFile
string
destination
string

Returns

error
core/diff.go:24-55
func MergeDiff(firstFile, secondFile, destination string) error

{
	PrintVerboseInfo("MergeDiff", "merging", firstFile, "+", secondFile, "->", destination)

	// get the diff lines
	diffLines, err := DiffFiles(firstFile, secondFile)
	if err != nil {
		PrintVerboseErr("MergeDiff", 0, err)
		return err
	}

	// copy second file to destination to apply patch
	secondFileContents, err := os.ReadFile(secondFile)
	if err != nil {
		PrintVerboseErr("MergeDiff", 1, err)
		return err
	}
	err = os.WriteFile(destination, secondFileContents, 0644)
	if err != nil {
		PrintVerboseErr("MergeDiff", 2, err)
		return err
	}

	// write the diff to the destination
	err = WriteDiff(destination, diffLines)
	if err != nil {
		PrintVerboseErr("MergeDiff", 3, err)
		return err
	}

	PrintVerboseInfo("MergeDiff", "merge completed")
	return nil
}
F
function

DiffFiles

DiffFiles returns the diff lines between source and dest files using the
diff command (assuming it is installed). If no diff is found, nil is
returned. If any errors occur, they are returned.

Parameters

sourceFile
string
destFile
string

Returns

[]byte
error
core/diff.go:60-82
func DiffFiles(sourceFile, destFile string) ([]byte, error)

{
	PrintVerboseInfo("DiffFiles", "diffing", sourceFile, "and", destFile)

	cmd := exec.Command("diff", "-u", sourceFile, destFile)
	var out bytes.Buffer
	cmd.Stdout = &out
	errCode := 0
	err := cmd.Run()
	if err != nil {
		if exitError, ok := err.(*exec.ExitError); ok {
			errCode = exitError.ExitCode()
		}
	}

	// diff returns 1 if there are differences
	if errCode == 1 {
		PrintVerboseInfo("DiffFiles", "diff found")
		return out.Bytes(), nil
	}

	PrintVerboseInfo("DiffFiles", "no diff found")
	return nil, nil
}
F
function

WriteDiff

WriteDiff applies the diff lines to the destination file using the patch
command (assuming it is installed). If any errors occur, they are returned.

Parameters

destFile
string
diffLines
[]byte

Returns

error
core/diff.go:86-105
func WriteDiff(destFile string, diffLines []byte) error

{
	PrintVerboseInfo("WriteDiff", "applying diff to", destFile)
	if len(diffLines) == 0 {
		PrintVerboseInfo("WriteDiff", "no changes to apply")
		return nil // no changes to apply
	}

	cmd := exec.Command("patch", "-R", destFile)
	cmd.Stdin = bytes.NewReader(diffLines)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err := cmd.Run()
	if err != nil {
		PrintVerboseErr("WriteDiff", 0, err)
		return err
	}

	PrintVerboseInfo("WriteDiff", "diff applied")
	return nil
}
S
struct

Grub

Grub represents a grub instance, it exposes methods to generate a new grub
config compatible with ABRoot, and to check if the system is booted into
the present root or the future root

core/grub.go:29-32
type Grub struct

Methods

Returns

bool
error
func (*Grub) IsBootedIntoPresentRoot() (bool, error)
{
	PrintVerboseInfo("Grub.IsBootedIntoPresentRoot", "running...")

	a := NewABRootManager()
	future, err := a.GetFuture()
	if err != nil {
		return false, err
	}

	if g.FutureRoot == "a" {
		PrintVerboseInfo("Grub.IsBootedIntoPresentRoot", "done")
		return future.Label == settings.Cnf.PartLabelA, nil
	} else {
		PrintVerboseInfo("Grub.IsBootedIntoPresentRoot", "done")
		return future.Label == settings.Cnf.PartLabelB, nil
	}
}

Fields

Name Type Description
PresentRoot string
FutureRoot string
F
function

generateABGrubConf

generateABGrubConf generates a new grub config with the given details

Parameters

kernelVersion
string
rootPath
string
rootUuid
string
rootLabel
string
generatedGrubConfigPath
string

Returns

error
core/grub.go:35-100
func generateABGrubConf(kernelVersion string, rootPath string, rootUuid string, rootLabel string, generatedGrubConfigPath string) error

{
	PrintVerboseInfo("generateABGrubConf", "generating grub config for ABRoot")

	kargs, err := KargsRead()
	if err != nil {
		PrintVerboseErr("generateABGrubConf", 0, err)
		return err
	}

	var grubPath, bootPrefix, systemRoot string
	if settings.Cnf.ThinProvisioning {
		grubPath = filepath.Join(rootPath, "boot", "init", rootLabel)
		bootPrefix = "/" + rootLabel

		diskM := NewDiskManager()
		sysRootPart, err := diskM.GetPartitionByLabel(rootLabel)
		if err != nil {
			PrintVerboseErr("generateABGrubConf", 3, err)
			return err
		}
		systemRoot = "/dev/mapper/" + sysRootPart.Device
	} else {
		grubPath = filepath.Join(rootPath, "boot", "grub")
		bootPrefix = "/.system/boot"
		systemRoot = "UUID=" + rootUuid
	}

	confPath := filepath.Join(grubPath, "abroot.cfg")
	template := `  search --no-floppy --fs-uuid --set=root %s
  linux   %s/vmlinuz-%s root=%s %s
  initrd  %s/initrd.img-%s
`

	err = os.MkdirAll(grubPath, 0755)
	if err != nil {
		PrintVerboseErr("generateABGrubConf", 2, err)
		return err
	}

	abrootBootConfig := fmt.Sprintf(template, rootUuid, bootPrefix, kernelVersion, systemRoot, kargs, bootPrefix, kernelVersion)

	generatedGrubConfigContents, err := os.ReadFile(filepath.Join(rootPath, generatedGrubConfigPath))
	if err != nil {
		PrintVerboseErr("generateABGrubConf", 3, "could not read grub config", err)
		return err
	}

	generatedGrubConfig := string(generatedGrubConfigContents)

	replacementString := "REPLACED_BY_ABROOT"
	if !strings.Contains(generatedGrubConfig, replacementString) {
		err := errors.New("could not find replacement string \"" + replacementString + "\", check /etc/grub.d configuration")
		PrintVerboseErr("generateABGrubConf", 3.1, err)
		return err
	}
	grubConfigWithBootEntry := strings.Replace(generatedGrubConfig, "REPLACED_BY_ABROOT", abrootBootConfig, 1)

	err = os.WriteFile(confPath, []byte(grubConfigWithBootEntry), 0644)
	if err != nil {
		PrintVerboseErr("generateABGrubConf", 4, "could not read grub config", err)
		return err
	}

	PrintVerboseInfo("generateABGrubConf", "done")
	return nil
}
F
function

NewGrub

NewGrub creates a new Grub instance

Parameters

bootPart

Returns

error
core/grub.go:103-144
func NewGrub(bootPart Partition) (*Grub, error)

{
	PrintVerboseInfo("NewGrub", "running...")

	grubPath := filepath.Join(bootPart.MountPoint, "grub")
	confPath := filepath.Join(grubPath, "grub.cfg")

	cfg, err := os.ReadFile(confPath)
	if err != nil {
		PrintVerboseErr("NewGrub", 0, err)
		return nil, err
	}

	var presentRoot, futureRoot string

	for _, entry := range strings.Split(string(cfg), "\n") {
		if strings.Contains(entry, "abroot-a") {
			if strings.Contains(entry, "Current State") {
				presentRoot = "a"
			} else if strings.Contains(entry, "Previous State") {
				futureRoot = "a"
			}
		} else if strings.Contains(entry, "abroot-b") {
			if strings.Contains(entry, "Current State") {
				presentRoot = "b"
			} else if strings.Contains(entry, "Previous State") {
				futureRoot = "b"
			}
		}
	}

	if presentRoot == "" || futureRoot == "" {
		err := errors.New("could not find root partitions")
		PrintVerboseErr("NewGrub", 1, err)
		return nil, err
	}

	PrintVerboseInfo("NewGrub", "done")
	return &Grub{
		PresentRoot: presentRoot,
		FutureRoot:  futureRoot,
	}, nil
}
S
struct

ImageRecipe

An ImageRecipe represents a Dockerfile/Containerfile-like recipe

core/image-recipe.go:22-27
type ImageRecipe struct

Methods

Write
Method

Write writes a ImageRecipe to the given path, returning an error if any

Parameters

path string

Returns

error
func (*ImageRecipe) Write(path string) error
{
	PrintVerboseInfo("ImageRecipe.Write", "running...")

	// create file
	file, err := os.Create(path)
	if err != nil {
		PrintVerboseErr("ImageRecipe.Write", 0, err)
		return err
	}
	defer file.Close()

	// write from
	_, err = file.WriteString(fmt.Sprintf("FROM %s\n", c.From))
	if err != nil {
		PrintVerboseErr("ImageRecipe.Write", 1, err)
		return err
	}

	// write labels
	for key, value := range c.Labels {
		_, err = file.WriteString(fmt.Sprintf("LABEL %s=%s\n", key, value))
		if err != nil {
			PrintVerboseErr("ImageRecipe.Write", 2, err)
			return err
		}
	}

	// write args
	for key, value := range c.Args {
		_, err = file.WriteString(fmt.Sprintf("ARG %s=%s\n", key, value))
		if err != nil {
			PrintVerboseErr("ImageRecipe.Write", 3, err)
			return err
		}
	}

	// write content
	_, err = file.WriteString(c.Content)
	if err != nil {
		PrintVerboseErr("ImageRecipe.Write", 4, err)
		return err
	}

	PrintVerboseInfo("ImageRecipe.Write", "done")
	return nil
}

Fields

Name Type Description
From string
Labels map[string]string
Args map[string]string
Content string
F
function

NewImageRecipe

NewImageRecipe creates a new ImageRecipe instance and returns a pointer to it

Parameters

image
string
labels
map[string]string
args
map[string]string
content
string

Returns

core/image-recipe.go:30-39
func NewImageRecipe(image string, labels map[string]string, args map[string]string, content string) *ImageRecipe

{
	PrintVerboseInfo("NewImageRecipe", "running...")

	return &ImageRecipe{
		From:    image,
		Labels:  labels,
		Args:    args,
		Content: content,
	}
}
F
function

getKernelVersion

getKernelVersion returns the latest kernel version available in the root

Parameters

bootPath
string

Returns

string
core/kernel.go:24-56
func getKernelVersion(bootPath string) string

{
	PrintVerboseInfo("getKernelVersion", "running...")

	kernelDir := filepath.Join(bootPath, "vmlinuz-*")
	files, err := filepath.Glob(kernelDir)
	if err != nil {
		PrintVerboseErr("getKernelVersion", 0, err)
		return ""
	}

	if len(files) == 0 {
		PrintVerboseErr("getKernelVersion", 1, errors.New("no kernel found"))
		return ""
	}

	var maxVer *version.Version
	for _, file := range files {
		verStr := filepath.Base(file)[8:]
		ver, err := version.NewVersion(verStr)
		if err != nil {
			continue
		}
		if maxVer == nil || ver.GreaterThan(maxVer) {
			maxVer = ver
		}
	}

	if maxVer != nil {
		return maxVer.String()
	}

	return ""
}
S
struct

ABSystem

An ABSystem allows to perform system operations such as upgrades,
package changes and rollback on an ABRoot-compliant system.

core/system.go:33-46
type ABSystem struct

Methods

CheckAll
Method

CheckAll performs all checks from the Checks struct

Returns

error
func (*ABSystem) CheckAll() error
{
	PrintVerboseInfo("ABSystem.CheckAll", "running...")

	err := s.Checks.PerformAllChecks()
	if err != nil {
		PrintVerboseErr("ABSystem.CheckAll", 0, err)
		return err
	}

	PrintVerboseInfo("ABSystem.CheckAll", "all checks passed")
	return nil
}
CheckUpdate
Method

CheckUpdate checks if there is an update available

Returns

bool
error
func (*ABSystem) CheckUpdate() (digest.Digest, bool, error)
{
	PrintVerboseInfo("ABSystem.CheckUpdate", "running...")
	return HasUpdate(s.CurImage.Digest)
}
Rebase
Method

Parameters

name string
dryRun bool

Returns

error
func (*ABSystem) Rebase(name string, dryRun bool) error
{

	if name == "" {
		return fmt.Errorf("no image provided")
	}

	if strings.Contains(name, ".") {
		registrySplit := strings.SplitN(name, "/", 2)
		settings.Cnf.Registry = registrySplit[0]
		name = registrySplit[1]
	}
	nameTagSplit := strings.Split(name, ":")
	name = nameTagSplit[0]
	if len(nameTagSplit) < 1 {
		fmt.Errorf("No tag provided")
	}
	settings.Cnf.Tag = nameTagSplit[1]
	if name != "" {
		settings.Cnf.Name = name
	}

	_, _, err := s.CheckUpdate()
	if err != nil {
		return err
	}

	if !dryRun {
		err := settings.WriteConfigToFile(settings.CnfPathAdmin)
		if err != nil {
			return err
		}
	}

	return nil

}
RunOperation
Method

RunOperation executes a root-switching operation from the options below: UPGRADE: Upgrades to a new image, if available, FORCE_UPGRADE: Forces the upgrade operation, even if no new image is available, APPLY: Applies package changes, and updates the system if an update is available. INITRAMFS: Updates the initramfs for the future root, but doesn't update the system.

Parameters

operation ABSystemOperation
deleteBeforeCopy bool
dryRun bool

Returns

error
func (*ABSystem) RunOperation(operation ABSystemOperation, deleteBeforeCopy bool, dryRun bool) error
{
	PrintVerboseInfo("ABSystem.RunOperation", "starting", operation)

	cq := goodies.NewCleanupQueue()
	defer cq.Run()

	// Stage 0: Check if upgrade is possible
	// -------------------------------------
	PrintVerboseSimple("[Stage 0] -------- ABSystemRunOperation")

	if s.finishedFileExists() {
		PrintVerboseWarn("ABSystem.RunOperation", 0, "reboot required")
		return errors.New("another operation finished successfully, a reboot is required")
	}

	err := s.LockOperation()
	if err != nil {
		PrintVerboseErr("ABSystem.RunOperation", 0.1, "could not create lock file:", err)
		return fmt.Errorf("could not create lock file: %w", err)
	}

	cq.Add(func(args ...interface{}) error {
		return s.UnlockOperation()
	}, nil, 100, &goodies.NoErrorHandler{}, false)

	// removes the finalizing file if it exists
	err = s.removeFinalizingFile()
	if err != nil {
		PrintVerboseErr("ABSystem.RunOperation", 0.2, err)
		return err
	}

	// Stage 1: Check if there is an update available
	// ------------------------------------------------
	PrintVerboseSimple("[Stage 1] -------- ABSystemRunOperation")

	if UserStopRequested() {
		err = ErrUserStopped
		PrintVerboseErr("ABSystem.RunOperation", 2, err)
		return err
	}

	var imageDigest digest.Digest
	if operation != INITRAMFS {
		var res bool
		imageDigest, res, err = s.CheckUpdate()
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 1, err)
			return err
		}
		if !res {
			if operation != FORCE_UPGRADE && operation != APPLY {
				PrintVerboseErr("ABSystem.RunOperation", 1.1, err)
				return ErrNoUpdate
			}
			imageDigest = s.CurImage.Digest
			if operation == FORCE_UPGRADE {
				PrintVerboseWarn("ABSystem.RunOperation", 1.2, "No update available but --force is set. Proceeding...")
			}
		}
	} else {
		imageDigest = s.CurImage.Digest
	}

	// Stage 2: Get the present root, future root and boot partitions,
	// 			mount future to /part-future and clean up
	// 			old /part-future/new directory (it is
	// 			possible that last transaction was interrupted
	// 			before the clean up was done).
	// ------------------------------------------------
	PrintVerboseSimple("[Stage 2] -------- ABSystemRunOperation")

	if UserStopRequested() {
		err = ErrUserStopped
		PrintVerboseErr("ABSystem.RunOperation", 2, err)
		return err
	}

	partPresent, err := s.RootM.GetPresent()
	if err != nil {
		PrintVerboseErr("ABSystem.RunOperation", 2.05, err)
		return err
	}

	partFuture, err := s.RootM.GetFuture()
	if err != nil {
		PrintVerboseErr("ABSystem.RunOperation", 2.1, err)
		return err
	}

	partBoot, err := s.RootM.GetBoot()
	if err != nil {
		PrintVerboseErr("ABSystem.RunOperation", 2.2, err)
		return err
	}

	partFuture.Partition.Unmount() // just in case
	partBoot.Unmount()

	futureRoot := "/part-future"
	err = partFuture.Partition.Mount(futureRoot)
	if err != nil {
		PrintVerboseErr("ABSystem.RunOperation", 2.3, err)
		return err
	}

	cq.Add(func(args ...interface{}) error {
		return partFuture.Partition.Unmount()
	}, nil, 90, &goodies.NoErrorHandler{}, false)

	// Stage 3: Make a imageRecipe with user packages
	//         	then download the image
	// ------------------------------------------------
	PrintVerboseSimple("[Stage 3] -------- ABSystemRunOperation")

	if UserStopRequested() {
		err = ErrUserStopped
		PrintVerboseErr("ABSystem.RunOperation", 2, err)
		return err
	}

	// Stage 3.1: Delete old images
	if !dryRun {
		err = DeleteAllButLatestImage()
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 3.1, err)
			return err
		}
	}

	labels := map[string]string{
		"maintainer":  "'Generated by ABRoot'",
		"ABRoot.root": partFuture.Label,
	}
	args := map[string]string{}
	pkgM, err := NewPackageManager(false)
	if err != nil {
		PrintVerboseErr("ABSystem.RunOperation", 3.2, err)
		return err
	}

	pkgsFinal, err := pkgM.GetFinalCmd()
	if err != nil {
		PrintVerboseErr("ABSystem.RunOperation", 3.3, err)
	}
	if pkgsFinal == "" {
		pkgsFinal = "true"
	}
	content := `RUN ` + pkgsFinal

	var imageName string
	switch operation {
	case INITRAMFS:
		imageName, err = RetrieveImageForRoot(partPresent.Label)
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 3.4, err)
			return err
		}
		// Handle case where an image for the current root may not exist
		// in storage
		if imageName == "" {
			imageName = settings.GetFullImageNameWithTag()
		}
	default:
		imageName = settings.GetFullImageName()
		imageName += "@" + imageDigest.String()
		labels["ABRoot.BaseImageDigest"] = imageDigest.String()
	}

	imageRecipe := NewImageRecipe(
		imageName,
		labels,
		args,
		content,
	)

	// Stage 3.2: Download image
	if !dryRun {
		err = OciPullImage(imageName)
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 3.5, err)
			return err
		}
	}

	// Stage 4: Extract the rootfs
	// ------------------------------------------------
	PrintVerboseSimple("[Stage 4] -------- ABSystemRunOperation")

	if UserStopRequested() {
		err = ErrUserStopped
		PrintVerboseErr("ABSystem.RunOperation", 2, err)
		return err
	}

	err = s.createFinalizingFile()
	if err != nil {
		PrintVerboseErr("ABSystem.RunOperation", 5.3, err)
		return err
	}

	if deleteBeforeCopy || os.Getenv("ABROOT_DELETE_BEFORE_COPY") != "" {
		PrintVerboseInfo("ABSystemRunOperation", "Deleting future system, this will render the future root temporarily unavailable")
		if !dryRun {
			err := ClearDirectory(partFuture.Partition.MountPoint, nil)
			if err != nil {
				PrintVerboseErr("ABSystem.RunOperation", 4, err)
				return err
			}
		}
	}

	abrootTrans := filepath.Join(futureRoot, "abroot-trans")
	if !dryRun {
		err = OciExportRootFs(
			"abroot-"+uuid.New().String(),
			imageRecipe,
			abrootTrans,
			futureRoot,
		)
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 4.2, err)
			return err
		}
	}

	// Stage 4.1: Delete old images
	if !dryRun {
		err = DeleteAllButLatestImage()
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 3.1, err)
			return err
		}
	}

	// Stage 4.2: Repair root integrity
	if !dryRun {
		err = RepairRootIntegrity(futureRoot)
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 2.4, err)
			return err
		}
	}

	// Stage 5: Write new abimage.abr and config to future/
	// ------------------------------------------------
	PrintVerboseSimple("[Stage 5] -------- ABSystemRunOperation")

	abimage, err := NewABImage(imageDigest, settings.GetFullImageNameWithTag())
	if err != nil {
		PrintVerboseErr("ABSystem.RunOperation", 5.1, err)
		return err
	}

	if !dryRun {
		err = abimage.WriteTo(futureRoot)
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 5.2, err)
			return err
		}
	}

	varParent := s.RootM.VarPartition.Parent
	if varParent != nil && varParent.IsEncrypted() {
		device := varParent.Device
		if varParent.IsDevMapper() {
			device = "/dev/mapper/" + device
		} else {
			device = "/dev/" + device
		}

		settings.Cnf.PartCryptVar = device
	}

	if !dryRun {
		err = settings.WriteConfigToFile(filepath.Join(futureRoot, "/usr/share/abroot/abroot.json"))
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 5.25, err)
			return err
		}
	}

	if !dryRun {
		err = pkgM.WriteSummaryToRoot(futureRoot)
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 5.26, err)
			return err
		}
	}

	// Stage 6: Update the bootloader
	// ------------------------------------------------
	PrintVerboseSimple("[Stage 6] -------- ABSystemRunOperation")

	generatedGrubConfigPath := "/boot/grub/grub.cfg"

	if !dryRun {

		chroot, err := NewChroot(
			futureRoot,
			partFuture.Partition.Uuid,
			partFuture.Partition.Device,
			true,
			filepath.Join("/var/lib/abroot/etc", partPresent.Label),
		)
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 7.02, err)
			return err
		}

		grubCommand := fmt.Sprintf(settings.Cnf.UpdateGrubCmd, generatedGrubConfigPath)
		err = chroot.Execute(grubCommand)
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 7.1, err)
			return err
		}

		err = chroot.Execute(settings.Cnf.UpdateInitramfsCmd) // ensure initramfs is updated
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 7.2, err)
			return err
		}

		err = chroot.Close()
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 7.25, err)
			return err
		}

	}

	var newKernelVer string

	if !dryRun {
		newKernelVer = getKernelVersion(filepath.Join(futureRoot, "boot"))
		if newKernelVer == "" {
			err := errors.New("could not get kernel version")
			PrintVerboseErr("ABSystem.RunOperation", 7.26, err)
			return err
		}
	}

	var rootUuid string
	// If Thin-Provisioning set, mount init partition and move linux and initrd
	// images to it.
	var initMountpoint string
	if settings.Cnf.ThinProvisioning {
		initPartition, err := s.RootM.GetInit()
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 7.3, err)
			return err
		}

		initMountpoint = filepath.Join(futureRoot, "boot", "init")
		err = initPartition.Mount(initMountpoint)
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 7.4, err)
			return err
		}

		cq.Add(func(args ...interface{}) error {
			return initPartition.Unmount()
		}, nil, 80, &goodies.NoErrorHandler{}, false)

		futureInitDir := filepath.Join(initMountpoint, partFuture.Label)

		if !dryRun {
			err = os.RemoveAll(futureInitDir)
			if err != nil {
				PrintVerboseWarn("ABSystem.RunOperation", 7.44)
			}
			err = os.MkdirAll(futureInitDir, 0o755)
			if err != nil {
				PrintVerboseWarn("ABSystem.RunOperation", 7.47, err)
			}

			err = MoveFile(
				filepath.Join(futureRoot, "boot", "vmlinuz-"+newKernelVer),
				filepath.Join(futureInitDir, "vmlinuz-"+newKernelVer),
			)
			if err != nil {
				PrintVerboseErr("ABSystem.RunOperation", 7.5, err)
				return err
			}
			err = MoveFile(
				filepath.Join(futureRoot, "boot", "initrd.img-"+newKernelVer),
				filepath.Join(futureInitDir, "initrd.img-"+newKernelVer),
			)
			if err != nil {
				PrintVerboseErr("ABSystem.RunOperation", 7.6, err)
				return err
			}
			err = MoveFile(
				filepath.Join(futureRoot, "boot", "config-"+newKernelVer),
				filepath.Join(futureInitDir, "config-"+newKernelVer),
			)
			if err != nil {
				PrintVerboseErr("ABSystem.RunOperation", 7.7, err)
				return err
			}
			err = MoveFile(
				filepath.Join(futureRoot, "boot", "System.map-"+newKernelVer),
				filepath.Join(futureInitDir, "System.map-"+newKernelVer),
			)
			if err != nil {
				PrintVerboseErr("ABSystem.RunOperation", 7.8, err)
				return err
			}

		}

		rootUuid = initPartition.Uuid
	} else {
		rootUuid = partFuture.Partition.Uuid
	}

	if !dryRun {
		err = generateABGrubConf(
			newKernelVer,
			futureRoot,
			rootUuid,
			partFuture.Label,
			generatedGrubConfigPath,
		)
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 7.9, err)
			return err
		}
	}

	// Stage 7: Sync /etc
	// ------------------------------------------------
	PrintVerboseSimple("[Stage 7] -------- ABSystemRunOperation")

	oldEtc := "/sysconf" // The current etc WITHOUT anything overlayed
	oldUpperEtc := fmt.Sprintf("/var/lib/abroot/etc/%s", partPresent.Label)
	newUpperEtc := fmt.Sprintf("/var/lib/abroot/etc/%s", partFuture.Label)

	// make sure the future etc directories exist, ignoring errors
	newWorkEtc := fmt.Sprintf("/var/lib/abroot/etc/%s-work", partFuture.Label)
	if !dryRun {
		os.MkdirAll(newUpperEtc, 0o755)
		os.MkdirAll(newWorkEtc, 0o755)

		err = EtcBuilder.ExtBuildCommand(oldEtc, futureRoot+"/sysconf", oldUpperEtc, newUpperEtc)
		if err != nil {
			PrintVerboseErr("AbSystem.RunOperation", 8, err)
			return err
		}
	}

	// Stage 8: Mount boot partition
	// ------------------------------------------------
	PrintVerboseSimple("[Stage 8] -------- ABSystemRunOperation")

	tmpBootMount := "/run/abroot/tmp-boot-mount-1/"
	err = os.MkdirAll(tmpBootMount, 0o755)
	if err != nil {
		PrintVerboseErr("ABSystem.RunOperation", 9, err)
		return err
	}

	err = partBoot.Mount(tmpBootMount)
	if err != nil {
		PrintVerboseErr("ABSystem.RunOperation", 9.1, err)
		return err
	}

	cq.Add(func(args ...interface{}) error {
		return partBoot.Unmount()
	}, nil, 100, &goodies.NoErrorHandler{}, false)

	// Stage 9: Atomic swap the bootloader
	// ------------------------------------------------
	PrintVerboseSimple("[Stage 9] -------- ABSystemRunOperation")

	grub, err := NewGrub(partBoot)
	if err != nil {
		PrintVerboseErr("ABSystem.RunOperation", 11, err)
		return err
	}

	// Only swap grub entries if we're booted into the present partition
	isPresent, err := grub.IsBootedIntoPresentRoot()
	if err != nil {
		PrintVerboseErr("ABSystem.RunOperation", 11.1, err)
		return err
	}
	if !dryRun && isPresent {
		grubCfgCurrent := filepath.Join(tmpBootMount, "grub/grub.cfg")
		grubCfgFuture := filepath.Join(tmpBootMount, "grub/grub.cfg.future")

		// Just like in Stage 9, tmpBootMount/grub/grub.cfg.future may not exist.
		if _, err = os.Stat(grubCfgFuture); os.IsNotExist(err) {
			PrintVerboseInfo("ABSystem.RunOperation", "Creating grub.cfg.future")

			grubCfgContents, err := os.ReadFile(grubCfgCurrent)
			if err != nil {
				PrintVerboseErr("ABSystem.RunOperation", 11.2, err)
			}

			var replacerPairs []string
			if grub.FutureRoot == "a" {
				replacerPairs = []string{
					"default=1", "default=0",
					"Previous State (A)", "Current State (A)",
					"Current State (B)", "Previous State (B)",
				}
			} else {
				replacerPairs = []string{
					"default=0", "default=1",
					"Current State (A)", "Previous State (A)",
					"Previous State (B)", "Current State (B)",
				}
			}

			replacer := strings.NewReplacer(replacerPairs...)
			os.WriteFile(grubCfgFuture, []byte(replacer.Replace(string(grubCfgContents))), 0o644)
		}

		err = AtomicSwap(grubCfgCurrent, grubCfgFuture)
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 11.3, err)
			return err
		}
	}

	if !dryRun {
		err = s.createFinishedFile()
		if err != nil {
			PrintVerboseErr("ABSystem.RunOperation", 11.4, err)
			return fmt.Errorf("could not write finished file: %w", err)
		}
	}

	PrintVerboseInfo("ABSystem.RunOperation", "upgrade completed")
	return nil
}
Rollback
Method

Rollback swaps the master grub files if the current root is not the default

Parameters

checkOnly bool

Returns

err error
func (*ABSystem) Rollback(checkOnly bool) (response ABRollbackResponse, err error)
{
	PrintVerboseInfo("ABSystem.Rollback", "starting")

	cq := goodies.NewCleanupQueue()
	defer cq.Run()

	if s.finishedFileExists() {
		if checkOnly {
			return ROLLBACK_RES_NO, nil
		}
		return ROLLBACK_FAILED, errors.New("an operation finished successfully, can't roll back until reboot")
	}

	// we won't allow upgrades while rolling back
	if !checkOnly {
		err = s.LockOperation()
		if err != nil {
			PrintVerboseErr("ABSystem.Rollback", 0, err)
			return ROLLBACK_FAILED, fmt.Errorf("can't lock operation: %w", err)
		}
	}

	partBoot, err := s.RootM.GetBoot()
	if err != nil {
		PrintVerboseErr("ABSystem.Rollback", 1, err)
		return ROLLBACK_FAILED, err
	}

	tmpBootMount := "/run/abroot/tmp-boot-mount-2/"
	err = os.MkdirAll(tmpBootMount, 0o755)
	if err != nil {
		PrintVerboseErr("ABSystem.Rollback", 2, err)
		return ROLLBACK_FAILED, err
	}

	err = partBoot.Mount(tmpBootMount)
	if err != nil {
		PrintVerboseErr("ABSystem.Rollback", 3, err)
		return ROLLBACK_FAILED, err
	}

	cq.Add(func(args ...interface{}) error {
		return partBoot.Unmount()
	}, nil, 100, &goodies.NoErrorHandler{}, false)

	grub, err := NewGrub(partBoot)
	if err != nil {
		PrintVerboseErr("ABSystem.Rollback", 4, err)
		return ROLLBACK_FAILED, err
	}

	// Only swap grub entries if we're booted into the present partition
	isPresent, err := grub.IsBootedIntoPresentRoot()
	if err != nil {
		PrintVerboseErr("ABSystem.Rollback", 5, err)
		return ROLLBACK_FAILED, err
	}

	// If checkOnly is true, we stop here and return the appropriate response
	if checkOnly {
		response = ROLLBACK_RES_YES
		if isPresent {
			response = ROLLBACK_RES_NO
		}
		return response, nil
	}

	if isPresent {
		PrintVerboseInfo("ABSystem.Rollback", "current root is the default, nothing to do")
		return ROLLBACK_UNNECESSARY, nil
	}

	grubCfgCurrent := filepath.Join(tmpBootMount, "grub/grub.cfg")
	grubCfgFuture := filepath.Join(tmpBootMount, "grub/grub.cfg.future")

	// Just like in Stage 9, tmpBootMount/grub/grub.cfg.future may not exist.
	if _, err = os.Stat(grubCfgFuture); os.IsNotExist(err) {
		PrintVerboseInfo("ABSystem.Rollback", "Creating grub.cfg.future")

		grubCfgContents, err := os.ReadFile(grubCfgCurrent)
		if err != nil {
			PrintVerboseErr("ABSystem.Rollback", 6, err)
		}

		var replacerPairs []string
		if grub.FutureRoot == "a" {
			replacerPairs = []string{
				"default=1", "default=0",
				"A (previous)", "A (current)",
				"B (current)", "B (previous)",
			}
		} else {
			replacerPairs = []string{
				"default=0", "default=1",
				"A (current)", "A (previous)",
				"B (previous)", "B (current)",
			}
		}

		replacer := strings.NewReplacer(replacerPairs...)
		os.WriteFile(grubCfgFuture, []byte(replacer.Replace(string(grubCfgContents))), 0o644)
	}

	err = AtomicSwap(grubCfgCurrent, grubCfgFuture)
	if err != nil {
		PrintVerboseErr("ABSystem.RunOperation", 7, err)
		return ROLLBACK_FAILED, err
	}

	// allow upgrades after rolling back
	err = s.UnlockOperation()
	if err != nil {
		PrintVerboseErr("ABSystem.Rollback", 9, err)
		PrintVerboseInfo("ABSystem.Rollback", "rollback completed with unlock failure")
	}

	PrintVerboseInfo("ABSystem.Rollback", "rollback completed")
	return ROLLBACK_SUCCESS, nil
}
LockOperation
Method

LockOperation creates a lock file, preventing upgrades from proceeding Returns ErrOperationLocked if the operation is already locked by a running abroot instance

Returns

error
func (*ABSystem) LockOperation() error
{
	pid := os.Getpid()
	pidData := []byte(strconv.Itoa(pid))

	err := os.MkdirAll(filepath.Dir(operationLockFile), 0o755)
	if err != nil {
		PrintVerboseWarn("ABSystem.LockOperation", 1, err)
	}

	if _, err = os.Stat(operationLockFile); err == nil {
		if s.isLockfilePidActive() {
			return ErrOperationLocked
		}
	}

	err = os.WriteFile(operationLockFile, pidData, 0o644)
	if err != nil {
		os.Remove(operationLockFile)
		return fmt.Errorf("can't write lockfile: %w", err)
	}

	PrintVerboseInfo("ABSystem.LockOperation", "lock file created")
	return nil
}

UnlockOperation removes the lock file, allowing upgrades to proceed

Returns

error
func (*ABSystem) UnlockOperation() error
{
	err := os.Remove(operationLockFile)
	if err != nil {
		PrintVerboseErr("ABSystem.UnlockOperation", 0, err)
		return err
	}

	PrintVerboseInfo("ABSystem.UnlockOperation", "lock file removed")
	return nil
}

Returns

bool
func (*ABSystem) finishedFileExists() bool
{
	_, err := os.Stat(finishedOperationFile)
	return !errors.Is(err, os.ErrNotExist)
}

Returns

error
func (*ABSystem) createFinishedFile() error
{
	os.MkdirAll(filepath.Dir(finishedOperationFile), 0o755)

	_, err := os.Create(finishedOperationFile)
	if err != nil {
		return err
	}
	return nil
}

Returns

bool
func (*ABSystem) isLockfilePidActive() bool
{
	runningPid, err := os.ReadFile(operationLockFile)

	if errors.Is(err, os.ErrNotExist) {
		return false
	}

	if err != nil {
		PrintVerboseErr("ABSystem.RemoveStageFile", 0, err)
		return true
	}

	if string(runningPid) == "" {
		PrintVerboseWarn("ABSystem.isLockfilePidActive", 1, "lock file does not contain PID")
		return true
	}

	if _, err := os.Stat(filepath.Join("/proc", string(runningPid))); errors.Is(err, os.ErrNotExist) {
		return false
	}

	return true
}

Returns

error
func (*ABSystem) createFinalizingFile() error
{
	os.MkdirAll(filepath.Dir(finalizingFile), 0o755)

	_, err := os.Create(finalizingFile)
	if err != nil {
		return err
	}
	return nil
}

Returns

error
func (*ABSystem) removeFinalizingFile() error
{
	err := os.Remove(finalizingFile)
	if err != nil && !errors.Is(err, os.ErrNotExist) {
		return err
	}
	return nil
}

Fields

Name Type Description
Checks *Checks
RootM *ABRootManager
CurImage *ABImage
T
type

ABSystemOperation

ABSystemOperation represents a system operation to be performed by the
ABSystem, must be given as a parameter to the RunOperation function.

core/system.go:72-72
type ABSystemOperation string
T
type

ABRollbackResponse

ABRollbackResponse represents the response of a rollback operation

core/system.go:75-75
type ABRollbackResponse string
F
function

NewABSystem

NewABSystem initializes a new ABSystem, which contains all the functions
to perform system operations such as upgrades, package changes and rollback.
It returns a pointer to the initialized ABSystem and an error, if any.

Returns

error
core/system.go:93-110
func NewABSystem() (*ABSystem, error)

{
	PrintVerboseInfo("NewABSystem: running...")

	i, err := NewABImageFromRoot()
	if err != nil {
		PrintVerboseErr("NewABSystem", 0, err)
		return nil, err
	}

	c := NewChecks()
	rm := NewABRootManager()

	return &ABSystem{
		Checks:   c,
		RootM:    rm,
		CurImage: i,
	}, nil
}
F
function

UserStopRequested

UserStopRequested checks if the user lock file exists and returns a boolean
note that if the user lock file exists, it means that the user explicitly
requested the upgrade to be locked (using an update manager for example)

Returns

bool
core/system.go:939-946
func UserStopRequested() bool

{
	if _, err := os.Stat(userStopFile); os.IsNotExist(err) {
		return false
	}

	PrintVerboseInfo("ABSystem.UserStopRequested", "lock file exists")
	return true
}
F
function

MakeStopRequest

MakeStopRequest requests all other abroot operations to stop

It also prevents any new operations from running.

Returns

error
core/system.go:951-958
func MakeStopRequest() error

{
	os.MkdirAll(filepath.Dir(userStopFile), 0o755)
	err := os.WriteFile(userStopFile, []byte{}, 0o644)
	if err != nil {
		return fmt.Errorf("could not write stop file: %w", err)
	}
	return nil
}
F
function

CancelStopRequest

CancelStopRequest removes the stop request

Returns

error
core/system.go:961-971
func CancelStopRequest() error

{
	if !UserStopRequested() {
		return nil
	}

	err := os.Remove(userStopFile)
	if err != nil {
		return fmt.Errorf("could remove stop file: %w", err)
	}
	return nil
}
F
function

init

core/utils.go:29-41
func init()

{
	if !RootCheck(false) {
		return
	}

	if _, err := os.Stat(abrootDir); os.IsNotExist(err) {
		err := os.Mkdir(abrootDir, 0755)
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
	}
}
F
function

RootCheck

Parameters

display
bool

Returns

bool
core/utils.go:43-53
func RootCheck(display bool) bool

{
	if os.Geteuid() != 0 {
		if display {
			fmt.Println("You must be root to run this command")
		}

		return false
	}

	return true
}
F
function

fileExists

fileExists checks if a file exists

Parameters

path
string

Returns

bool
core/utils.go:56-64
func fileExists(path string) bool

{
	if _, err := os.Stat(path); err == nil {
		PrintVerboseInfo("fileExists", "File exists:", path)
		return true
	}

	PrintVerboseInfo("fileExists", "File does not exist:", path)
	return false
}
F
function

CopyFile

CopyFile copies a file from source to dest

Parameters

source
string
dest
string

Returns

error
core/utils.go:67-93
func CopyFile(source, dest string) error

{
	PrintVerboseInfo("CopyFile", "Running...")

	PrintVerboseInfo("CopyFile", "Opening source file")
	srcFile, err := os.Open(source)
	if err != nil {
		PrintVerboseErr("CopyFile", 0, err)
		return err
	}
	defer srcFile.Close()

	PrintVerboseInfo("CopyFile", "Opening destination file")
	destFile, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE, 0755)
	if err != nil {
		PrintVerboseErr("CopyFile", 1, err)
		return err
	}
	defer destFile.Close()

	PrintVerboseInfo("CopyFile", "Performing copy operation")
	if _, err := io.Copy(destFile, srcFile); err != nil {
		PrintVerboseErr("CopyFile", 2, err)
		return err
	}

	return nil
}
F
function

MoveFile

MoveFile copies a file from source to dest, then remove the source file

Parameters

source
string
dest
string

Returns

error
core/utils.go:96-110
func MoveFile(source, dest string) error

{
	err := CopyFile(source, dest)
	if err != nil {
		return err
	}

	PrintVerboseInfo("MoveFile", "Removing source file")
	err = os.Remove(source)
	if err != nil {
		PrintVerboseErr("MoveFile", 0, err)
		return err
	}

	return nil
}
F
function

ClearDirectory

ClearDirectory deletes all contents of a directory without removing the directory itself.
Skips files/directories specified in the skip list.

Parameters

path
string
skipList
[]string

Returns

error
core/utils.go:114-131
func ClearDirectory(path string, skipList []string) error

{
	files, err := os.ReadDir(path)
	if err != nil {
		return err
	}

	for _, file := range files {
		if slices.Contains(skipList, file.Name()) {
			continue
		}
		err = os.RemoveAll(filepath.Join(path, file.Name()))
		if err != nil {
			return err
		}
	}

	return nil
}
F
function

isDeviceLUKSEncrypted

isDeviceLUKSEncrypted checks whether a device specified by devicePath is a LUKS-encrypted device

Parameters

devicePath
string

Returns

bool
error
core/utils.go:134-155
func isDeviceLUKSEncrypted(devicePath string) (bool, error)

{
	PrintVerboseInfo("isDeviceLUKSEncrypted", "Verifying if", devicePath, "is encrypted")

	isLuksCmd := "cryptsetup isLuks %s"

	cmd := exec.Command("sh", "-c", fmt.Sprintf(isLuksCmd, devicePath))
	err := cmd.Run()
	if err != nil {
		// We expect the command to return exit status 1 if partition isn't
		// LUKS-encrypted
		if exitError, ok := err.(*exec.ExitError); ok {
			if exitError.ExitCode() == 1 {
				return false, nil
			}
		}
		err = fmt.Errorf("failed to check if %s is LUKS-encrypted: %s", devicePath, err)
		PrintVerboseErr("isDeviceLUKSEncrypted", 0, err)
		return false, err
	}

	return true, nil
}
F
function

getDirSize

getDirSize calculates the total size of a directory recursively.

Parameters

path
string

Returns

int64
error
core/utils.go:158-201
func getDirSize(path string) (int64, error)

{
	ds, err := os.Stat(path)
	if err != nil {
		return 0, err
	}
	if !ds.IsDir() {
		return 0, fmt.Errorf("%s is not a directory", path)
	}

	inodes := map[uint64]bool{}
	var totalSize int64 = 0

	dfs := os.DirFS(path)
	err = fs.WalkDir(dfs, ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}

		if !d.IsDir() {
			fileinfo, err := d.Info()
			if err != nil {
				return err
			}

			fileinfoSys := fileinfo.Sys().(*syscall.Stat_t)
			if fileinfoSys.Nlink > 1 {
				if _, ok := inodes[fileinfoSys.Ino]; !ok {
					totalSize += fileinfo.Size()
					inodes[fileinfoSys.Ino] = true
				}
			} else {
				totalSize += fileinfo.Size()
				inodes[fileinfoSys.Ino] = true
			}
		}

		return nil
	})
	if err != nil {
		return 0, err
	}

	return totalSize, nil
}
S
struct

Checks

Represents a Checks struct which contains all the checks which can
be performed one by one or all at once using PerformAllChecks()

core/checks.go:28-28
type Checks struct

Methods

PerformAllChecks performs all checks

Returns

error
func (*Checks) PerformAllChecks() error
{
	err := c.CheckCompatibilityFS()
	if err != nil {
		return err
	}

	err = c.CheckConnectivity()
	if err != nil {
		return err
	}

	err = c.CheckRoot()
	if err != nil {
		return err
	}

	return nil
}

CheckCompatibilityFS checks if the filesystem is compatible with ABRoot v2 if not, it returns an error. Note that currently only ext4, btrfs and xfs are supported/tested. Here we assume some utilities are installed, such as findmnt and lsblk

Returns

error
func (*Checks) CheckCompatibilityFS() error
{
	PrintVerboseInfo("Checks.CheckCompatibilityFS", "running...")

	var fs []string
	if runtime.GOOS == "linux" {
		fs = []string{"ext4", "btrfs", "xfs"}
	} else {
		err := fmt.Errorf("your OS (%s) is not supported", runtime.GOOS)
		PrintVerboseErr("Checks.CheckCompatibilityFS", 0, err)
		return err
	}

	cmd, err := exec.Command("findmnt", "-n", "-o", "source", "/").Output()
	if err != nil {
		PrintVerboseErr("Checks.CheckCompatibilityFS", 1, err)
		return err
	}
	device := string([]byte(cmd[:len(cmd)-1]))

	cmd, err = exec.Command("lsblk", "-o", "fstype", "-n", device).Output()
	if err != nil {
		PrintVerboseErr("Checks.CheckCompatibilityFS", 2, err)
		return err
	}
	fsType := string([]byte(cmd[:len(cmd)-1]))

	for _, f := range fs {
		if f == string(fsType) {
			PrintVerboseInfo("CheckCompatibilityFS", fsType, "is supported")
			return nil
		}
	}

	err = fmt.Errorf("the filesystem (%s) is not supported", fsType)
	PrintVerboseErr("Checks.CheckCompatibilityFS", 3, err)
	return err
}

CheckConnectivity checks if the system is connected to the internet

Returns

error
func (*Checks) CheckConnectivity() error
{
	PrintVerboseInfo("Checks.CheckConnectivity", "running...")

	timeout := 5 * time.Second
	_, err := net.DialTimeout("tcp", "vanillaos.org:80", timeout)
	if err != nil {
		PrintVerboseErr("Checks.CheckConnectivity", 1, err)
		return err
	}

	return nil
}
CheckRoot
Method

CheckRoot checks if the user is root and returns an error if not

Returns

error
func (*Checks) CheckRoot() error
{
	PrintVerboseInfo("Checks.CheckRoot", "running...")

	if os.Geteuid() == 0 {
		PrintVerboseInfo("Checks.CheckRoot", "you are root")
		return nil
	}

	err := errors.New("not root")
	PrintVerboseErr("Checks.CheckRoot", 1, err)
	return err
}
F
function

NewChecks

NewChecks returns a new Checks struct

Returns

core/checks.go:31-33
func NewChecks() *Checks

{
	return &Checks{}
}
T
type

ConfEditResult

ConfEditResult is the result of the ConfEdit function

core/conf.go:20-20
type ConfEditResult int
F
function

ConfEdit

ConfEdit opens the configuration file in the default editor

Returns

core/conf.go:23-94
func ConfEdit() (ConfEditResult, error)

{
	editor := os.Getenv("EDITOR")
	if editor == "" {
		nanoBin, err := exec.LookPath("nano")
		if err == nil {
			editor = nanoBin
		}

		viBin, err := exec.LookPath("vi")
		if err == nil {
			editor = viBin
		}

		editorBin, err := exec.LookPath("editor")
		if err == nil {
			editor = editorBin
		}

		if editor == "" {
			return CONF_FAILED, fmt.Errorf("no editor found in $EDITOR, nano or vi")
		}
	}

	// getting the configuration content so as we can compare it later
	// to see if it has been changed
	cnfContent, err := os.ReadFile(settings.CnfPathAdmin)
	if err != nil {
		return CONF_FAILED, err
	}

	tmpFilePath := settings.CnfPathAdmin + ".tmp.json"

	err = os.WriteFile(tmpFilePath, cnfContent, 0o755)
	if err != nil {
		return CONF_FAILED, err
	}
	defer os.Remove(tmpFilePath)

	// open the editor
	cmd := exec.Command(editor, tmpFilePath)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err = cmd.Run()
	if err != nil {
		return CONF_FAILED, err
	}

	// getting the new content
	newCnfContent, err := os.ReadFile(tmpFilePath)
	if err != nil {
		return CONF_FAILED, err
	}

	// we compare the old and new content to return the proper result
	if string(cnfContent) == string(newCnfContent) {
		return CONF_UNCHANGED, nil
	}

	viper.SetConfigFile(tmpFilePath)
	err = viper.ReadInConfig()
	if err != nil {
		return CONF_FAILED, err
	}

	err = AtomicSwap(settings.CnfPathAdmin, tmpFilePath)
	if err != nil {
		return CONF_FAILED, err
	}

	return CONF_CHANGED, nil
}