Skip to content

Commit

Permalink
Add AssemblyScript support to compute init and build commands (#160)
Browse files Browse the repository at this point in the history
* Add AssemblyScript language support to compute init and build commands.
  • Loading branch information
phamann authored Oct 27, 2020
1 parent 11e10f6 commit d25ef1a
Show file tree
Hide file tree
Showing 15 changed files with 794 additions and 145 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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ RELEASE_CHANGELOG.md
**/target
rust-toolchain
.cargo
**/node_modules

# Binaries for programs and plugins
*.exe
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.
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 {
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)
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{
"assembly/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

0 comments on commit d25ef1a

Please sign in to comment.