301 lines
6.9 KiB
Go
301 lines
6.9 KiB
Go
package utils
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/go-errors/errors"
|
|
"github.com/spf13/afero"
|
|
"github.com/tidwall/jsonc"
|
|
)
|
|
|
|
var (
|
|
//go:embed denos/*
|
|
denoEmbedDir embed.FS
|
|
// Used by unit tests
|
|
DenoPathOverride string
|
|
)
|
|
|
|
const (
|
|
// Legacy bundle options
|
|
DockerDenoDir = "/home/deno"
|
|
DockerEszipDir = "/root/eszips"
|
|
DenoVersion = "1.30.3"
|
|
)
|
|
|
|
func GetDenoPath() (string, error) {
|
|
if len(DenoPathOverride) > 0 {
|
|
return DenoPathOverride, nil
|
|
}
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
denoBinName := "deno"
|
|
if runtime.GOOS == "windows" {
|
|
denoBinName = "deno.exe"
|
|
}
|
|
denoPath := filepath.Join(home, ".supabase", denoBinName)
|
|
return denoPath, nil
|
|
}
|
|
|
|
func InstallOrUpgradeDeno(ctx context.Context, fsys afero.Fs) error {
|
|
denoPath, err := GetDenoPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := fsys.Stat(denoPath); err == nil {
|
|
// Upgrade Deno.
|
|
cmd := exec.CommandContext(ctx, denoPath, "upgrade", "--version", DenoVersion)
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Stdout = os.Stdout
|
|
return cmd.Run()
|
|
} else if !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
|
|
// Install Deno.
|
|
if err := MkdirIfNotExistFS(fsys, filepath.Dir(denoPath)); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 1. Determine OS triple
|
|
assetFilename, err := getDenoAssetFileName()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
assetRepo := "denoland/deno"
|
|
if runtime.GOOS == "linux" && runtime.GOARCH == "arm64" {
|
|
// TODO: version pin to official release once available https://github.com/denoland/deno/issues/1846
|
|
assetRepo = "LukeChannings/deno-arm64"
|
|
}
|
|
|
|
// 2. Download & install Deno binary.
|
|
{
|
|
assetUrl := fmt.Sprintf("https://github.com/%s/releases/download/v%s/%s", assetRepo, DenoVersion, assetFilename)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, assetUrl, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return errors.New("Failed installing Deno binary.")
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
|
|
// There should be only 1 file: the deno binary
|
|
if len(r.File) != 1 {
|
|
return err
|
|
}
|
|
denoContents, err := r.File[0].Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer denoContents.Close()
|
|
|
|
denoBytes, err := io.ReadAll(denoContents)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := afero.WriteFile(fsys, denoPath, denoBytes, 0755); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isScriptModified(fsys afero.Fs, destPath string, src []byte) (bool, error) {
|
|
dest, err := afero.ReadFile(fsys, destPath)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return true, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
// compare the md5 checksum of src bytes with user's copy.
|
|
// if the checksums doesn't match, script is modified.
|
|
return sha256.Sum256(dest) != sha256.Sum256(src), nil
|
|
}
|
|
|
|
type DenoScriptDir struct {
|
|
ExtractPath string
|
|
BuildPath string
|
|
}
|
|
|
|
// Copy Deno scripts needed for function deploy and downloads, returning a DenoScriptDir struct or an error.
|
|
func CopyDenoScripts(ctx context.Context, fsys afero.Fs) (*DenoScriptDir, error) {
|
|
denoPath, err := GetDenoPath()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
denoDirPath := filepath.Dir(denoPath)
|
|
scriptDirPath := filepath.Join(denoDirPath, "denos")
|
|
|
|
// make the script directory if not exist
|
|
if err := MkdirIfNotExistFS(fsys, scriptDirPath); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// copy embed files to script directory
|
|
err = fs.WalkDir(denoEmbedDir, "denos", func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// skip copying the directory
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
destPath := filepath.Join(denoDirPath, path)
|
|
|
|
contents, err := fs.ReadFile(denoEmbedDir, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// check if the script should be copied
|
|
modified, err := isScriptModified(fsys, destPath, contents)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !modified {
|
|
return nil
|
|
}
|
|
|
|
if err := afero.WriteFile(fsys, filepath.Join(denoDirPath, path), contents, 0666); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sd := DenoScriptDir{
|
|
ExtractPath: filepath.Join(scriptDirPath, "extract.ts"),
|
|
BuildPath: filepath.Join(scriptDirPath, "build.ts"),
|
|
}
|
|
|
|
return &sd, nil
|
|
}
|
|
|
|
type ImportMap struct {
|
|
Imports map[string]string `json:"imports"`
|
|
Scopes map[string]map[string]string `json:"scopes"`
|
|
}
|
|
|
|
func NewImportMap(absJsonPath string, fsys afero.Fs) (*ImportMap, error) {
|
|
data, err := afero.ReadFile(fsys, absJsonPath)
|
|
if err != nil {
|
|
return nil, errors.Errorf("failed to load import map: %w", err)
|
|
}
|
|
result := ImportMap{}
|
|
if err := result.Parse(data); err != nil {
|
|
return nil, err
|
|
}
|
|
// Resolve all paths relative to current file
|
|
for k, v := range result.Imports {
|
|
result.Imports[k] = resolveHostPath(absJsonPath, v, fsys)
|
|
}
|
|
for module, mapping := range result.Scopes {
|
|
for k, v := range mapping {
|
|
result.Scopes[module][k] = resolveHostPath(absJsonPath, v, fsys)
|
|
}
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
func (m *ImportMap) Parse(data []byte) error {
|
|
data = jsonc.ToJSONInPlace(data)
|
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
|
if err := decoder.Decode(&m); err != nil {
|
|
return errors.Errorf("failed to parse import map: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func resolveHostPath(jsonPath, hostPath string, fsys afero.Fs) string {
|
|
// Leave absolute paths unchanged
|
|
if filepath.IsAbs(hostPath) {
|
|
return hostPath
|
|
}
|
|
resolved := filepath.Join(filepath.Dir(jsonPath), hostPath)
|
|
if exists, err := afero.Exists(fsys, resolved); !exists {
|
|
// Leave URLs unchanged
|
|
if err != nil {
|
|
logger := GetDebugLogger()
|
|
fmt.Fprintln(logger, err)
|
|
}
|
|
return hostPath
|
|
}
|
|
// Directory imports need to be suffixed with /
|
|
// Ref: https://deno.com/manual@v1.33.0/basics/import_maps
|
|
if strings.HasSuffix(hostPath, string(filepath.Separator)) {
|
|
resolved += string(filepath.Separator)
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
func (m *ImportMap) BindHostModules() []string {
|
|
hostFuncDir, err := filepath.Abs(FunctionsDir)
|
|
if err != nil {
|
|
logger := GetDebugLogger()
|
|
fmt.Fprintln(logger, err)
|
|
}
|
|
binds := []string{}
|
|
for _, hostPath := range m.Imports {
|
|
if !filepath.IsAbs(hostPath) || strings.HasPrefix(hostPath, hostFuncDir) {
|
|
continue
|
|
}
|
|
dockerPath := ToDockerPath(hostPath)
|
|
binds = append(binds, hostPath+":"+dockerPath+":ro")
|
|
}
|
|
for _, mapping := range m.Scopes {
|
|
for _, hostPath := range mapping {
|
|
if !filepath.IsAbs(hostPath) || strings.HasPrefix(hostPath, hostFuncDir) {
|
|
continue
|
|
}
|
|
dockerPath := ToDockerPath(hostPath)
|
|
binds = append(binds, hostPath+":"+dockerPath+":ro")
|
|
}
|
|
}
|
|
return binds
|
|
}
|
|
|
|
func ToDockerPath(absHostPath string) string {
|
|
prefix := filepath.VolumeName(absHostPath)
|
|
dockerPath := filepath.ToSlash(absHostPath)
|
|
return strings.TrimPrefix(dockerPath, prefix)
|
|
}
|