Skip to content

Commit

Permalink
Add AssemblyScript language support to compute init and build commands.
Browse files Browse the repository at this point in the history
  • Loading branch information
phamann committed Oct 23, 2020
1 parent 11e10f6 commit 2fe6fc8
Show file tree
Hide file tree
Showing 12 changed files with 788 additions and 109 deletions.
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
// compute commands can use this to standardizec 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 {
//Constrcut 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.
var stdoutBuf, stderrBuf bytes.Buffer
stdoutIn, _ := cmd.StdoutPipe()
stderrIn, _ := cmd.StderrPipe()
stdout := io.MultiWriter(s.output, &stdoutBuf)
stderr := io.MultiWriter(s.output, &stderrBuf)

// 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)
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.
if !s.verbose && stderrBuf.Len() > 0 {
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
}
case err != nil:
return err
}
return nil
}
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.
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/")),
}
}

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
// 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")),
}
}

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

// Call npm install.
cmd := common.NewStreamingExec("npm", []string{"install"}, []string{}, false, out)
if err := cmd.Exec(); err != nil {
return err
}

return nil
}

// 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
}

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)
}
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
}
Loading

0 comments on commit 2fe6fc8

Please sign in to comment.