Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
60320ba
feat: search, load and validates manifests
upils Nov 26, 2025
c67c661
fix: revert unintentional change
upils Nov 26, 2025
22ae256
refactor: move things around
upils Nov 26, 2025
6cb437f
refactor: move logic to manifestutil
upils Nov 26, 2025
00825de
feat: validate rootfs with manifest
upils Nov 27, 2025
442bcc6
fix: debugging
upils Nov 27, 2025
e19f6f1
fix: improve hardlinks check
upils Nov 27, 2025
b6ecc27
feat: trying alternative approach
upils Nov 28, 2025
1406b18
feat: refine first approach
upils Dec 1, 2025
8aba035
refactor: dedicated verify.go
upils Dec 1, 2025
70b2c5e
refactor: rework verify* funcs
upils Dec 2, 2025
c8b7a39
fix: typos and inconsistencies
upils Dec 2, 2025
1181871
refactor: improve readability
upils Dec 2, 2025
31f1d3d
refactor: improve consistency
upils Dec 3, 2025
df49dfc
style: respect internal best practices
upils Dec 3, 2025
f4169b9
style: minor consistency fixes
upils Dec 4, 2025
aade454
fix: ignore size check on manifest
upils Dec 5, 2025
e0bd4a4
style: apply suggestions
upils Dec 8, 2025
2e572fb
style: apply suggestions
upils Dec 8, 2025
3c5de1c
style: apply suggestions
upils Dec 8, 2025
7eff973
style: apply suggestions
upils Dec 8, 2025
b7c7c66
style: apply suggestions
upils Dec 8, 2025
c7196e9
fix: apply suggestions
upils Dec 16, 2025
7539db7
refactor: simplify
upils Dec 19, 2025
cd22c1f
feat: intermediary representation
upils Jan 5, 2026
27d8645
refactor: simplify existing dir checking
upils Jan 5, 2026
373f3c1
refactor: regroup hardlink-related code
upils Jan 5, 2026
1414990
refactor: refine manifest checking API
upils Jan 5, 2026
2ce5b21
refactor: add obtainManifest
upils Jan 6, 2026
17162f4
refactor: simplify by removing SliceKeys function
upils Jan 6, 2026
c21ef34
style: remove redondant comment
upils Jan 13, 2026
77f5771
refactor: rework pathInfos comparison
upils Jan 14, 2026
40ef8e8
refactor: simplify FindPathsInRelease
upils Jan 14, 2026
0a25844
refactor: refine API
upils Jan 14, 2026
71c4496
refactor: simplify based on PR suggestions
upils Jan 15, 2026
4696dcc
style: tweak error messages
upils Jan 15, 2026
089109d
fix: fail if dir inconsistent with manifest
upils Jan 16, 2026
09faa6a
fix: use loaded manifest hash as reference
upils Jan 16, 2026
bd7a707
fix: raise error when path should not be hardlinked
upils Jan 19, 2026
3ea70ce
fix: collect and record reference manifest size
upils Jan 19, 2026
3efb7d2
feat: log root directory processing
upils Jan 19, 2026
a421818
refactor: more experiment around API design
upils Jan 21, 2026
e88de2d
refactor: experiment to improve API
upils Jan 21, 2026
acac421
refactor: more refining
upils Jan 22, 2026
0262cb2
refactor: CheckDir in slicer
upils Jan 22, 2026
7445fcd
refactor: add check in slicer
upils Jan 22, 2026
5fca0c5
refactor: simplify and reduce slicer API
upils Jan 22, 2026
fb6500b
ci: rerun
upils Jan 22, 2026
d7fa155
wip
upils Jan 23, 2026
43e8a71
refactor: cleaning
upils Jan 23, 2026
46d2119
fix: check manifest consistency at selection
upils Jan 23, 2026
164c68b
refactor: return a manifest from inspection
upils Jan 26, 2026
15550ad
refactor: apply PR suggestions
upils Jan 26, 2026
4ecb555
refactor: avoid iterating twice on slices
upils Jan 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions cmd/chisel/cmd_cut.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package main

import (
"fmt"
"os"
"path/filepath"
"slices"
"time"

"github.com/jessevdk/go-flags"

"github.com/canonical/chisel/internal/archive"
"github.com/canonical/chisel/internal/cache"
"github.com/canonical/chisel/internal/manifestutil"
"github.com/canonical/chisel/internal/setup"
"github.com/canonical/chisel/internal/slicer"
)
Expand Down Expand Up @@ -73,6 +76,29 @@ func (cmd *cmdCut) Execute(args []string) error {
}
}

rootfsPath, err := preExistingRootfs(cmd.RootDir)
if err != nil {
return err
}
Comment thread
upils marked this conversation as resolved.

if len(rootfsPath) > 0 {
manifest, err := manifestutil.RootFSManifest(release, rootfsPath)
if err != nil {
// TODO: When enabling the feature, error out.
logf("Warning: %v", err)
Comment thread
upils marked this conversation as resolved.
Outdated
} else {
// Merge the slice keys used to build the existing rootfs with the ones
// explicitly requested slices.
// Previous slice keys contains both explicitly requested slices and slices
Comment thread
upils marked this conversation as resolved.
Outdated
// resolved following essentials.
for _, s := range manifestutil.SliceKeys(manifest) {
Comment thread
upils marked this conversation as resolved.
Outdated
if !slices.Contains(sliceKeys, s) {
sliceKeys = append(sliceKeys, s)
}
}
}
}

selection, err := setup.Select(release, sliceKeys, cmd.Arch)
if err != nil {
return err
Expand Down Expand Up @@ -128,3 +154,28 @@ func (cmd *cmdCut) Execute(args []string) error {
})
return err
}

// preExistingRootfs determines if the target directory was produced by chisel.
Comment thread
upils marked this conversation as resolved.
Outdated
func preExistingRootfs(rootdir string) (string, error) {
// Get targetDir path
// Note: This is already done in `slicer.Run`. Extract it and avoid doing it twice?
targetDir := filepath.Clean(rootdir)
if !filepath.IsAbs(targetDir) {
dir, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("cannot obtain current directory: %w", err)
}
targetDir = filepath.Join(dir, targetDir)
}

entries, err := os.ReadDir(targetDir)
if err != nil {
return "", fmt.Errorf("cannot read root directory %q: %v", targetDir, err)
}

if len(entries) == 0 {
return "", nil
Comment thread
upils marked this conversation as resolved.
Outdated
}

return targetDir, nil
}
231 changes: 231 additions & 0 deletions internal/manifestutil/manifestutil.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
package manifestutil

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"slices"
"sort"
"strings"
"syscall"

"github.com/canonical/chisel/internal/apacheutil"
"github.com/canonical/chisel/internal/archive"
"github.com/canonical/chisel/internal/setup"
"github.com/canonical/chisel/public/jsonwall"
"github.com/canonical/chisel/public/manifest"
"github.com/klauspost/compress/zstd"
)

const DefaultFilename = "manifest.wall"
Expand All @@ -34,6 +40,24 @@ func FindPaths(slices []*setup.Slice) map[string][]*setup.Slice {
return manifestSlices
}

// FindPathsInRelease finds all the paths marked with "generate:manifest"
// for the given release.
func FindPathsInRelease(r *setup.Release) []string {
manifests := []string{}
for _, pkg := range r.Packages {
for _, slice := range pkg.Slices {
for path, info := range slice.Contents {
if info.Generate == setup.GenerateManifest {
dir := strings.TrimSuffix(path, "**")
path = filepath.Join(dir, DefaultFilename)
manifests = append(manifests, path)
}
}
}
}
return manifests
}

type WriteOptions struct {
PackageInfo []*archive.PackageInfo
Selection []*setup.Slice
Expand Down Expand Up @@ -340,3 +364,210 @@ func Validate(mfest *manifest.Manifest) (err error) {
}
return nil
}

// RootFSManifest extracts the manifest from a targetDir
func RootFSManifest(release *setup.Release, targetDir string) (*manifest.Manifest, error) {
manifestPaths := FindPathsInRelease(release)
if len(manifestPaths) == 0 {
// No manifest in the release means it cannot produce a rootfs that can
// be recut. Treat this case as cutting a new rootfs.
return nil, nil
}

// Select the first manifest of the list as the reference one for now.
// Another heuristic could be used (ex. select the one from base-files_chisel).
refManifestRelPath := manifestPaths[0]
refManifestPath := path.Join(targetDir, refManifestRelPath)
refManifest, err := load(refManifestPath)
if err != nil {
return nil, fmt.Errorf("cannot read manifest %q from the root directory: %v", refManifestRelPath, err)
}

err = checkConsistency(refManifestRelPath, targetDir, manifestPaths[1:])
if err != nil {
return nil, err
}

err = validateRootfs(refManifest, targetDir)
if err != nil {
return nil, err
}
return refManifest, nil
}

// load reads, validates and returns a manifest.
func load(manifestPath string) (*manifest.Manifest, error) {
Comment thread
upils marked this conversation as resolved.
Outdated
f, err := os.Open(manifestPath)
if err != nil {
return nil, err
}
defer f.Close()

r, err := zstd.NewReader(f)
if err != nil {
return nil, err
}
defer r.Close()

mfest, err := manifest.Read(r)
if err != nil {
return nil, err
}

err = Validate(mfest)
if err != nil {
return nil, err
}

return mfest, nil
}

// checkConsistency checks consistency between a list of manifests and a
// reference one.
func checkConsistency(referenceRelPath string, targetDir string, manifests []string) error {
Comment thread
upils marked this conversation as resolved.
Outdated
reference := path.Join(targetDir, referenceRelPath)
hashReference, err := hash(reference)
if err != nil {
return err
}

infoRef, err := os.Stat(reference)
Comment thread
upils marked this conversation as resolved.
Outdated
if err != nil {
return err
}

modeRef := infoRef.Mode()

for _, manifestRelPath := range manifests {
manifestPath := path.Join(targetDir, manifestRelPath)
infoManifest, err := os.Stat(manifestPath)
if err != nil {
return err
}

modeManifest := infoManifest.Mode()
Comment thread
upils marked this conversation as resolved.
Outdated
if modeManifest != modeRef {
return fmt.Errorf("invalid manifest: permissions on %s (%s) are different from the reference manifest %s (%s)", manifestRelPath, modeManifest, referenceRelPath, modeRef)
Comment thread
upils marked this conversation as resolved.
Outdated
}

hashM, err := hash(manifestPath)
Comment thread
upils marked this conversation as resolved.
Outdated
if err != nil {
return err
}
if !slices.Equal(hashM, hashReference) {
return fmt.Errorf("invalid manifest: %s is inconsistent with %s", manifestRelPath, referenceRelPath)
}
}

return nil
}

func hash(path string) ([]byte, error) {
Comment thread
upils marked this conversation as resolved.
Outdated
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()

h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return nil, err
}

return h.Sum(nil), nil
}

// SliceKeys returns setup.SliceKeys from a manifest.
func SliceKeys(m *manifest.Manifest) []setup.SliceKey {
sliceKeys := []setup.SliceKey{}
m.IterateSlices("", func(slice *manifest.Slice) error {
sk, err := apacheutil.ParseSliceKey(slice.Name)
if err != nil {
return err
}
sliceKeys = append(sliceKeys, sk)
return nil
})

return sliceKeys
}

// reportFromRootfs builds a Report from root directory
// Implementation heavily borrowed from testutil.TreeDump
func reportFromRootfs(rootDir string) (*Report, error) {
report, err := NewReport(rootDir)
if err != nil {
return nil, fmt.Errorf("internal error: cannot create report: %w", err)
}

var inodes []uint64
pathsByInodes := make(map[uint64][]string)

dirfs := os.DirFS(report.Root)
err = fs.WalkDir(dirfs, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return fmt.Errorf("walk error: %w", err)
}
if path == "." {
return nil
}
fpath := filepath.Join(report.Root, path)
finfo, err := d.Info()
if err != nil {
return fmt.Errorf("cannot get stat info for %q: %w", fpath, err)
}

entry := ReportEntry{
Mode: finfo.Mode(),
}

var size int

ftype := finfo.Mode() & fs.ModeType
switch ftype {
case fs.ModeDir:
path = "/" + path + "/"
case fs.ModeSymlink:
lpath, err := os.Readlink(fpath)
if err != nil {
return err
}
path = "/" + path
entry.Link = lpath
case 0: // Regular
data, err := os.ReadFile(fpath)
if err != nil {
return fmt.Errorf("cannot read file: %w", err)
}
if len(data) >= 0 {
sum := sha256.Sum256(data)
entry.SHA256 = hex.EncodeToString(sum[:])
}
Comment thread
upils marked this conversation as resolved.
Outdated
Comment thread
upils marked this conversation as resolved.
Outdated
path = "/" + path
size = int(finfo.Size())
default:
return fmt.Errorf("unknown file type %d: %s", ftype, fpath)
}
entry.Path = path
entry.Size = size

if ftype != fs.ModeDir {
stat, ok := finfo.Sys().(*syscall.Stat_t)
if !ok {
return fmt.Errorf("cannot get syscall stat info for %q", fpath)
}
inode := stat.Ino
if len(pathsByInodes[inode]) == 1 {
inodes = append(inodes, inode)
}
entry.Inode = inode
pathsByInodes[inode] = append(pathsByInodes[inode], path)
}

report.Entries[path] = entry

return nil
})

return report, nil
}
Loading