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
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
91 changes: 91 additions & 0 deletions pkg/common/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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 a
phamann marked this conversation as resolved.
Show resolved Hide resolved
// compute commands can use this to standardizec the flow control for each
phamann marked this conversation as resolved.
Show resolved Hide resolved
// 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 {
//Constrcut the command with given arguments and environment.
phamann marked this conversation as resolved.
Show resolved Hide resolved
//
// 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 stdoutBuf, stderrBuf bytes.Buffer
stdoutIn, _ := cmd.StdoutPipe()
stderrIn, _ := cmd.StderrPipe()
phamann marked this conversation as resolved.
Show resolved Hide resolved
stdout := io.MultiWriter(s.output, &stdoutBuf)
stderr := io.MultiWriter(s.output, &stderrBuf)
phamann marked this conversation as resolved.
Show resolved Hide resolved

// Start the command.
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start execution process: %w", err)
}

var errStdout, errStderr error
var wg sync.WaitGroup
wg.Add(1)

go func() {
_, errStdout = io.Copy(stdout, stdoutIn)
wg.Done()
}()

_, errStderr = io.Copy(stderr, stderrIn)
phamann marked this conversation as resolved.
Show resolved Hide resolved
wg.Wait()

if errStdout != nil {
return fmt.Errorf("error streaming stdout output from child process: %w", errStdout)
}
if errStderr != nil {
return fmt.Errorf("error streaming stderr output from child process: %w", errStderr)
}

// Wait for the command to exit.
if err := cmd.Wait(); err != nil {
// If we're not in verbose mode return the bufferred stderr output
// from cargo as the error.
phamann marked this conversation as resolved.
Show resolved Hide resolved
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
}
19 changes: 19 additions & 0 deletions pkg/common/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,22 @@ 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):
if err := os.MkdirAll(path, 0750); err != nil {
return err
}
phamann marked this conversation as resolved.
Show resolved Hide resolved
case err != nil:
return err
}
return nil
phamann marked this conversation as resolved.
Show resolved Hide resolved
}
230 changes: 230 additions & 0 deletions pkg/compute/assemblyscript.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
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 is an implments Toolchain for the AssemblyScript lanaguage.
phamann marked this conversation as resolved.
Show resolved Hide resolved
type AssemblyScript struct{}

// Name implements the Toolchain inferface and returns the name of the toolchain.
func (a AssemblyScript) Name() string { return "assemblyscript" }

// DisplayName implements the Toolchain inferface and returns the name of the
// toolchain suitable for displaying or printing to output.
func (a AssemblyScript) DisplayName() string { return "AssemblyScript (beta)" }

// StarterKits implements the Toolchain inferface and returns the list of
// starter kits that can be used to initialize a new package for the toolchain.
func (a AssemblyScript) StarterKits() []StarterKit {
return []StarterKit{
{
Name: "Default",
Path: "https://github.com/fastly/compute-starter-kit-assemblyscript-default",
Tag: "v0.1.0",
},
}
}

// SourceDirectory implements the Toolchain inferface and returns the source
// directory for AssemblyScript packages.
func (a AssemblyScript) SourceDirectory() string { return "src" }

// IncludeFiles implements the Toolchain interface and returns a list of
// additional files to include in the package archive for AssemblyScript packages.
func (a AssemblyScript) IncludeFiles() []string {
return []string{"package.json"}
}

// Verify implments 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.sj and npm by visiting:\n\n\t$ %s", text.Bold("https://nodejs.org/")),
phamann marked this conversation as resolved.
Show resolved Hide resolved
}
}

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 installd 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 dirctory.
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 implments the Toolchain interface and initializes a newly cloned
phamann marked this conversation as resolved.
Show resolved Hide resolved
// 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.sj 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")),
}
}
phamann marked this conversation as resolved.
Show resolved Hide resolved

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
if err := cmd.Exec(); err != nil {
return err
}

return nil
phamann marked this conversation as resolved.
Show resolved Hide resolved
}

// 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("error getting current working directory: %w", err)
}
binDir := filepath.Join(pwd, "bin")
if err := common.MakeDirectoryIfNotExists(binDir); err != nil {
return fmt.Errorf("error making bin directory: %w", err)
}

npmdir, err := getNpmBinPath()
if err != nil {
return err
phamann marked this conversation as resolved.
Show resolved Hide resolved
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion - thoughts on pulling these "metadata"-y bits into some centralized "initialization" function that gets called once and sets some "metadata" fields on like a config struct or something so that we don't have to have these checks in multiple places.

An additional benefit of this would be that the functions are more focused on what they are supposed to be doing --- e.g., "building" --- and less on validating the environment.


A valid counter argument to this would be then you have dependencies on the metadata fields being set and the functions --- e.g., Build --- are not entirely self-contained.


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

// 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 "", fmt.Errorf("error getting npm bin path: %w", err)
phamann marked this conversation as resolved.
Show resolved Hide resolved
}
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 */
cmd := exec.Command("npm", "link", "--json", "--depth", "0", name)
if err := cmd.Start(); err != nil {
return false
}
if err := cmd.Wait(); err != nil {
return false
}
return true
phamann marked this conversation as resolved.
Show resolved Hide resolved
}
Loading