Skip to content

Commit c44547e

Browse files
Merge branch 'main' into cookiejar
2 parents 5bb73b5 + 0ad59b9 commit c44547e

File tree

99 files changed

+1410
-1515
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

99 files changed

+1410
-1515
lines changed

.github/workflows/main.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- name: Set up Go
1717
uses: actions/setup-go@v4
1818
with:
19-
go-version: "1.21"
19+
go-version: "1.22.2"
2020

2121
- name: Install buildifier
2222
run: make install-buildifier
@@ -36,7 +36,7 @@ jobs:
3636
- name: Install Go
3737
uses: actions/setup-go@v4
3838
with:
39-
go-version: "1.21"
39+
go-version: "1.22.2"
4040

4141
- name: Install Node
4242
uses: actions/setup-node@v3
@@ -158,7 +158,7 @@ jobs:
158158
- name: Install Go
159159
uses: actions/setup-go@v4
160160
with:
161-
go-version: "1.21"
161+
go-version: "1.22.2"
162162

163163
- name: Fetch Release Artifacts
164164
uses: actions/download-artifact@v3

.github/workflows/pull-request.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
- name: Set up Go
1515
uses: actions/setup-go@v4
1616
with:
17-
go-version: "1.21"
17+
go-version: "1.22.2"
1818

1919
- name: Install buildifier
2020
run: make install-buildifier
@@ -34,7 +34,7 @@ jobs:
3434
- name: Install Go
3535
uses: actions/setup-go@v4
3636
with:
37-
go-version: "1.21"
37+
go-version: "1.22.2"
3838

3939
- name: Install Node
4040
uses: actions/setup-node@v3

.gitignore

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
.DS_Store
33

44
# Rendered Apps
5-
examples/*.webp
6-
examples/*.gif
5+
examples/**/*.webp
6+
examples/**/*.gif
77

88
# Pixlet Binary
99
pixlet

README.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def main():
6060
Render and serve it with:
6161

6262
```console
63-
curl https://raw.githubusercontent.com/tidbyt/pixlet/main/examples/hello_world.star | \
63+
curl https://raw.githubusercontent.com/tidbyt/pixlet/main/examples/hello_world/hello_world.star | \
6464
pixlet serve /dev/stdin
6565
```
6666

@@ -135,7 +135,7 @@ to show the Bitcoin tracker on your Tidbyt:
135135

136136
```console
137137
# render the bitcoin example
138-
pixlet render examples/bitcoin.star
138+
pixlet render examples/bitcoin/bitcoin.star
139139

140140
# login to your Tidbyt account
141141
pixlet login
@@ -144,7 +144,7 @@ pixlet login
144144
pixlet devices
145145

146146
# push to your favorite Tidbyt
147-
pixlet push <YOUR DEVICE ID> examples/bitcoin.webp
147+
pixlet push <YOUR DEVICE ID> examples/bitcoin/bitcoin.webp
148148
```
149149

150150
To get the ID for a device, run `pixlet devices`. Alternatively, you can
@@ -158,8 +158,8 @@ If all goes well, you should see the Bitcoin tracker appear on your Tidbyt:
158158
Pushing an applet to your Tidbyt without an installation ID simply displays your applet one time. If you would like your applet to continously display as part of the rotation, add an installation ID to the push command:
159159

160160
```console
161-
pixlet render examples/bitcoin.star
162-
pixlet push --installation-id <INSTALLATION ID> <YOUR DEVICE ID> examples/bitcoin.webp
161+
pixlet render examples/bitcoin/bitcoin.star
162+
pixlet push --installation-id <INSTALLATION ID> <YOUR DEVICE ID> examples/bitcoin/bitcoin.webp
163163
```
164164

165165
For example, if we set the `installationID` to "Bitcoin", it would appear in the mobile app as follows:

bundle/bundle.go

+67-80
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ import (
77
"compress/gzip"
88
"fmt"
99
"io"
10+
"io/fs"
1011
"os"
1112
"path/filepath"
1213

14+
"github.com/nlepage/go-tarfs"
15+
1316
"tidbyt.dev/pixlet/manifest"
17+
"tidbyt.dev/pixlet/runtime"
1418
)
1519

1620
const (
@@ -26,16 +30,12 @@ const (
2630

2731
// AppBundle represents the unpacked bundle in our system.
2832
type AppBundle struct {
29-
Source []byte
3033
Manifest *manifest.Manifest
34+
Source fs.FS
3135
}
3236

33-
// InitFromPath translates a directory containing an app manifest and source
34-
// into an AppBundle.
35-
func InitFromPath(dir string) (*AppBundle, error) {
36-
// Load manifest
37-
path := filepath.Join(dir, manifest.ManifestFileName)
38-
m, err := os.Open(path)
37+
func FromFS(fs fs.FS) (*AppBundle, error) {
38+
m, err := fs.Open(manifest.ManifestFileName)
3939
if err != nil {
4040
return nil, fmt.Errorf("could not open manifest: %w", err)
4141
}
@@ -46,80 +46,39 @@ func InitFromPath(dir string) (*AppBundle, error) {
4646
return nil, fmt.Errorf("could not load manifest: %w", err)
4747
}
4848

49-
// Load source
50-
path = filepath.Join(dir, man.FileName)
51-
s, err := os.Open(path)
52-
if err != nil {
53-
return nil, fmt.Errorf("could not open app source: %w", err)
54-
}
55-
defer s.Close()
56-
57-
src, err := io.ReadAll(s)
58-
if err != nil {
59-
return nil, fmt.Errorf("could not read app source: %w", err)
60-
}
61-
6249
// Create app bundle struct
6350
return &AppBundle{
6451
Manifest: man,
65-
Source: src,
52+
Source: fs,
6653
}, nil
6754
}
6855

56+
// FromDir translates a directory containing an app manifest and source
57+
// into an AppBundle.
58+
func FromDir(dir string) (*AppBundle, error) {
59+
return FromFS(os.DirFS(dir))
60+
}
61+
6962
// LoadBundle loads a compressed archive into an AppBundle.
7063
func LoadBundle(in io.Reader) (*AppBundle, error) {
7164
gzr, err := gzip.NewReader(in)
7265
if err != nil {
73-
return nil, fmt.Errorf("could not create gzip reader: %w", err)
66+
return nil, fmt.Errorf("creating gzip reader: %w", err)
7467
}
7568
defer gzr.Close()
7669

77-
tr := tar.NewReader(gzr)
78-
ab := &AppBundle{}
79-
80-
for {
81-
header, err := tr.Next()
70+
// read the entire tarball into memory so that we can seek
71+
// around it, and so that the underlying reader can be closed.
72+
var b bytes.Buffer
73+
io.Copy(&b, gzr)
8274

83-
switch {
84-
case err == io.EOF:
85-
// If there are no more files in the bundle, validate and return it.
86-
if ab.Manifest == nil {
87-
return nil, fmt.Errorf("could not find manifest in archive")
88-
}
89-
if ab.Source == nil {
90-
return nil, fmt.Errorf("could not find source in archive")
91-
}
92-
return ab, nil
93-
case err != nil:
94-
// If there is an error, return immediately.
95-
return nil, fmt.Errorf("could not read archive: %w", err)
96-
case header == nil:
97-
// If for some reason we end up with a blank header, continue to the
98-
// next one.
99-
continue
100-
case header.Name == AppSourceName:
101-
// Load the app source.
102-
buff := make([]byte, header.Size)
103-
_, err := io.ReadFull(tr, buff)
104-
if err != nil {
105-
return nil, fmt.Errorf("could not read source from archive: %w", err)
106-
}
107-
ab.Source = buff
108-
case header.Name == manifest.ManifestFileName:
109-
// Load the app manifest.
110-
buff := make([]byte, header.Size)
111-
_, err := io.ReadFull(tr, buff)
112-
if err != nil {
113-
return nil, fmt.Errorf("could not read manifest from archive: %w", err)
114-
}
115-
116-
man, err := manifest.LoadManifest(bytes.NewReader(buff))
117-
if err != nil {
118-
return nil, fmt.Errorf("could not load manifest: %w", err)
119-
}
120-
ab.Manifest = man
121-
}
75+
r := bytes.NewReader(b.Bytes())
76+
fs, err := tarfs.New(r)
77+
if err != nil {
78+
return nil, fmt.Errorf("creating tarfs: %w", err)
12279
}
80+
81+
return FromFS(fs)
12382
}
12483

12584
// WriteBundleToPath is a helper to be able to write the bundle to a provided
@@ -137,6 +96,16 @@ func (b *AppBundle) WriteBundleToPath(dir string) error {
13796

13897
// WriteBundle writes a compressed archive to the provided writer.
13998
func (ab *AppBundle) WriteBundle(out io.Writer) error {
99+
// we don't want to naively write the entire source FS to the tarball,
100+
// since it could contain a lot of extraneous files. instead, run the
101+
// applet and interrogate it for the files it needs to include in the
102+
// bundle.
103+
app, err := runtime.NewAppletFromFS(ab.Manifest.ID, ab.Source, runtime.WithPrintDisabled())
104+
if err != nil {
105+
return fmt.Errorf("loading applet for bundling: %w", err)
106+
}
107+
bundleFiles := app.PathsForBundle()
108+
140109
// Setup writers.
141110
gzw := gzip.NewWriter(out)
142111
defer gzw.Close()
@@ -146,7 +115,7 @@ func (ab *AppBundle) WriteBundle(out io.Writer) error {
146115

147116
// Write manifest.
148117
buff := &bytes.Buffer{}
149-
err := ab.Manifest.WriteManifest(buff)
118+
err = ab.Manifest.WriteManifest(buff)
150119
if err != nil {
151120
return fmt.Errorf("could not write manifest to buffer: %w", err)
152121
}
@@ -166,19 +135,37 @@ func (ab *AppBundle) WriteBundle(out io.Writer) error {
166135
return fmt.Errorf("could not write manifest to archive: %w", err)
167136
}
168137

169-
// Write source.
170-
hdr = &tar.Header{
171-
Name: AppSourceName,
172-
Mode: 0600,
173-
Size: int64(len(ab.Source)),
174-
}
175-
err = tw.WriteHeader(hdr)
176-
if err != nil {
177-
return fmt.Errorf("could not write source header: %w", err)
178-
}
179-
_, err = tw.Write(ab.Source)
180-
if err != nil {
181-
return fmt.Errorf("could not write source to archive: %w", err)
138+
// write sources.
139+
for _, path := range bundleFiles {
140+
stat, err := fs.Stat(ab.Source, path)
141+
if err != nil {
142+
return fmt.Errorf("could not stat %s: %w", path, err)
143+
}
144+
145+
hdr, err := tar.FileInfoHeader(stat, "")
146+
if err != nil {
147+
return fmt.Errorf("creating header for %s: %w", path, err)
148+
}
149+
hdr.Name = filepath.ToSlash(path)
150+
151+
err = tw.WriteHeader(hdr)
152+
if err != nil {
153+
return fmt.Errorf("writing header for %s: %w", path, err)
154+
}
155+
156+
if !stat.IsDir() {
157+
file, err := ab.Source.Open(path)
158+
if err != nil {
159+
return fmt.Errorf("opening file %s: %w", path, err)
160+
}
161+
162+
written, err := io.Copy(tw, file)
163+
if err != nil {
164+
return fmt.Errorf("writing file %s: %w", path, err)
165+
} else if written != stat.Size() {
166+
return fmt.Errorf("did not write entire file %s: %w", path, err)
167+
}
168+
}
182169
}
183170

184171
return nil

bundle/bundle_test.go

+21-5
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import (
1111

1212
func TestBundleWriteAndLoad(t *testing.T) {
1313
// Ensure we can load the bundle from an app.
14-
ab, err := bundle.InitFromPath("testdata/testapp")
14+
ab, err := bundle.FromDir("testdata/testapp")
1515
assert.NoError(t, err)
1616
assert.Equal(t, "test-app", ab.Manifest.ID)
17-
assert.True(t, len(ab.Source) > 0)
17+
assert.NotNil(t, ab.Source)
1818

1919
// Create a temp directory.
2020
dir, err := os.MkdirTemp("", "")
@@ -32,7 +32,23 @@ func TestBundleWriteAndLoad(t *testing.T) {
3232
newBun, err := bundle.LoadBundle(f)
3333
assert.NoError(t, err)
3434
assert.Equal(t, "test-app", newBun.Manifest.ID)
35-
assert.True(t, len(ab.Source) > 0)
35+
assert.NotNil(t, ab.Source)
36+
37+
// Ensure the loaded bundle contains the files we expect.
38+
filesExpected := []string{
39+
"manifest.yaml",
40+
"test_app.star",
41+
"test.txt",
42+
"a_subdirectory/hi.jpg",
43+
}
44+
for _, file := range filesExpected {
45+
_, err := newBun.Source.Open(file)
46+
assert.NoError(t, err)
47+
}
48+
49+
// Ensure the loaded bundle does not contain any extra files.
50+
_, err = newBun.Source.Open("unused.txt")
51+
assert.ErrorIs(t, err, os.ErrNotExist)
3652
}
3753
func TestLoadBundle(t *testing.T) {
3854
f, err := os.Open("testdata/bundle.tar.gz")
@@ -41,7 +57,7 @@ func TestLoadBundle(t *testing.T) {
4157
ab, err := bundle.LoadBundle(f)
4258
assert.NoError(t, err)
4359
assert.Equal(t, "test-app", ab.Manifest.ID)
44-
assert.True(t, len(ab.Source) > 0)
60+
assert.NotNil(t, ab.Source)
4561
}
4662
func TestLoadBundleExcessData(t *testing.T) {
4763
f, err := os.Open("testdata/excess-files.tar.gz")
@@ -51,5 +67,5 @@ func TestLoadBundleExcessData(t *testing.T) {
5167
ab, err := bundle.LoadBundle(f)
5268
assert.NoError(t, err)
5369
assert.Equal(t, "test-app", ab.Manifest.ID)
54-
assert.True(t, len(ab.Source) > 0)
70+
assert.NotNil(t, ab.Source)
5571
}
32 KB
Loading

bundle/testdata/testapp/manifest.yaml

-2
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,3 @@ name: Test App
44
summary: For Testing
55
desc: It's an app for testing.
66
author: Test Dev
7-
fileName: test_app.star
8-
packageName: testapp

bundle/testdata/testapp/test_app.star

+6
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ Description: It's an app for testing.
55
Author: Test Dev
66
"""
77

8+
load("a_subdirectory/hi.jpg", hi_jpeg = "file")
89
load("render.star", "render")
910
load("schema.star", "schema")
11+
load("test.txt", test_txt = "file")
1012

1113
DEFAULT_WHO = "world"
1214

15+
TEST_TXT_CONTENT = test_txt.readall()
16+
17+
HI_JPEG_BYTES = hi_jpeg.readall("rb")
18+
1319
def main(config):
1420
who = config.str("who", DEFAULT_WHO)
1521
message = "Hello, {}!".format(who)

bundle/testdata/testapp/unused.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
this file is not used in the app

0 commit comments

Comments
 (0)