github.com/supabase/cli@v1.168.1/internal/utils/deno.go (about)

     1  package utils
     2  
     3  import (
     4  	"archive/zip"
     5  	"bytes"
     6  	"context"
     7  	"crypto/sha256"
     8  	"embed"
     9  	"encoding/hex"
    10  	"encoding/json"
    11  	"fmt"
    12  	"io"
    13  	"io/fs"
    14  	"net/http"
    15  	"os"
    16  	"os/exec"
    17  	"path"
    18  	"path/filepath"
    19  	"runtime"
    20  	"strings"
    21  
    22  	"github.com/go-errors/errors"
    23  	"github.com/spf13/afero"
    24  )
    25  
    26  var (
    27  	//go:embed denos/*
    28  	denoEmbedDir embed.FS
    29  	// Used by unit tests
    30  	DenoPathOverride string
    31  )
    32  
    33  const (
    34  	DockerDenoDir     = "/home/deno"
    35  	DockerModsDir     = DockerDenoDir + "/modules"
    36  	DockerFuncDirPath = DockerDenoDir + "/functions"
    37  )
    38  
    39  func GetDenoPath() (string, error) {
    40  	if len(DenoPathOverride) > 0 {
    41  		return DenoPathOverride, nil
    42  	}
    43  	home, err := os.UserHomeDir()
    44  	if err != nil {
    45  		return "", err
    46  	}
    47  	denoBinName := "deno"
    48  	if runtime.GOOS == "windows" {
    49  		denoBinName = "deno.exe"
    50  	}
    51  	denoPath := filepath.Join(home, ".supabase", denoBinName)
    52  	return denoPath, nil
    53  }
    54  
    55  func InstallOrUpgradeDeno(ctx context.Context, fsys afero.Fs) error {
    56  	denoPath, err := GetDenoPath()
    57  	if err != nil {
    58  		return err
    59  	}
    60  
    61  	if _, err := fsys.Stat(denoPath); err == nil {
    62  		// Upgrade Deno.
    63  		cmd := exec.CommandContext(ctx, denoPath, "upgrade", "--version", DenoVersion)
    64  		cmd.Stderr = os.Stderr
    65  		cmd.Stdout = os.Stdout
    66  		return cmd.Run()
    67  	} else if !errors.Is(err, os.ErrNotExist) {
    68  		return err
    69  	}
    70  
    71  	// Install Deno.
    72  	if err := MkdirIfNotExistFS(fsys, filepath.Dir(denoPath)); err != nil {
    73  		return err
    74  	}
    75  
    76  	// 1. Determine OS triple
    77  	assetFilename, err := getDenoAssetFileName()
    78  	if err != nil {
    79  		return err
    80  	}
    81  	assetRepo := "denoland/deno"
    82  	if runtime.GOOS == "linux" && runtime.GOARCH == "arm64" {
    83  		// TODO: version pin to official release once available https://github.com/denoland/deno/issues/1846
    84  		assetRepo = "LukeChannings/deno-arm64"
    85  	}
    86  
    87  	// 2. Download & install Deno binary.
    88  	{
    89  		assetUrl := fmt.Sprintf("https://github.com/%s/releases/download/v%s/%s", assetRepo, DenoVersion, assetFilename)
    90  		req, err := http.NewRequestWithContext(ctx, http.MethodGet, assetUrl, nil)
    91  		if err != nil {
    92  			return err
    93  		}
    94  		resp, err := http.DefaultClient.Do(req)
    95  		if err != nil {
    96  			return err
    97  		}
    98  		defer resp.Body.Close()
    99  
   100  		if resp.StatusCode != 200 {
   101  			return errors.New("Failed installing Deno binary.")
   102  		}
   103  
   104  		body, err := io.ReadAll(resp.Body)
   105  		if err != nil {
   106  			return err
   107  		}
   108  
   109  		r, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
   110  		// There should be only 1 file: the deno binary
   111  		if len(r.File) != 1 {
   112  			return err
   113  		}
   114  		denoContents, err := r.File[0].Open()
   115  		if err != nil {
   116  			return err
   117  		}
   118  		defer denoContents.Close()
   119  
   120  		denoBytes, err := io.ReadAll(denoContents)
   121  		if err != nil {
   122  			return err
   123  		}
   124  
   125  		if err := afero.WriteFile(fsys, denoPath, denoBytes, 0755); err != nil {
   126  			return err
   127  		}
   128  	}
   129  
   130  	return nil
   131  }
   132  
   133  func isScriptModified(fsys afero.Fs, destPath string, src []byte) (bool, error) {
   134  	dest, err := afero.ReadFile(fsys, destPath)
   135  	if err != nil {
   136  		if errors.Is(err, fs.ErrNotExist) {
   137  			return true, nil
   138  		}
   139  		return false, err
   140  	}
   141  
   142  	// compare the md5 checksum of src bytes with user's copy.
   143  	// if the checksums doesn't match, script is modified.
   144  	return sha256.Sum256(dest) != sha256.Sum256(src), nil
   145  }
   146  
   147  type DenoScriptDir struct {
   148  	ExtractPath string
   149  	BuildPath   string
   150  }
   151  
   152  // Copy Deno scripts needed for function deploy and downloads, returning a DenoScriptDir struct or an error.
   153  func CopyDenoScripts(ctx context.Context, fsys afero.Fs) (*DenoScriptDir, error) {
   154  	denoPath, err := GetDenoPath()
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	denoDirPath := filepath.Dir(denoPath)
   160  	scriptDirPath := filepath.Join(denoDirPath, "denos")
   161  
   162  	// make the script directory if not exist
   163  	if err := MkdirIfNotExistFS(fsys, scriptDirPath); err != nil {
   164  		return nil, err
   165  	}
   166  
   167  	// copy embed files to script directory
   168  	err = fs.WalkDir(denoEmbedDir, "denos", func(path string, d fs.DirEntry, err error) error {
   169  		if err != nil {
   170  			return err
   171  		}
   172  
   173  		// skip copying the directory
   174  		if d.IsDir() {
   175  			return nil
   176  		}
   177  
   178  		destPath := filepath.Join(denoDirPath, path)
   179  
   180  		contents, err := fs.ReadFile(denoEmbedDir, path)
   181  		if err != nil {
   182  			return err
   183  		}
   184  
   185  		// check if the script should be copied
   186  		modified, err := isScriptModified(fsys, destPath, contents)
   187  		if err != nil {
   188  			return err
   189  		}
   190  		if !modified {
   191  			return nil
   192  		}
   193  
   194  		if err := afero.WriteFile(fsys, filepath.Join(denoDirPath, path), contents, 0666); err != nil {
   195  			return err
   196  		}
   197  
   198  		return nil
   199  	})
   200  
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  
   205  	sd := DenoScriptDir{
   206  		ExtractPath: filepath.Join(scriptDirPath, "extract.ts"),
   207  		BuildPath:   filepath.Join(scriptDirPath, "build.ts"),
   208  	}
   209  
   210  	return &sd, nil
   211  }
   212  
   213  type ImportMap struct {
   214  	Imports map[string]string            `json:"imports"`
   215  	Scopes  map[string]map[string]string `json:"scopes"`
   216  }
   217  
   218  func NewImportMap(path string, fsys afero.Fs) (*ImportMap, error) {
   219  	contents, err := fsys.Open(path)
   220  	if err != nil {
   221  		return nil, errors.Errorf("failed to load import map: %w", err)
   222  	}
   223  	defer contents.Close()
   224  	return NewFromReader(contents)
   225  }
   226  
   227  func NewFromReader(r io.Reader) (*ImportMap, error) {
   228  	decoder := json.NewDecoder(r)
   229  	importMap := &ImportMap{}
   230  	if err := decoder.Decode(importMap); err != nil {
   231  		return nil, errors.Errorf("failed to parse import map: %w", err)
   232  	}
   233  	return importMap, nil
   234  }
   235  
   236  func (m *ImportMap) Resolve(fsys afero.Fs) ImportMap {
   237  	result := ImportMap{
   238  		Imports: make(map[string]string, len(m.Imports)),
   239  		Scopes:  make(map[string]map[string]string, len(m.Scopes)),
   240  	}
   241  	for k, v := range m.Imports {
   242  		result.Imports[k] = resolveHostPath(v, fsys)
   243  	}
   244  	for module, mapping := range m.Scopes {
   245  		result.Scopes[module] = map[string]string{}
   246  		for k, v := range mapping {
   247  			result.Scopes[module][k] = resolveHostPath(v, fsys)
   248  		}
   249  	}
   250  	return result
   251  }
   252  
   253  func (m *ImportMap) BindModules(resolved ImportMap) []string {
   254  	cwd, err := os.Getwd()
   255  	if err != nil {
   256  		return nil
   257  	}
   258  	var binds []string
   259  	for k, dockerPath := range resolved.Imports {
   260  		if strings.HasPrefix(dockerPath, DockerModsDir) {
   261  			hostPath := filepath.Join(cwd, FunctionsDir, m.Imports[k])
   262  			binds = append(binds, hostPath+":"+dockerPath+":ro,z")
   263  		}
   264  	}
   265  	for module, mapping := range resolved.Scopes {
   266  		for k, dockerPath := range mapping {
   267  			if strings.HasPrefix(dockerPath, DockerModsDir) {
   268  				hostPath := filepath.Join(cwd, FunctionsDir, m.Scopes[module][k])
   269  				binds = append(binds, hostPath+":"+dockerPath+":ro,z")
   270  			}
   271  		}
   272  	}
   273  	return binds
   274  }
   275  
   276  func resolveHostPath(hostPath string, fsys afero.Fs) string {
   277  	// All local fs imports will be mounted to /home/deno/modules
   278  	if filepath.IsAbs(hostPath) {
   279  		return getModulePath(hostPath)
   280  	}
   281  	rel := filepath.Join(FunctionsDir, hostPath)
   282  	exists, err := afero.Exists(fsys, rel)
   283  	if err != nil {
   284  		logger := GetDebugLogger()
   285  		fmt.Fprintln(logger, err)
   286  	}
   287  	if !exists {
   288  		return hostPath
   289  	}
   290  	if strings.HasPrefix(rel, FunctionsDir) {
   291  		suffix := strings.TrimPrefix(rel, FunctionsDir)
   292  		return path.Join(DockerFuncDirPath, filepath.ToSlash(suffix))
   293  	}
   294  	// Directory imports need to be suffixed with /
   295  	// Ref: https://deno.com/manual@v1.33.0/basics/import_maps
   296  	if strings.HasSuffix(hostPath, string(filepath.Separator)) {
   297  		rel += string(filepath.Separator)
   298  	}
   299  	return getModulePath(rel)
   300  }
   301  
   302  func getModulePath(hostPath string) string {
   303  	mod := path.Join(DockerModsDir, GetPathHash(hostPath))
   304  	if strings.HasSuffix(hostPath, string(filepath.Separator)) {
   305  		mod += "/"
   306  	} else if ext := filepath.Ext(hostPath); len(ext) > 0 {
   307  		mod += ext
   308  	}
   309  	return mod
   310  }
   311  
   312  func GetPathHash(path string) string {
   313  	digest := sha256.Sum256([]byte(path))
   314  	return hex.EncodeToString(digest[:])
   315  }
   316  
   317  func AbsImportMapPath(importMapPath, slug string, fsys afero.Fs) (string, error) {
   318  	if importMapPath == "" {
   319  		if functionConfig, ok := Config.Functions[slug]; ok && functionConfig.ImportMap != "" {
   320  			importMapPath = functionConfig.ImportMap
   321  			if !filepath.IsAbs(importMapPath) {
   322  				importMapPath = filepath.Join(SupabaseDirPath, importMapPath)
   323  			}
   324  		} else if exists, err := afero.Exists(fsys, FallbackImportMapPath); exists {
   325  			importMapPath = FallbackImportMapPath
   326  		} else {
   327  			if err != nil {
   328  				logger := GetDebugLogger()
   329  				fmt.Fprintln(logger, err)
   330  			}
   331  			return importMapPath, nil
   332  		}
   333  	}
   334  	resolved, err := filepath.Abs(importMapPath)
   335  	if err != nil {
   336  		return "", err
   337  	}
   338  	if f, err := fsys.Stat(resolved); err != nil {
   339  		return "", errors.Errorf("Failed to read import map: %w", err)
   340  	} else if f.IsDir() {
   341  		return "", errors.New("Importing directory is unsupported: " + resolved)
   342  	}
   343  	return resolved, nil
   344  }
   345  
   346  func AbsTempImportMapPath(cwd, hostPath string) string {
   347  	name := GetPathHash(hostPath) + ".json"
   348  	return filepath.Join(cwd, ImportMapsDir, name)
   349  }
   350  
   351  func BindImportMap(hostImportMapPath, dockerImportMapPath string, fsys afero.Fs) ([]string, error) {
   352  	importMap, err := NewImportMap(hostImportMapPath, fsys)
   353  	if err != nil {
   354  		return nil, err
   355  	}
   356  	resolved := importMap.Resolve(fsys)
   357  	binds := importMap.BindModules(resolved)
   358  	if len(binds) > 0 {
   359  		cwd, err := os.Getwd()
   360  		if err != nil {
   361  			return nil, errors.Errorf("failed to get working directory: %w", err)
   362  		}
   363  		contents, err := json.MarshalIndent(resolved, "", "    ")
   364  		if err != nil {
   365  			return nil, errors.Errorf("failed to encode json: %w", err)
   366  		}
   367  		// Rewrite import map to temporary host path
   368  		hostImportMapPath = AbsTempImportMapPath(cwd, hostImportMapPath)
   369  		if err := WriteFile(hostImportMapPath, contents, fsys); err != nil {
   370  			return nil, err
   371  		}
   372  	}
   373  	binds = append(binds, hostImportMapPath+":"+dockerImportMapPath+":ro,z")
   374  	return binds, nil
   375  }