Skip to content

Commit d25ef1a

Browse files
author
Patrick Hamann
authored
Add AssemblyScript support to compute init and build commands (#160)
* Add AssemblyScript language support to compute init and build commands.
1 parent 11e10f6 commit d25ef1a

15 files changed

Lines changed: 794 additions & 145 deletions

File tree

.github/workflows/pr_test.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ jobs:
5555
strategy:
5656
matrix:
5757
go-version: [1.14.x]
58+
node-version: [12]
5859
rust-toolchain: [1.46.0]
5960
platform: [ubuntu-latest, macos-latest, windows-latest]
6061
runs-on: ${{ matrix.platform }}
@@ -83,6 +84,9 @@ jobs:
8384
toolchain: ${{ matrix.rust-toolchain }}
8485
- name: Add wasm32-wasi Rust target
8586
run: rustup target add wasm32-wasi --toolchain ${{ matrix.rust-toolchain }}
87+
- uses: actions/setup-node@v1
88+
with:
89+
node-version: ${{ matrix.node-version }}
8690
- name: Test
8791
run: make test
8892
shell: bash

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ RELEASE_CHANGELOG.md
1818
**/target
1919
rust-toolchain
2020
.cargo
21+
**/node_modules
2122

2223
# Binaries for programs and plugins
2324
*.exe

pkg/app/run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ func Run(args []string, env config.Environment, file config.File, configFilePath
129129
serviceVersionLock := serviceversion.NewLockCommand(serviceVersionRoot.CmdClause, &globals)
130130

131131
computeRoot := compute.NewRootCommand(app, &globals)
132-
computeInit := compute.NewInitCommand(computeRoot.CmdClause, &globals)
132+
computeInit := compute.NewInitCommand(computeRoot.CmdClause, httpClient, &globals)
133133
computeBuild := compute.NewBuildCommand(computeRoot.CmdClause, httpClient, &globals)
134134
computeDeploy := compute.NewDeployCommand(computeRoot.CmdClause, httpClient, &globals)
135135
computeUpdate := compute.NewUpdateCommand(computeRoot.CmdClause, httpClient, &globals)

pkg/app/run_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ COMMANDS
274274
of the --path destination
275275
-d, --description=DESCRIPTION Description of the package
276276
-a, --author=AUTHOR ... Author(s) of the package
277+
-l, --language=LANGUAGE Language of the package
277278
-f, --from=FROM Git repository containing package template
278279
-p, --path=PATH Destination to write the new package,
279280
defaulting to the current directory

pkg/common/exec.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package common
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"io"
7+
"os"
8+
"os/exec"
9+
"strings"
10+
// "sync"
11+
)
12+
13+
// StreamingExec models a generic command execution that consumers can use to
14+
// execute commands and stream their output to an io.Writer. For example
15+
// compute commands can use this to standardize the flow control for each
16+
// compiler toolchain.
17+
type StreamingExec struct {
18+
command string
19+
args []string
20+
env []string
21+
verbose bool
22+
output io.Writer
23+
}
24+
25+
// NewStreamingExec constructs a new StreamingExec instance.
26+
func NewStreamingExec(cmd string, args, env []string, verbose bool, out io.Writer) *StreamingExec {
27+
return &StreamingExec{
28+
cmd,
29+
args,
30+
env,
31+
verbose,
32+
out,
33+
}
34+
}
35+
36+
// Exec executes the compiler command and pipes the child process stdout and
37+
// stderr output to the supplied io.Writer, it waits for the command to exit
38+
// cleanly or returns an error.
39+
func (s StreamingExec) Exec() error {
40+
// Construct the command with given arguments and environment.
41+
//
42+
// gosec flagged this:
43+
// G204 (CWE-78): Subprocess launched with variable
44+
// Disabling as the variables come from trusted sources.
45+
/* #nosec */
46+
cmd := exec.Command(s.command, s.args...)
47+
cmd.Env = append(os.Environ(), s.env...)
48+
49+
// Pipe the child process stdout and stderr to our own output writer.
50+
var stderrBuf bytes.Buffer
51+
cmd.Stdout = s.output
52+
cmd.Stderr = io.MultiWriter(s.output, &stderrBuf)
53+
54+
if err := cmd.Run(); err != nil {
55+
if !s.verbose && stderrBuf.Len() > 0 {
56+
return fmt.Errorf("error during execution process:\n%s", strings.TrimSpace(stderrBuf.String()))
57+
}
58+
return fmt.Errorf("error during execution process")
59+
}
60+
61+
return nil
62+
}

pkg/common/file.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,21 @@ func CopyFile(src, dst string) (err error) {
8080

8181
return
8282
}
83+
84+
// MakeDirectoryIfNotExists asserts whether a directory exists and makes it
85+
// if not. Returns nil if exists or successfully made.
86+
func MakeDirectoryIfNotExists(path string) error {
87+
fi, err := os.Stat(path)
88+
switch {
89+
case err == nil && fi.IsDir():
90+
return nil
91+
case err == nil && !fi.IsDir():
92+
return fmt.Errorf("%s already exists as a regular file", path)
93+
case os.IsNotExist(err):
94+
return os.MkdirAll(path, 0750)
95+
case err != nil:
96+
return err
97+
}
98+
99+
return nil
100+
}

pkg/compute/assemblyscript.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package compute
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/fastly/cli/pkg/common"
12+
"github.com/fastly/cli/pkg/errors"
13+
"github.com/fastly/cli/pkg/text"
14+
)
15+
16+
// AssemblyScript implements Toolchain for the AssemblyScript language.
17+
type AssemblyScript struct{}
18+
19+
// NewAssemblyScript constructs a new AssemblyScript.
20+
func NewAssemblyScript() *AssemblyScript {
21+
return &AssemblyScript{}
22+
}
23+
24+
// Verify implements the Toolchain interface and verifies whether the
25+
// AssemblyScript language toolchain is correctly configured on the host.
26+
func (a AssemblyScript) Verify(out io.Writer) error {
27+
// 1) Check `npm` is on $PATH
28+
//
29+
// npm is Node/AssemblyScript's toolchain installer and manager, it is
30+
// needed to assert that the correct versions of the asc compiler and
31+
// @fastly/as-compute package are installed. We only check whether the
32+
// binary exists on the users $PATH and error with installation help text.
33+
fmt.Fprintf(out, "Checking if npm is installed...\n")
34+
35+
p, err := exec.LookPath("npm")
36+
if err != nil {
37+
return errors.RemediationError{
38+
Inner: fmt.Errorf("`npm` not found in $PATH"),
39+
Remediation: fmt.Sprintf("To fix this error, install Node.js and npm by visiting:\n\n\t$ %s", text.Bold("https://nodejs.org/")),
40+
}
41+
}
42+
43+
fmt.Fprintf(out, "Found npm at %s\n", p)
44+
45+
// 2) Check package.json file exists in $PWD
46+
//
47+
// A valid npm package is needed for compilation and to assert whether the
48+
// required dependencies are installed locally. Therefore, we first assert
49+
// whether one exists in the current $PWD.
50+
fpath, err := filepath.Abs("package.json")
51+
if err != nil {
52+
return fmt.Errorf("getting package.json path: %w", err)
53+
}
54+
55+
if !common.FileExists(fpath) {
56+
return errors.RemediationError{
57+
Inner: fmt.Errorf("package.json not found"),
58+
Remediation: fmt.Sprintf("To fix this error, run the following command:\n\n\t$ %s", text.Bold("npm init")),
59+
}
60+
}
61+
62+
fmt.Fprintf(out, "Found package.json at %s\n", fpath)
63+
64+
// 3) Check if `asc` is installed.
65+
//
66+
// asc is the AssemblyScript compiler. We first check if it exists in the
67+
// package.json and then whether the binary exists in the npm bin directory.
68+
fmt.Fprintf(out, "Checking if AssemblyScript is installed...\n")
69+
if !checkPackageDependencyExists("assemblyscript") {
70+
return errors.RemediationError{
71+
Inner: fmt.Errorf("`assemblyscript` not found in package.json"),
72+
Remediation: fmt.Sprintf("To fix this error, run the following command:\n\n\t$ %s", text.Bold("npm install --save-dev assemblyscript")),
73+
}
74+
}
75+
76+
p, err = getNpmBinPath()
77+
if err != nil {
78+
return errors.RemediationError{
79+
Inner: fmt.Errorf("could not determine npm bin path"),
80+
Remediation: fmt.Sprintf("To fix this error, run the following command:\n\n\t$ %s", text.Bold("npm install --global npm@latest")),
81+
}
82+
}
83+
84+
path, err := exec.LookPath(filepath.Join(p, "asc"))
85+
if err != nil {
86+
return fmt.Errorf("getting asc path: %w", err)
87+
}
88+
if !common.FileExists(path) {
89+
return errors.RemediationError{
90+
Inner: fmt.Errorf("`asc` binary not found in %s", p),
91+
Remediation: fmt.Sprintf("To fix this error, run the following command:\n\n\t$ %s", text.Bold("npm install --save-dev assemblyscript")),
92+
}
93+
}
94+
95+
fmt.Fprintf(out, "Found asc at %s\n", path)
96+
97+
return nil
98+
}
99+
100+
// Initialize implements the Toolchain interface and initializes a newly cloned
101+
// package by installing required dependencies.
102+
func (a AssemblyScript) Initialize(out io.Writer) error {
103+
// 1) Check `npm` is on $PATH
104+
//
105+
// npm is Node/AssemblyScript's toolchain package manager, it is needed to
106+
// install the package dependencies on initialization. We only check whether
107+
// the binary exists on the users $PATH and error with installation help text.
108+
fmt.Fprintf(out, "Checking if npm is installed...\n")
109+
110+
p, err := exec.LookPath("npm")
111+
if err != nil {
112+
return errors.RemediationError{
113+
Inner: fmt.Errorf("`npm` not found in $PATH"),
114+
Remediation: fmt.Sprintf("To fix this error, install Node.js and npm by visiting:\n\n\t$ %s", text.Bold("https://nodejs.org/")),
115+
}
116+
}
117+
118+
fmt.Fprintf(out, "Found npm at %s\n", p)
119+
120+
// 2) Check package.json file exists in $PWD
121+
//
122+
// A valid npm package manifest file is needed for the install command to
123+
// work. Therefore, we first assert whether one exists in the current $PWD.
124+
fpath, err := filepath.Abs("package.json")
125+
if err != nil {
126+
return fmt.Errorf("getting package.json path: %w", err)
127+
}
128+
129+
if !common.FileExists(fpath) {
130+
return errors.RemediationError{
131+
Inner: fmt.Errorf("package.json not found"),
132+
Remediation: fmt.Sprintf("To fix this error, run the following command:\n\n\t$ %s", text.Bold("npm init")),
133+
}
134+
}
135+
136+
fmt.Fprintf(out, "Found package.json at %s\n", fpath)
137+
138+
// Call npm install.
139+
cmd := common.NewStreamingExec("npm", []string{"install"}, []string{}, false, out)
140+
return cmd.Exec()
141+
}
142+
143+
// Build implements the Toolchain interface and attempts to compile the package
144+
// AssemblyScript source to a Wasm binary.
145+
func (a AssemblyScript) Build(out io.Writer, verbose bool) error {
146+
// Check if bin directory exists and create if not.
147+
pwd, err := os.Getwd()
148+
if err != nil {
149+
return fmt.Errorf("getting current working directory: %w", err)
150+
}
151+
binDir := filepath.Join(pwd, "bin")
152+
if err := common.MakeDirectoryIfNotExists(binDir); err != nil {
153+
return fmt.Errorf("making bin directory: %w", err)
154+
}
155+
156+
npmdir, err := getNpmBinPath()
157+
if err != nil {
158+
return fmt.Errorf("getting npm path: %w", err)
159+
}
160+
161+
args := []string{
162+
"assembly/index.ts",
163+
"--binaryFile",
164+
filepath.Join(binDir, "main.wasm"),
165+
"--optimize",
166+
"--noAssert",
167+
}
168+
if verbose {
169+
args = append(args, "--verbose")
170+
}
171+
172+
fmt.Fprintf(out, "Installing package dependencies...\n")
173+
174+
// Call asc with the build arguments.
175+
cmd := common.NewStreamingExec(filepath.Join(npmdir, "asc"), args, []string{}, verbose, out)
176+
if err := cmd.Exec(); err != nil {
177+
return err
178+
}
179+
180+
return nil
181+
}
182+
183+
func getNpmBinPath() (string, error) {
184+
path, err := exec.Command("npm", "bin").Output()
185+
if err != nil {
186+
return "", err
187+
}
188+
return strings.TrimSpace(string(path)), nil
189+
}
190+
191+
func checkPackageDependencyExists(name string) bool {
192+
// gosec flagged this:
193+
// G204 (CWE-78): Subprocess launched with variable
194+
// Disabling as the variables come from trusted sources.
195+
/* #nosec */
196+
err := exec.Command("npm", "list", "--json", "--depth", "0", name).Run()
197+
return err == nil
198+
}

0 commit comments

Comments
 (0)