Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AssemblyScript support to compute init and build commands #160

Merged
merged 10 commits into from
Oct 27, 2020
4 changes: 4 additions & 0 deletions .github/workflows/pr_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jobs:
strategy:
matrix:
go-version: [1.14.x]
node-version: [12]
rust-toolchain: [1.46.0]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
Expand Down Expand Up @@ -83,6 +84,9 @@ jobs:
toolchain: ${{ matrix.rust-toolchain }}
- name: Add wasm32-wasi Rust target
run: rustup target add wasm32-wasi --toolchain ${{ matrix.rust-toolchain }}
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Test
run: make test
shell: bash
Expand Down
2 changes: 1 addition & 1 deletion pkg/app/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func Run(args []string, env config.Environment, file config.File, configFilePath
serviceVersionLock := serviceversion.NewLockCommand(serviceVersionRoot.CmdClause, &globals)

computeRoot := compute.NewRootCommand(app, &globals)
computeInit := compute.NewInitCommand(computeRoot.CmdClause, &globals)
computeInit := compute.NewInitCommand(computeRoot.CmdClause, httpClient, &globals)
computeBuild := compute.NewBuildCommand(computeRoot.CmdClause, httpClient, &globals)
computeDeploy := compute.NewDeployCommand(computeRoot.CmdClause, httpClient, &globals)
computeUpdate := compute.NewUpdateCommand(computeRoot.CmdClause, httpClient, &globals)
Expand Down
1 change: 1 addition & 0 deletions pkg/app/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ COMMANDS
of the --path destination
-d, --description=DESCRIPTION Description of the package
-a, --author=AUTHOR ... Author(s) of the package
-l, --language=LANGUAGE Language of the package
-f, --from=FROM Git repository containing package template
-p, --path=PATH Destination to write the new package,
defaulting to the current directory
Expand Down
62 changes: 62 additions & 0 deletions pkg/common/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package common

import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"strings"
// "sync"
)

// StreamingExec models a generic command execution that consumers can use to
// execute commands and stream their output to an io.Writer. For example
// compute commands can use this to standardize the flow control for each
// compiler toolchain.
type StreamingExec struct {
command string
args []string
env []string
verbose bool
output io.Writer
}

// NewStreamingExec constructs a new StreamingExec instance.
func NewStreamingExec(cmd string, args, env []string, verbose bool, out io.Writer) *StreamingExec {
return &StreamingExec{
cmd,
args,
env,
verbose,
out,
}
}

// Exec executes the compiler command and pipes the child process stdout and
// stderr output to the supplied io.Writer, it waits for the command to exit
// cleanly or returns an error.
func (s StreamingExec) Exec() error {
// Construct the command with given arguments and environment.
//
// gosec flagged this:
// G204 (CWE-78): Subprocess launched with variable
// Disabling as the variables come from trusted sources.
/* #nosec */
cmd := exec.Command(s.command, s.args...)
cmd.Env = append(os.Environ(), s.env...)

// Pipe the child process stdout and stderr to our own output writer.
phamann marked this conversation as resolved.
Show resolved Hide resolved
var stderrBuf bytes.Buffer
cmd.Stdout = s.output
cmd.Stderr = io.MultiWriter(s.output, &stderrBuf)

if err := cmd.Run(); err != nil {
if !s.verbose && stderrBuf.Len() > 0 {
phamann marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("error during execution process:\n%s", strings.TrimSpace(stderrBuf.String()))
}
return fmt.Errorf("error during execution process")
}

return nil
}
18 changes: 18 additions & 0 deletions pkg/common/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,21 @@ func CopyFile(src, dst string) (err error) {

return
}

// MakeDirectoryIfNotExists asserts whether a directory exists and makes it
// if not. Returns nil if exists or successfully made.
func MakeDirectoryIfNotExists(path string) error {
fi, err := os.Stat(path)
switch {
case err == nil && fi.IsDir():
return nil
case err == nil && !fi.IsDir():
return fmt.Errorf("%s already exists as a regular file", path)
case os.IsNotExist(err):
return os.MkdirAll(path, 0750)
case err != nil:
return err
}

return nil
}
198 changes: 198 additions & 0 deletions pkg/compute/assemblyscript.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package compute

import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/fastly/cli/pkg/common"
"github.com/fastly/cli/pkg/errors"
"github.com/fastly/cli/pkg/text"
)

// AssemblyScript implements Toolchain for the AssemblyScript language.
type AssemblyScript struct{}

// NewAssemblyScript constructs a new AssemblyScript.
func NewAssemblyScript() *AssemblyScript {
return &AssemblyScript{}
}

// Verify implements the Toolchain interface and verifies whether the
// AssemblyScript language toolchain is correctly configured on the host.
func (a AssemblyScript) Verify(out io.Writer) error {
// 1) Check `npm` is on $PATH
//
// npm is Node/AssemblyScript's toolchain installer and manager, it is
// needed to assert that the correct versions of the asc compiler and
// @fastly/as-compute package are installed. We only check whether the
// binary exists on the users $PATH and error with installation help text.
fmt.Fprintf(out, "Checking if npm is installed...\n")

p, err := exec.LookPath("npm")
if err != nil {
return errors.RemediationError{
Inner: fmt.Errorf("`npm` not found in $PATH"),
Remediation: fmt.Sprintf("To fix this error, install Node.js and npm by visiting:\n\n\t$ %s", text.Bold("https://nodejs.org/")),
}
}

fmt.Fprintf(out, "Found npm at %s\n", p)

// 2) Check package.json file exists in $PWD
//
// A valid npm package is needed for compilation and to assert whether the
// required dependencies are installed locally. Therefore, we first assert
// whether one exists in the current $PWD.
fpath, err := filepath.Abs("package.json")
if err != nil {
return fmt.Errorf("getting package.json path: %w", err)
}

if !common.FileExists(fpath) {
return errors.RemediationError{
Inner: fmt.Errorf("package.json not found"),
Remediation: fmt.Sprintf("To fix this error, run the following command:\n\n\t$ %s", text.Bold("npm init")),
}
}

fmt.Fprintf(out, "Found package.json at %s\n", fpath)

// 3) Check if `asc` is installed.
//
// asc is the AssemblyScript compiler. We first check if it exists in the
// package.json and then whether the binary exists in the npm bin directory.
fmt.Fprintf(out, "Checking if AssemblyScript is installed...\n")
if !checkPackageDependencyExists("assemblyscript") {
return errors.RemediationError{
Inner: fmt.Errorf("`assemblyscript` not found in package.json"),
Remediation: fmt.Sprintf("To fix this error, run the following command:\n\n\t$ %s", text.Bold("npm install --save-dev assemblyscript")),
}
}

p, err = getNpmBinPath()
if err != nil {
return errors.RemediationError{
Inner: fmt.Errorf("could not determine npm bin path"),
Remediation: fmt.Sprintf("To fix this error, run the following command:\n\n\t$ %s", text.Bold("npm install --global npm@latest")),
}
}

path, err := exec.LookPath(filepath.Join(p, "asc"))
if err != nil {
return fmt.Errorf("getting asc path: %w", err)
}
if !common.FileExists(path) {
return errors.RemediationError{
Inner: fmt.Errorf("`asc` binary not found in %s", p),
Remediation: fmt.Sprintf("To fix this error, run the following command:\n\n\t$ %s", text.Bold("npm install --save-dev assemblyscript")),
}
}

fmt.Fprintf(out, "Found asc at %s\n", path)

return nil
}

// Initialize implements the Toolchain interface and initializes a newly cloned
// package by installing required dependencies.
func (a AssemblyScript) Initialize(out io.Writer) error {
// 1) Check `npm` is on $PATH
//
// npm is Node/AssemblyScript's toolchain package manager, it is needed to
// install the package dependencies on initialization. We only check whether
// the binary exists on the users $PATH and error with installation help text.
fmt.Fprintf(out, "Checking if npm is installed...\n")

p, err := exec.LookPath("npm")
if err != nil {
return errors.RemediationError{
Inner: fmt.Errorf("`npm` not found in $PATH"),
Remediation: fmt.Sprintf("To fix this error, install Node.js and npm by visiting:\n\n\t$ %s", text.Bold("https://nodejs.org/")),
}
}

fmt.Fprintf(out, "Found npm at %s\n", p)

// 2) Check package.json file exists in $PWD
//
// A valid npm package manifest file is needed for the install command to
// work. Therefore, we first assert whether one exists in the current $PWD.
fpath, err := filepath.Abs("package.json")
if err != nil {
return fmt.Errorf("getting package.json path: %w", err)
}

if !common.FileExists(fpath) {
return errors.RemediationError{
Inner: fmt.Errorf("package.json not found"),
Remediation: fmt.Sprintf("To fix this error, run the following command:\n\n\t$ %s", text.Bold("npm init")),
}
}

fmt.Fprintf(out, "Found package.json at %s\n", fpath)

// Call npm install.
cmd := common.NewStreamingExec("npm", []string{"install"}, []string{}, false, out)
phamann marked this conversation as resolved.
Show resolved Hide resolved
return cmd.Exec()
}

// Build implements the Toolchain interface and attempts to compile the package
// AssemblyScript source to a Wasm binary.
func (a AssemblyScript) Build(out io.Writer, verbose bool) error {
// Check if bin directory exists and create if not.
pwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("getting current working directory: %w", err)
}
binDir := filepath.Join(pwd, "bin")
if err := common.MakeDirectoryIfNotExists(binDir); err != nil {
return fmt.Errorf("making bin directory: %w", err)
}

npmdir, err := getNpmBinPath()
if err != nil {
return fmt.Errorf("getting npm path: %w", err)
}

args := []string{
"src/index.ts",
"--binaryFile",
filepath.Join(binDir, "main.wasm"),
"--optimize",
"--noAssert",
}
if verbose {
args = append(args, "--verbose")
}

fmt.Fprintf(out, "Installing package dependencies...\n")

// Call asc with the build arguments.
cmd := common.NewStreamingExec(filepath.Join(npmdir, "asc"), args, []string{}, verbose, out)
if err := cmd.Exec(); err != nil {
return err
}

return nil
}

func getNpmBinPath() (string, error) {
path, err := exec.Command("npm", "bin").Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(path)), nil
}

func checkPackageDependencyExists(name string) bool {
// gosec flagged this:
// G204 (CWE-78): Subprocess launched with variable
// Disabling as the variables come from trusted sources.
/* #nosec */
err := exec.Command("npm", "list", "--json", "--depth", "0", name).Run()
return err == nil
}
Loading