Skip to content

Commit

Permalink
bundle: Add WithoutRuntime write option (#1051)
Browse files Browse the repository at this point in the history
WithoutRuntime is a WriteOption that can be used to write the bundle without
using the runtime to determine the files to include in the bundle. Instead,
all files in the source FS will be included in the bundle.

This is useful when writing a bundle that is known not to contain any
unnecessary files, when loading and rewriting a bundle that was already
tree-shaken, or when loading the entire runtime is not possible for
performance or security reasons.
  • Loading branch information
rohansingh authored Apr 24, 2024
1 parent 0ad59b9 commit 9083434
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 93 deletions.
93 changes: 0 additions & 93 deletions bundle/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,16 @@
package bundle

import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"

"github.com/nlepage/go-tarfs"

"tidbyt.dev/pixlet/manifest"
"tidbyt.dev/pixlet/runtime"
)

const (
Expand Down Expand Up @@ -80,93 +77,3 @@ func LoadBundle(in io.Reader) (*AppBundle, error) {

return FromFS(fs)
}

// WriteBundleToPath is a helper to be able to write the bundle to a provided
// directory.
func (b *AppBundle) WriteBundleToPath(dir string) error {
path := filepath.Join(dir, AppBundleName)
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("could not create file for bundle: %w", err)
}
defer f.Close()

return b.WriteBundle(f)
}

// WriteBundle writes a compressed archive to the provided writer.
func (ab *AppBundle) WriteBundle(out io.Writer) error {
// we don't want to naively write the entire source FS to the tarball,
// since it could contain a lot of extraneous files. instead, run the
// applet and interrogate it for the files it needs to include in the
// bundle.
app, err := runtime.NewAppletFromFS(ab.Manifest.ID, ab.Source, runtime.WithPrintDisabled())
if err != nil {
return fmt.Errorf("loading applet for bundling: %w", err)
}
bundleFiles := app.PathsForBundle()

// Setup writers.
gzw := gzip.NewWriter(out)
defer gzw.Close()

tw := tar.NewWriter(gzw)
defer tw.Close()

// Write manifest.
buff := &bytes.Buffer{}
err = ab.Manifest.WriteManifest(buff)
if err != nil {
return fmt.Errorf("could not write manifest to buffer: %w", err)
}
b := buff.Bytes()

hdr := &tar.Header{
Name: manifest.ManifestFileName,
Mode: 0600,
Size: int64(len(b)),
}
err = tw.WriteHeader(hdr)
if err != nil {
return fmt.Errorf("could not write manifest header: %w", err)
}
_, err = tw.Write(b)
if err != nil {
return fmt.Errorf("could not write manifest to archive: %w", err)
}

// write sources.
for _, path := range bundleFiles {
stat, err := fs.Stat(ab.Source, path)
if err != nil {
return fmt.Errorf("could not stat %s: %w", path, err)
}

hdr, err := tar.FileInfoHeader(stat, "")
if err != nil {
return fmt.Errorf("creating header for %s: %w", path, err)
}
hdr.Name = filepath.ToSlash(path)

err = tw.WriteHeader(hdr)
if err != nil {
return fmt.Errorf("writing header for %s: %w", path, err)
}

if !stat.IsDir() {
file, err := ab.Source.Open(path)
if err != nil {
return fmt.Errorf("opening file %s: %w", path, err)
}

written, err := io.Copy(tw, file)
if err != nil {
return fmt.Errorf("writing file %s: %w", path, err)
} else if written != stat.Size() {
return fmt.Errorf("did not write entire file %s: %w", path, err)
}
}
}

return nil
}
39 changes: 39 additions & 0 deletions bundle/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,45 @@ func TestBundleWriteAndLoad(t *testing.T) {
_, err = newBun.Source.Open("unused.txt")
assert.ErrorIs(t, err, os.ErrNotExist)
}

func TestBundleWriteAndLoadWithoutRuntime(t *testing.T) {
ab, err := bundle.FromDir("testdata/testapp")
assert.NoError(t, err)
assert.Equal(t, "test-app", ab.Manifest.ID)
assert.NotNil(t, ab.Source)

// Create a temp directory.
dir, err := os.MkdirTemp("", "")
assert.NoError(t, err)

// Write bundle to the temp directory, without tree-shaking.
err = ab.WriteBundleToPath(dir, bundle.WithoutRuntime())
assert.NoError(t, err)

// Ensure we can load up the bundle just created.
path := filepath.Join(dir, bundle.AppBundleName)
f, err := os.Open(path)
assert.NoError(t, err)
defer f.Close()
newBun, err := bundle.LoadBundle(f)
assert.NoError(t, err)
assert.Equal(t, "test-app", newBun.Manifest.ID)
assert.NotNil(t, ab.Source)

// Ensure the loaded bundle contains the files we expect.
filesExpected := []string{
"manifest.yaml",
"test_app.star",
"test.txt",
"a_subdirectory/hi.jpg",
"unused.txt",
}
for _, file := range filesExpected {
_, err := newBun.Source.Open(file)
assert.NoError(t, err)
}
}

func TestLoadBundle(t *testing.T) {
f, err := os.Open("testdata/bundle.tar.gz")
assert.NoError(t, err)
Expand Down
142 changes: 142 additions & 0 deletions bundle/write.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Package bundle provides primitives for bundling apps for portability.
package bundle

import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"slices"

"tidbyt.dev/pixlet/manifest"
"tidbyt.dev/pixlet/runtime"
)

type WriteOption interface{}

type withoutRuntimeOption struct{}

// WithoutRuntime is a WriteOption that can be used to write the bundle without
// using the runtime to determine the files to include in the bundle. Instead,
// all files in the source FS will be included in the bundle.
//
// This is useful when writing a bundle that is known not to contain any
// unnecessary files, when loading and rewriting a bundle that was already
// tree-shaken, or when loading the entire runtime is not possible for
// performance or security reasons.
func WithoutRuntime() WriteOption {
return withoutRuntimeOption{}
}

// WriteBundleToPath is a helper to be able to write the bundle to a provided
// directory.
func (b *AppBundle) WriteBundleToPath(dir string, opts ...WriteOption) error {
path := filepath.Join(dir, AppBundleName)
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("could not create file for bundle: %w", err)
}
defer f.Close()

return b.WriteBundle(f, opts...)
}

// WriteBundle writes a compressed archive to the provided writer.
func (ab *AppBundle) WriteBundle(out io.Writer, opts ...WriteOption) error {
var bundleFiles []string

if slices.Contains(opts, WithoutRuntime()) {
// we can't use the runtime to determine the files to include in the
// bundle, so we'll just include everything in the source FS.
err := fs.WalkDir(ab.Source, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return fmt.Errorf("walking directory: %w", err)
}
if !d.IsDir() {
bundleFiles = append(bundleFiles, path)
}
return nil
})
if err != nil {
return fmt.Errorf("walking source FS: %w", err)
}
} else {
// we don't want to naively write the entire source FS to the tarball,
// since it could contain a lot of extraneous files. instead, run the
// applet and interrogate it for the files it needs to include in the
// bundle.
app, err := runtime.NewAppletFromFS(ab.Manifest.ID, ab.Source, runtime.WithPrintDisabled())
if err != nil {
return fmt.Errorf("loading applet for bundling: %w", err)
}
bundleFiles = app.PathsForBundle()
}

// Setup writers.
gzw := gzip.NewWriter(out)
defer gzw.Close()

tw := tar.NewWriter(gzw)
defer tw.Close()

// Write manifest.
buff := &bytes.Buffer{}
err := ab.Manifest.WriteManifest(buff)
if err != nil {
return fmt.Errorf("could not write manifest to buffer: %w", err)
}
b := buff.Bytes()

hdr := &tar.Header{
Name: manifest.ManifestFileName,
Mode: 0600,
Size: int64(len(b)),
}
err = tw.WriteHeader(hdr)
if err != nil {
return fmt.Errorf("could not write manifest header: %w", err)
}
_, err = tw.Write(b)
if err != nil {
return fmt.Errorf("could not write manifest to archive: %w", err)
}

// write sources.
for _, path := range bundleFiles {
stat, err := fs.Stat(ab.Source, path)
if err != nil {
return fmt.Errorf("could not stat %s: %w", path, err)
}

hdr, err := tar.FileInfoHeader(stat, "")
if err != nil {
return fmt.Errorf("creating header for %s: %w", path, err)
}
hdr.Name = filepath.ToSlash(path)

err = tw.WriteHeader(hdr)
if err != nil {
return fmt.Errorf("writing header for %s: %w", path, err)
}

if !stat.IsDir() {
file, err := ab.Source.Open(path)
if err != nil {
return fmt.Errorf("opening file %s: %w", path, err)
}

written, err := io.Copy(tw, file)
if err != nil {
return fmt.Errorf("writing file %s: %w", path, err)
} else if written != stat.Size() {
return fmt.Errorf("did not write entire file %s: %w", path, err)
}
}
}

return nil
}

0 comments on commit 9083434

Please sign in to comment.