17 Commits
2.3.0 ... 2.6.0

Author SHA1 Message Date
Clayton Craft
741c0553d5 Allow including initramfs-extra files in the initramfs (MR 48)
This uses a "deviceinfo_create_initfs_extra" to allow including
initramfs-extra files in the initramfs and skip creating a separate
initramfs-extra archive when it's set to "false".
If this variable is unset, mkinitfs uses a default value of "false".
2024-09-27 12:13:24 -07:00
Clayton Craft
cd97df108a filelist: trim whitespace from lines read from files (MR 55)
Fixes issues with leading/trailing whitespaces really messing with mkinitfs
2024-07-11 14:18:55 -07:00
Arnav Singh
1fed057a82 doc: fix spelling typo 2024-06-25 11:26:10 -07:00
Caleb Connolly
5efdb9f170 archive: add /usr/sbin symlinks for UsrMerge (MR 53)
[ci:skip-build]: already built successfully in CI
2024-06-18 23:59:34 +02:00
Clayton Craft
81de8b438d archive,hookfiles: convert paths to usr-merge when necessary (MR 39)
Co-authored-by: Caleb Connolly <caleb@postmarketos.org>
2024-05-29 15:29:41 -07:00
Caleb Connolly
af9a0f0ca5 archive: create symlinks for /bin, /sbin, /lib (MR 39) 2024-05-29 15:29:35 -07:00
Clayton Craft
014563fdbc osutil: add function to detect merged /usr systems (MR 39)
This is very crude, but in lieu of an actual spec for usr merge or some
standard way to detect whether it's done, this will have to do for now.
We can and should improve it later!
2024-05-29 15:29:35 -07:00
Caleb Connolly
83282187c2 archive: split out and rework symlink handling (MR 39)
There were some subtle bugs when handling symlinks to directories, and
other usrmerge bits. Rework the symlink handling to deal with this
properly and produce a sane ramdisk on usrmerge systems.

Signed-off-by: Caleb Connolly <caleb@connolly.tech>
Co-authored-by: Clayton Craft <clayton@craftyguy.net>
2024-05-29 15:29:35 -07:00
Caleb Connolly
eda4f3ba22 filelist/modules: try /usr/lib/modules (MR 39)
This is the path used on usrmerge distros, try it first as /lib/modules
will implicitly follow the /lib symlink.

Signed-off-by: Caleb Connolly <caleb@connolly.tech>
2024-05-29 15:29:35 -07:00
Caleb Connolly
866d37b85d archive: improve error messages (MR 39)
Improve errors when writing out the cpio archive fails.

Signed-off-by: Caleb Connolly <caleb@connolly.tech>
2024-05-29 15:29:35 -07:00
Clayton Craft
1334fdfa26 deviceinfo: replace implementation with mvdan/sh (MR 52)
This library has a convenient "source file" method designed for sourcing
shell envs and returning values set in them. deviceinfo's syntax every
where else seems to be "whatever sh can 'source'", so using this library
seems a lot nicer than trying to implement a parser/interpreter here
(and almost certainly missing corner cases, functionality, etc)

[ci:skip-build]: already built successfully in CI
2024-05-15 16:11:17 -07:00
Clayton Craft
56db822b88 deviceinfo: implement stringer (MR 52) 2024-05-15 16:11:17 -07:00
Clayton Craft
631d6078c2 misc/getfiles: add systemd lib dir to search paths (MR 51)
fixes #41
2024-05-15 16:07:43 -07:00
Clayton Craft
e5f14d70a6 doc: add archive compression formats/levels (MR 50)
fixes #28
2024-03-20 10:08:18 -07:00
Stefan Hansson
dd5cdeace5 misc: Also check for .zst-compressed variants of files (MR 49)
Initially I thought of breaking off the Stat + error check call into its
own function as to reduce repetition, but given that it's only useful in
this situation where it only happens twice anyway, I'm not sure it
actually would reduce complexity.

Additionally, this means that .zst-compressed variants of files will be
searched for in all contexts where this function is used. I'm not sure
this is desirable.

Tested and works with arrow-db820c. I didn't test it on actual hardware,
but I verified that the firmware ended up in the initramfs via
$ pmbootstrap initfs ls. I choose this device because it uses firmware
from linux-firmware and also needs said firmware present in the
initramfs.

Closes https://gitlab.com/postmarketOS/postmarketos-mkinitfs/-/issues/39
2024-03-14 12:01:48 -07:00
Clayton Craft
1a99953aa2 bootdeploy: fallback to vmlinuz* kernels when zboot is set (MR 47)
Some kernel packages (e.g. linux-lts in Alpine) don't ship linux.efi, so
this needs to fallback to "vmlinuz" or else it won't be able to find a
kernel for boot-deploy.
2024-01-29 15:32:25 -08:00
Clayton Craft
e2f4e6254f modules: fix issue with some module extensions being ignored in dirs (MR 46)
There are several valid extensions that kernel modules can have, and the
list I had here was not complete... this meant that mkinitfs would fail
to include modules with extensions like ".ko.gz" when searching
directories.

This makes the check for a "valid" module file name a lot simpler,
allowing any file with ".ko" in the file name. While it's possible for a
non-module file to have ".ko" somewhere in the file name, it seems
unlikely if it's in the kernel modules directory... and this is an OK
compromise for now.
2024-01-29 10:16:28 -08:00
18 changed files with 379 additions and 168 deletions

View File

@@ -106,30 +106,6 @@ func main() {
modules.New("/usr/share/mkinitfs/modules"), modules.New("/usr/share/mkinitfs/modules"),
modules.New("/etc/mkinitfs/modules"), modules.New("/etc/mkinitfs/modules"),
}) })
if err := initramfsAr.AddItems(initfs); err != nil {
log.Println(err)
log.Println("failed to generate: ", "initramfs")
retCode = 1
return
}
if err := initramfsAr.Write(filepath.Join(workDir, "initramfs"), os.FileMode(0644)); err != nil {
log.Println(err)
log.Println("failed to generate: ", "initramfs")
retCode = 1
return
}
misc.TimeFunc(start, "initramfs")
//
// initramfs-extra
//
// deviceinfo.InitfsExtraCompression needs a little more post-processing
compressionFormat, compressionLevel = archive.ExtractFormatLevel(devinfo.InitfsExtraCompression)
log.Printf("== Generating %s ==\n", "initramfs-extra")
log.Printf("- Using compression format %s with level %q\n", compressionFormat, compressionLevel)
start = time.Now()
initramfsExtraAr := archive.New(compressionFormat, compressionLevel)
initfsExtra := initramfs.New([]filelist.FileLister{ initfsExtra := initramfs.New([]filelist.FileLister{
hookfiles.New("/usr/share/mkinitfs/files-extra"), hookfiles.New("/usr/share/mkinitfs/files-extra"),
hookfiles.New("/etc/mkinitfs/files-extra"), hookfiles.New("/etc/mkinitfs/files-extra"),
@@ -138,19 +114,58 @@ func main() {
modules.New("/usr/share/mkinitfs/modules-extra"), modules.New("/usr/share/mkinitfs/modules-extra"),
modules.New("/etc/mkinitfs/modules-extra"), modules.New("/etc/mkinitfs/modules-extra"),
}) })
if err := initramfsExtraAr.AddItemsExclude(initfsExtra, initfs); err != nil {
if err := initramfsAr.AddItems(initfs); err != nil {
log.Println(err) log.Println(err)
log.Println("failed to generate: ", "initramfs-extra") log.Println("failed to generate: ", "initramfs")
retCode = 1 retCode = 1
return return
} }
if err := initramfsExtraAr.Write(filepath.Join(workDir, "initramfs-extra"), os.FileMode(0644)); err != nil {
// Include initramfs-extra files in the initramfs if not making a separate
// archive
if !devinfo.CreateInitfsExtra {
if err := initramfsAr.AddItems(initfsExtra); err != nil {
log.Println(err)
log.Println("failed to generate: ", "initramfs")
retCode = 1
return
}
}
if err := initramfsAr.Write(filepath.Join(workDir, "initramfs"), os.FileMode(0644)); err != nil {
log.Println(err) log.Println(err)
log.Println("failed to generate: ", "initramfs-extra") log.Println("failed to generate: ", "initramfs")
retCode = 1 retCode = 1
return return
} }
misc.TimeFunc(start, "initramfs-extra") misc.TimeFunc(start, "initramfs")
if devinfo.CreateInitfsExtra {
//
// initramfs-extra
//
// deviceinfo.InitfsExtraCompression needs a little more post-processing
compressionFormat, compressionLevel = archive.ExtractFormatLevel(devinfo.InitfsExtraCompression)
log.Printf("== Generating %s ==\n", "initramfs-extra")
log.Printf("- Using compression format %s with level %q\n", compressionFormat, compressionLevel)
start = time.Now()
initramfsExtraAr := archive.New(compressionFormat, compressionLevel)
if err := initramfsExtraAr.AddItemsExclude(initfsExtra, initfs); err != nil {
log.Println(err)
log.Println("failed to generate: ", "initramfs-extra")
retCode = 1
return
}
if err := initramfsExtraAr.Write(filepath.Join(workDir, "initramfs-extra"), os.FileMode(0644)); err != nil {
log.Println(err)
log.Println("failed to generate: ", "initramfs-extra")
retCode = 1
return
}
misc.TimeFunc(start, "initramfs-extra")
}
// Final processing of initramfs / kernel is done by boot-deploy // Final processing of initramfs / kernel is done by boot-deploy
if !disableBootDeploy { if !disableBootDeploy {

View File

@@ -42,6 +42,7 @@ mkinitfs reads deviceinfo values from */usr/share/deviceinfo/deviceinfo* and
*/etc/deviceinfo*, in that order. The following variables */etc/deviceinfo*, in that order. The following variables
are *required* by mkinitfs: are *required* by mkinitfs:
- deviceinfo_create_initfs_extra
- deviceinfo_generate_systemd_boot - deviceinfo_generate_systemd_boot
- deviceinfo_initfs_compression - deviceinfo_initfs_compression
- deviceinfo_initfs_extra_compression - deviceinfo_initfs_extra_compression
@@ -54,6 +55,36 @@ a bare minimum, and to require only variables that don't hold lists of things.
necessary tools to extract the configured archive format are in the initramfs necessary tools to extract the configured archive format are in the initramfs
archive. archive.
# ARCHIVE COMPRESSION
Archive compression parameters are specified in the
*deviceinfo_initfs_compression* and *deviceinfo_initfs_extra_compression*
deviceinfo variables. Their values do not have to match, but special
consideration should be taken since some formats may require additional kernel
options or tools in the initramfs to support it.
Supported compression *formats* for mkinitfs are:
- gzip
- lz4
- lzma
- none
- zstd
Supported compression *levels* for mkinitfs:
- best
- default
- fast
The value of these variables follows this syntax: *<format>:<level>*. For
example, *zstd* with the *fast* compression level would be:
*deviceinfo_initfs_compression="zstd:fast"*
Defaults to *gzip* and *default* for both archives if format and/or level is
unsupported or omitted.
# DIRECTORIES # DIRECTORIES
The following directories are used by mkinitfs to generate the initramfs and The following directories are used by mkinitfs to generate the initramfs and
@@ -116,7 +147,7 @@ create/manage. mkinitfs reads configuration from */usr/share/mkinitfs* first, an
## /usr/share/mkinitfs/modules, /etc/mkinitfs/modules ## /usr/share/mkinitfs/modules, /etc/mkinitfs/modules
## /usr/share/mkinitfs/modules-extra, /etc/mkinitfs/modules-extra ## /usr/share/mkinitfs/modules-extra, /etc/mkinitfs/modules-extra
Files with the *.modules* extention in these directories are lists of Files with the *.modules* extension in these directories are lists of
kernel modules to include in the initramfs. Individual modules and kernel modules to include in the initramfs. Individual modules and
directories can be listed in the files here. Globbing is also supported. directories can be listed in the files here. Globbing is also supported.

10
go.mod
View File

@@ -7,5 +7,13 @@ require (
github.com/klauspost/compress v1.15.12 github.com/klauspost/compress v1.15.12
github.com/pierrec/lz4/v4 v4.1.17 github.com/pierrec/lz4/v4 v4.1.17
github.com/ulikunitz/xz v0.5.10 github.com/ulikunitz/xz v0.5.10
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c golang.org/x/sys v0.18.0
)
require (
github.com/mvdan/sh v2.6.4+incompatible // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/term v0.18.0 // indirect
mvdan.cc/sh v2.6.4+incompatible // indirect
) )

12
go.sum
View File

@@ -2,9 +2,21 @@ github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e h1:hHg27A0RS
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A= github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A=
github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM= github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM=
github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
github.com/mvdan/sh v2.6.4+incompatible h1:D4oEWW0J8cL7zeQkrXw76IAYXF0mJfDaBwjgzmKb6zs=
github.com/mvdan/sh v2.6.4+incompatible/go.mod h1:kipHzrJQZEDCMTNRVRAlMMFjqHEYrthfIlFkJSrmDZE=
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
mvdan.cc/sh v2.6.4+incompatible h1:eD6tDeh0pw+/TOTI1BBEryZ02rD2nMcFsgcvde7jffM=
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=

View File

@@ -237,7 +237,10 @@ func (archive *Archive) AddItemsExclude(flister filelist.FileLister, exclude fil
// Adds the given file or directory at "source" to the archive at "dest" // Adds the given file or directory at "source" to the archive at "dest"
func (archive *Archive) AddItem(source string, dest string) error { func (archive *Archive) AddItem(source string, dest string) error {
if osutil.HasMergedUsr() {
source = osutil.MergeUsr(source)
dest = osutil.MergeUsr(dest)
}
sourceStat, err := os.Lstat(source) sourceStat, err := os.Lstat(source)
if err != nil { if err != nil {
e, ok := err.(*os.PathError) e, ok := err.(*os.PathError)
@@ -248,6 +251,12 @@ func (archive *Archive) AddItem(source string, dest string) error {
return fmt.Errorf("AddItem: failed to get stat for %q: %w", source, err) return fmt.Errorf("AddItem: failed to get stat for %q: %w", source, err)
} }
// A symlink to a directory doesn't have the os.ModeDir bit set, so we need
// to check if it's a symlink first
if sourceStat.Mode()&os.ModeSymlink != 0 {
return archive.addSymlink(source, dest)
}
if sourceStat.Mode()&os.ModeDir != 0 { if sourceStat.Mode()&os.ModeDir != 0 {
return archive.addDir(dest) return archive.addDir(dest)
} }
@@ -255,6 +264,45 @@ func (archive *Archive) AddItem(source string, dest string) error {
return archive.addFile(source, dest) return archive.addFile(source, dest)
} }
func (archive *Archive) addSymlink(source string, dest string) error {
target, err := os.Readlink(source)
if err != nil {
log.Print("addSymlink: failed to get symlink target for: ", source)
return err
}
// Make sure we pick up the symlink target too
targetAbs := target
if filepath.Dir(target) == "." {
// relative symlink, make it absolute so we can add the target to the archive
targetAbs = filepath.Join(filepath.Dir(source), target)
}
if !filepath.IsAbs(targetAbs) {
targetAbs, err = osutil.RelativeSymlinkTargetToDir(targetAbs, filepath.Dir(source))
if err != nil {
return err
}
}
archive.AddItem(targetAbs, targetAbs)
// Now add the symlink itself
destFilename := strings.TrimPrefix(dest, "/")
archive.items.add(archiveItem{
sourcePath: source,
header: &cpio.Header{
Name: destFilename,
Linkname: target,
Mode: 0644 | cpio.ModeSymlink,
Size: int64(len(target)),
},
})
return nil
}
func (archive *Archive) addFile(source string, dest string) error { func (archive *Archive) addFile(source string, dest string) error {
if err := archive.addDir(filepath.Dir(dest)); err != nil { if err := archive.addDir(filepath.Dir(dest)); err != nil {
return err return err
@@ -266,42 +314,6 @@ func (archive *Archive) addFile(source string, dest string) error {
return err return err
} }
// Symlink: write symlink to archive then set 'file' to link target
if sourceStat.Mode()&os.ModeSymlink != 0 {
// log.Printf("File %q is a symlink", file)
target, err := os.Readlink(source)
if err != nil {
log.Print("addFile: failed to get symlink target: ", source)
return err
}
destFilename := strings.TrimPrefix(dest, "/")
archive.items.add(archiveItem{
sourcePath: source,
header: &cpio.Header{
Name: destFilename,
Linkname: target,
Mode: 0644 | cpio.ModeSymlink,
Size: int64(len(target)),
// Checksum: 1,
},
})
if filepath.Dir(target) == "." {
target = filepath.Join(filepath.Dir(source), target)
}
// make sure target is an absolute path
if !filepath.IsAbs(target) {
target, err = osutil.RelativeSymlinkTargetToDir(target, filepath.Dir(source))
if err != nil {
return err
}
}
err = archive.addFile(target, target)
return err
}
destFilename := strings.TrimPrefix(dest, "/") destFilename := strings.TrimPrefix(dest, "/")
archive.items.add(archiveItem{ archive.items.add(archiveItem{
@@ -404,6 +416,13 @@ func (archive *Archive) writeCompressed(path string, mode os.FileMode) (err erro
} }
func (archive *Archive) writeCpio() error { func (archive *Archive) writeCpio() error {
// Just in case
if osutil.HasMergedUsr() {
archive.addSymlink("/bin", "/bin")
archive.addSymlink("/sbin", "/sbin")
archive.addSymlink("/lib", "/lib")
archive.addSymlink("/usr/sbin", "/usr/sbin")
}
// having a transient function for actually adding files to the archive // having a transient function for actually adding files to the archive
// allows the deferred fd.close to run after every copy and prevent having // allows the deferred fd.close to run after every copy and prevent having
// tons of open file handles until the copying is all done // tons of open file handles until the copying is all done
@@ -418,19 +437,19 @@ func (archive *Archive) writeCpio() error {
if header.Mode.IsRegular() { if header.Mode.IsRegular() {
fd, err := os.Open(source) fd, err := os.Open(source)
if err != nil { if err != nil {
return fmt.Errorf("archive.writeCpio: uname to open file %q, %w", source, err) return fmt.Errorf("archive.writeCpio: Unable to open file %q, %w", source, err)
} }
defer fd.Close() defer fd.Close()
if _, err := io.Copy(archive.cpioWriter, fd); err != nil { if _, err := io.Copy(archive.cpioWriter, fd); err != nil {
return fmt.Errorf("archive.writeCpio: unable to write out archive: %w", err) return fmt.Errorf("archive.writeCpio: Couldn't process %q: %w", source, err)
} }
} else if header.Linkname != "" { } else if header.Linkname != "" {
// the contents of a symlink is just need the link name // the contents of a symlink is just need the link name
if _, err := archive.cpioWriter.Write([]byte(header.Linkname)); err != nil { if _, err := archive.cpioWriter.Write([]byte(header.Linkname)); err != nil {
return fmt.Errorf("archive.writeCpio: unable to write out symlink: %w", err) return fmt.Errorf("archive.writeCpio: unable to write out symlink: %q -> %q: %w", source, header.Linkname, err)
} }
} else { } else {
return fmt.Errorf("archive.writeCpio: unknown type for file: %s", source) return fmt.Errorf("archive.writeCpio: unknown type for file: %q: %d", source, header.Mode)
} }
} }

View File

@@ -78,12 +78,17 @@ func (b *BootDeploy) Run() error {
} }
// boot-deploy -i initramfs -k vmlinuz-postmarketos-rockchip -d /tmp/cpio -o /tmp/foo initramfs-extra // boot-deploy -i initramfs -k vmlinuz-postmarketos-rockchip -d /tmp/cpio -o /tmp/foo initramfs-extra
cmd := exec.Command("boot-deploy", args := []string{
"-i", "initramfs", "-i", "initramfs",
"-k", kernFilename, "-k", kernFilename,
"-d", b.inDir, "-d", b.inDir,
"-o", b.outDir, "-o", b.outDir,
"initramfs-extra") }
if b.devinfo.CreateInitfsExtra {
args = append(args, "initramfs-extra")
}
cmd := exec.Command("boot-deploy", args...)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
@@ -95,13 +100,16 @@ func (b *BootDeploy) Run() error {
} }
func getKernelPath(outDir string, zboot bool) ([]string, error) { func getKernelPath(outDir string, zboot bool) ([]string, error) {
kernFile := "vmlinuz*" var kernels []string
if zboot { if zboot {
kernFile = "linux.efi" kernels, _ = filepath.Glob(filepath.Join(outDir, "linux.efi"))
if len(kernels) > 0 {
return kernels, nil
}
// else fallback to vmlinuz* below
} }
var kernels []string kernFile := "vmlinuz*"
kernels, _ = filepath.Glob(filepath.Join(outDir, kernFile)) kernels, _ = filepath.Glob(filepath.Join(outDir, kernFile))
if len(kernels) == 0 { if len(kernels) == 0 {
return nil, errors.New("Unable to find any kernels at " + filepath.Join(outDir, kernFile)) return nil, errors.New("Unable to find any kernels at " + filepath.Join(outDir, kernFile))

View File

@@ -44,7 +44,7 @@ func (h *HookDirs) List() (*filelist.FileList, error) {
s := bufio.NewScanner(f) s := bufio.NewScanner(f)
for s.Scan() { for s.Scan() {
dir := s.Text() dir := strings.TrimSpace(s.Text())
if len(dir) == 0 || strings.HasPrefix(dir, "#") { if len(dir) == 0 || strings.HasPrefix(dir, "#") {
continue continue
} }

View File

@@ -11,6 +11,7 @@ import (
"gitlab.com/postmarketOS/postmarketos-mkinitfs/internal/filelist" "gitlab.com/postmarketOS/postmarketos-mkinitfs/internal/filelist"
"gitlab.com/postmarketOS/postmarketos-mkinitfs/internal/misc" "gitlab.com/postmarketOS/postmarketos-mkinitfs/internal/misc"
"gitlab.com/postmarketOS/postmarketos-mkinitfs/internal/osutil"
) )
type HookFiles struct { type HookFiles struct {
@@ -58,12 +59,15 @@ func slurpFiles(fd io.Reader) (*filelist.FileList, error) {
s := bufio.NewScanner(fd) s := bufio.NewScanner(fd)
for s.Scan() { for s.Scan() {
line := s.Text() line := strings.TrimSpace(s.Text())
if len(line) == 0 || strings.HasPrefix(line, "#") { if len(line) == 0 || strings.HasPrefix(line, "#") {
continue continue
} }
src, dest, has_dest := strings.Cut(line, ":") src, dest, has_dest := strings.Cut(line, ":")
if osutil.HasMergedUsr() {
src = osutil.MergeUsr(src)
}
fFiles, err := misc.GetFiles([]string{src}, true) fFiles, err := misc.GetFiles([]string{src}, true)
if err != nil { if err != nil {

View File

@@ -33,8 +33,14 @@ func (m *Modules) List() (*filelist.FileList, error) {
} }
files := filelist.NewFileList() files := filelist.NewFileList()
libDir := "/usr/lib/modules"
if exists, err := misc.Exists(libDir); !exists {
libDir = "/lib/modules"
} else if err != nil {
return nil, fmt.Errorf("received unexpected error when getting status for %q: %w", libDir, err)
}
modDir := filepath.Join("/lib/modules", kernVer) modDir := filepath.Join(libDir, kernVer)
if exists, err := misc.Exists(modDir); !exists { if exists, err := misc.Exists(modDir); !exists {
// dir /lib/modules/<kernel> if kernel built without module support, so just print a message // dir /lib/modules/<kernel> if kernel built without module support, so just print a message
log.Printf("-- kernel module directory not found: %q, not including modules", modDir) log.Printf("-- kernel module directory not found: %q, not including modules", modDir)
@@ -77,7 +83,7 @@ func slurpModules(fd io.Reader, modDir string) (*filelist.FileList, error) {
files := filelist.NewFileList() files := filelist.NewFileList()
s := bufio.NewScanner(fd) s := bufio.NewScanner(fd)
for s.Scan() { for s.Scan() {
line := s.Text() line := strings.TrimSpace(s.Text())
if len(line) == 0 || strings.HasPrefix(line, "#") { if len(line) == 0 || strings.HasPrefix(line, "#") {
continue continue
} }
@@ -97,8 +103,8 @@ func slurpModules(fd io.Reader, modDir string) (*filelist.FileList, error) {
} }
} else if dir == "" { } else if dir == "" {
// item is a module name // item is a module name
if modFilelist, err := getModule(s.Text(), modDir); err != nil { if modFilelist, err := getModule(line, modDir); err != nil {
return nil, fmt.Errorf("unable to get module file %q: %w", s.Text(), err) return nil, fmt.Errorf("unable to get module file %q: %w", line, err)
} else { } else {
for _, file := range modFilelist { for _, file := range modFilelist {
files.Add(file, file) files.Add(file, file)
@@ -118,7 +124,9 @@ func getModulesInDir(modPath string) (files []string, err error) {
// Unable to walk path // Unable to walk path
return err return err
} }
if filepath.Ext(path) != ".ko" && filepath.Ext(path) != ".xz" { // this assumes module names are in the format <name>.ko[.format],
// where ".format" (e.g. ".gz") is optional.
if !strings.Contains(".ko", path) {
return nil return nil
} }
files = append(files, path) files = append(files, path)
@@ -180,7 +188,7 @@ func getModuleDeps(modName string, modulesDep io.Reader) ([]string, error) {
s := bufio.NewScanner(modulesDep) s := bufio.NewScanner(modulesDep)
for s.Scan() { for s.Scan() {
line := s.Text() line := strings.TrimSpace(s.Text())
if len(line) == 0 || strings.HasPrefix(line, "#") { if len(line) == 0 || strings.HasPrefix(line, "#") {
continue continue
} }

View File

@@ -18,6 +18,7 @@ func TestStripExts(t *testing.T) {
{"another_file", "another_file"}, {"another_file", "another_file"},
{"a.b.c.d.e.f.g.h.i", "a"}, {"a.b.c.d.e.f.g.h.i", "a"},
{"virtio_blk.ko", "virtio_blk"}, {"virtio_blk.ko", "virtio_blk"},
{"virtio_blk.ko ", "virtio_blk"},
} }
for _, table := range tables { for _, table := range tables {
out := stripExts(table.in) out := stripExts(table.in)

View File

@@ -41,10 +41,22 @@ func getFile(file string, required bool) (files []string, err error) {
fileInfo, err := os.Stat(file) fileInfo, err := os.Stat(file)
if err != nil { if err != nil {
if required { // Check if there is a Zstd-compressed version of the file
return files, fmt.Errorf("getFile: failed to stat file %q: %w", file, err) fileZstd := file + ".zst" // .zst is the extension used by linux-firmware
fileInfoZstd, errZstd := os.Stat(fileZstd)
if errZstd == nil {
file = fileZstd
fileInfo = fileInfoZstd
// Unset nil so we don't retain the error from the os.Stat call for the uncompressed version.
err = nil
} else {
if required {
return files, fmt.Errorf("getFile: failed to stat file %q: %w (also tried %q: %w)", file, err, fileZstd, errZstd)
}
return files, nil
} }
return files, nil
} }
if fileInfo.IsDir() { if fileInfo.IsDir() {
@@ -108,6 +120,7 @@ func getDeps(file string, parents map[string]struct{}) (files []string, err erro
"/usr/lib", "/usr/lib",
"/lib", "/lib",
"/usr/lib/expect*", "/usr/lib/expect*",
"/usr/lib/systemd",
} }
for _, lib := range libs { for _, lib := range libs {

View File

@@ -10,6 +10,44 @@ import (
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
// Try to guess whether the system has merged dirs under /usr
func HasMergedUsr() bool {
for _, dir := range []string{"/bin", "/lib"} {
stat, err := os.Lstat(dir)
if err != nil {
// TODO: probably because the dir doesn't exist... so
// should we assume that it's because the system has some weird
// implementation of "merge /usr"?
return true
} else if stat.Mode()&os.ModeSymlink == 0 {
// Not a symlink, so must not be merged /usr
return false
}
}
return true
}
// Converts given path to one supported by a merged /usr config.
// E.g., /bin/foo becomes /usr/bin/foo, /lib/bar becomes /usr/lib/bar
// See: https://www.freedesktop.org/wiki/Software/systemd/TheCaseForTheUsrMerge
func MergeUsr(file string) string {
// Prepend /usr to supported paths
for _, prefix := range []string{"/bin", "/sbin", "/lib", "/lib64"} {
if strings.HasPrefix(file, prefix) {
file = filepath.Join("/usr", file)
break
}
}
// Convert /usr/sbin --> /usr/bin
if part, found := strings.CutPrefix(file, "/usr/sbin"); found {
file = filepath.Join("/usr/bin/", part)
}
return file
}
// Converts a relative symlink target path (e.g. ../../lib/foo.so), that is // Converts a relative symlink target path (e.g. ../../lib/foo.so), that is
// absolute path // absolute path
func RelativeSymlinkTargetToDir(symPath string, dir string) (string, error) { func RelativeSymlinkTargetToDir(symPath string, dir string) (string, error) {

View File

@@ -0,0 +1,49 @@
// Copyright 2024 Clayton Craft <clayton@craftyguy.net>
// SPDX-License-Identifier: GPL-3.0-or-later
package osutil
import (
"testing"
)
func TestMergeUsr(t *testing.T) {
subtests := []struct {
in string
expected string
}{
{
in: "/bin/foo",
expected: "/usr/bin/foo",
},
{
in: "/sbin/foo",
expected: "/usr/bin/foo",
},
{
in: "/usr/sbin/foo",
expected: "/usr/bin/foo",
},
{
in: "/usr/bin/foo",
expected: "/usr/bin/foo",
},
{
in: "/lib/foo.so",
expected: "/usr/lib/foo.so",
},
{
in: "/lib64/foo.so",
expected: "/usr/lib64/foo.so",
},
}
for _, st := range subtests {
t.Run(st.in, func(t *testing.T) {
out := MergeUsr(st.in)
if out != st.expected {
t.Fatalf("expected: %q, got: %q\n", st.expected, out)
}
})
}
}

View File

@@ -4,14 +4,14 @@
package deviceinfo package deviceinfo
import ( import (
"bufio" "context"
"fmt" "fmt"
"io"
"log"
"os"
"reflect" "reflect"
"strconv"
"strings" "strings"
"time"
"github.com/mvdan/sh/shell"
"gitlab.com/postmarketOS/postmarketos-mkinitfs/internal/misc" "gitlab.com/postmarketOS/postmarketos-mkinitfs/internal/misc"
) )
@@ -20,6 +20,8 @@ type DeviceInfo struct {
InitfsExtraCompression string InitfsExtraCompression string
UbootBoardname string UbootBoardname string
GenerateSystemdBoot string GenerateSystemdBoot string
FormatVersion string
CreateInitfsExtra bool
} }
// Reads the relevant entries from "file" into DeviceInfo struct // Reads the relevant entries from "file" into DeviceInfo struct
@@ -32,13 +34,7 @@ func (d *DeviceInfo) ReadDeviceinfo(file string) error {
return fmt.Errorf("unexpected error getting status for %q: %s", file, err) return fmt.Errorf("unexpected error getting status for %q: %s", file, err)
} }
fd, err := os.Open(file) if err := d.unmarshal(file); err != nil {
if err != nil {
return err
}
defer fd.Close()
if err := d.unmarshal(fd); err != nil {
return err return err
} }
@@ -46,53 +42,44 @@ func (d *DeviceInfo) ReadDeviceinfo(file string) error {
} }
// Unmarshals a deviceinfo into a DeviceInfo struct // Unmarshals a deviceinfo into a DeviceInfo struct
func (d *DeviceInfo) unmarshal(r io.Reader) error { func (d *DeviceInfo) unmarshal(file string) error {
s := bufio.NewScanner(r) ctx, cancelCtx := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
for s.Scan() { defer cancelCtx()
line := s.Text() vars, err := shell.SourceFile(ctx, file)
if strings.HasPrefix(line, "#") { if err != nil {
continue return fmt.Errorf("parsing deviceinfo %q failed: %w", file, err)
} }
// line isn't setting anything, so just ignore it
if !strings.Contains(line, "=") {
continue
}
// sometimes line has a comment at the end after setting an option
line = strings.SplitN(line, "#", 2)[0]
line = strings.TrimSpace(line)
// must support having '=' in the value (e.g. kernel cmdline)
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("error parsing deviceinfo line, invalid format: %s", line)
}
name, val := parts[0], parts[1]
val = strings.ReplaceAll(val, "\"", "")
if name == "deviceinfo_format_version" && val != "0" {
return fmt.Errorf("deviceinfo format version %q is not supported", val)
}
fieldName := nameToField(name)
if fieldName == "" {
return fmt.Errorf("error parsing deviceinfo line, invalid format: %s", line)
}
for k, v := range vars {
fieldName := nameToField(k)
field := reflect.ValueOf(d).Elem().FieldByName(fieldName) field := reflect.ValueOf(d).Elem().FieldByName(fieldName)
if !field.IsValid() { if !field.IsValid() {
// an option that meets the deviceinfo "specification", but isn't // an option that meets the deviceinfo "specification", but isn't
// one we care about in this module // one we care about in this module
continue continue
} }
field.SetString(val) switch field.Interface().(type) {
case string:
field.SetString(v.String())
case bool:
if v, err := strconv.ParseBool(v.String()); err != nil {
return fmt.Errorf("deviceinfo %q has unsupported type for field %q, expected 'bool'", file, k)
} else {
field.SetBool(v)
}
case int:
if v, err := strconv.ParseInt(v.String(), 10, 32); err != nil {
return fmt.Errorf("deviceinfo %q has unsupported type for field %q, expected 'int'", file, k)
} else {
field.SetInt(v)
}
default:
return fmt.Errorf("deviceinfo %q has unsupported type for field %q", file, k)
}
} }
if err := s.Err(); err != nil {
log.Print("unable to parse deviceinfo: ", err) if d.FormatVersion != "0" {
return err return fmt.Errorf("deviceinfo %q has an unsupported format version %q", file, d.FormatVersion)
} }
return nil return nil
@@ -116,3 +103,25 @@ func nameToField(name string) string {
return field return field
} }
func (d DeviceInfo) String() string {
return fmt.Sprintf(`{
%s: %v
%s: %v
%s: %v
%s: %v
%s: %v
%s: %v
%s: %v
%s: %v
}`,
"deviceinfo_format_version", d.FormatVersion,
"deviceinfo_", d.FormatVersion,
"deviceinfo_initfs_compression", d.InitfsCompression,
"deviceinfo_initfs_extra_compression", d.InitfsCompression,
"deviceinfo_ubootBoardname", d.UbootBoardname,
"deviceinfo_generateSystemdBoot", d.GenerateSystemdBoot,
"deviceinfo_formatVersion", d.FormatVersion,
"deviceinfo_createInitfsExtra", d.CreateInitfsExtra,
)
}

View File

@@ -4,8 +4,6 @@
package deviceinfo package deviceinfo
import ( import (
"fmt"
"reflect"
"strings" "strings"
"testing" "testing"
) )
@@ -44,6 +42,7 @@ func TestNameToField(t *testing.T) {
{"modules_initfs", "ModulesInitfs"}, {"modules_initfs", "ModulesInitfs"},
{"deviceinfo_initfs_compression___", "InitfsCompression"}, {"deviceinfo_initfs_compression___", "InitfsCompression"},
{"deviceinfo_initfs_extra_compression", "InitfsExtraCompression"}, {"deviceinfo_initfs_extra_compression", "InitfsExtraCompression"},
{"deviceinfo_create_initfs_extra", "CreateInitfsExtra"},
} }
for _, table := range tables { for _, table := range tables {
@@ -59,37 +58,25 @@ func TestUnmarshal(t *testing.T) {
tables := []struct { tables := []struct {
// field is just used for reflection within the test, so it must be a // field is just used for reflection within the test, so it must be a
// valid DeviceInfo field // valid DeviceInfo field
field string file string
in string expected DeviceInfo
expected string
}{ }{
{"InitfsCompression", "deviceinfo_initfs_compression=\"gzip:-9\"\n", "gzip:-9"}, {"./test_resources/deviceinfo-unmarshal-1", DeviceInfo{
// line with multiple '=' FormatVersion: "0",
{"InitfsCompression", "deviceinfo_initfs_compression=zstd:--foo=1 -T0 --bar=bazz", "zstd:--foo=1 -T0 --bar=bazz"}, UbootBoardname: "foobar-bazz",
// empty option InitfsCompression: "zstd:--foo=1 -T0 --bar=bazz",
{"InitfsCompression", "deviceinfo_initfs_compression=\"\"\n", ""}, InitfsExtraCompression: "",
// line with comment at the end CreateInitfsExtra: true,
{"", "# this is a comment!\n", ""}, },
// empty lines are fine },
{"", "", ""},
// line with whitepace characters only
{"", " \t \n\r", ""},
} }
var d DeviceInfo var d DeviceInfo
for _, table := range tables { for _, table := range tables {
testName := fmt.Sprintf("unmarshal::'%s':", strings.ReplaceAll(table.in, "\n", "\\n")) if err := d.unmarshal(table.file); err != nil {
if err := d.unmarshal(strings.NewReader(table.in)); err != nil { t.Error(err)
t.Errorf("%s received an unexpected err: ", err)
} }
if d != table.expected {
// Check against expected value t.Errorf("expected: %s, got: %s", table.expected, d)
field := reflect.ValueOf(&d).Elem().FieldByName(table.field)
out := ""
if table.field != "" {
out = field.String()
}
if out != table.expected {
t.Errorf("%s expected: %q, got: %q", testName, table.expected, out)
} }
} }

View File

@@ -1,2 +1,3 @@
deviceinfo_format_version="0"
deviceinfo_initfs_compression="gz -9" deviceinfo_initfs_compression="gz -9"
deviceinfo_mesa_driver="panfrost" deviceinfo_mesa_driver="panfrost"

View File

@@ -1 +1,2 @@
deviceinfo_mesa_driver="msm" deviceinfo_format_version="0"
deviceinfo_mesa_driver="msm"

View File

@@ -0,0 +1,7 @@
deviceinfo_format_version="0"
deviceinfo_uboot_boardname="foobar-bazz"
# line with multiple =
deviceinfo_initfs_compression="zstd:--foo=1 -T0 --bar=bazz"
# empty option
deviceinfo_initfs_extra_compression=""
deviceinfo_create_initfs_extra="true"