github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/cmd/vpm/build.go (about)

     1  /*
     2   * Copyright (c) 2024-present unTill Pro, Ltd.
     3   * @author Alisher Nurmanov
     4   */
     5  
     6  package main
     7  
     8  import (
     9  	"errors"
    10  	"fmt"
    11  	"github.com/voedger/voedger/pkg/parser"
    12  	"io"
    13  	"os"
    14  	"path/filepath"
    15  	"strings"
    16  
    17  	"github.com/google/uuid"
    18  
    19  	"github.com/spf13/cobra"
    20  	"github.com/voedger/voedger/pkg/compile"
    21  	"github.com/voedger/voedger/pkg/goutils/exec"
    22  	"github.com/voedger/voedger/pkg/goutils/logger"
    23  	coreutils "github.com/voedger/voedger/pkg/utils"
    24  )
    25  
    26  func newBuildCmd(params *vpmParams) *cobra.Command {
    27  	cmd := &cobra.Command{
    28  		Use:   "build [-C] [-o <archive-name>]",
    29  		Short: "build",
    30  		RunE: func(cmd *cobra.Command, args []string) error {
    31  			exists, err := checkPackageGenFileExists(params.Dir)
    32  			if err != nil {
    33  				return err
    34  			}
    35  			if !exists {
    36  				return errors.New("packages_gen.go not found. Run 'vpm init'")
    37  			}
    38  
    39  			compileRes, err := compile.CompileNoDummyApp(params.Dir)
    40  			if err := checkAppSchemaNotFoundErr(err); err != nil {
    41  				return err
    42  			}
    43  			if err := checkCompileResult(compileRes); err != nil {
    44  				return err
    45  			}
    46  			return build(compileRes, params)
    47  		},
    48  	}
    49  	cmd.SilenceErrors = true
    50  	cmd.Flags().StringVarP(&params.Dir, "change-dir", "C", "", "Change to dir before running the command. Any files named on the command line are interpreted after changing directories. If used, this flag must be the first one in the command line.")
    51  	cmd.Flags().StringVarP(&params.Output, "output", "o", "", "output archive name")
    52  	return cmd
    53  }
    54  
    55  func checkAppSchemaNotFoundErr(err error) error {
    56  	if err != nil {
    57  		logger.Error(err)
    58  		if errors.Is(err, compile.ErrAppSchemaNotFound) {
    59  			return errors.New("failed to build, app schema not found")
    60  		}
    61  	}
    62  	return nil
    63  }
    64  
    65  func checkCompileResult(compileRes *compile.Result) error {
    66  	switch {
    67  	case compileRes == nil:
    68  		return errors.New("failed to compile, check schemas")
    69  	case len(compileRes.NotFoundDeps) > 0:
    70  		return errors.New("failed to compile, missing dependencies. Run 'vpm tidy'")
    71  	default:
    72  		return nil
    73  	}
    74  }
    75  
    76  func build(compileRes *compile.Result, params *vpmParams) error {
    77  	// temp directory to save the build info: vsql files, wasm files
    78  	tempBuildInfoDir := filepath.Join(os.TempDir(), uuid.New().String(), buildDirName)
    79  	if err := os.MkdirAll(tempBuildInfoDir, coreutils.FileMode_rwxrwxrwx); err != nil {
    80  		return err
    81  	}
    82  	// create temp build info directory along with vsql and wasm files
    83  	if err := buildDir(compileRes.PkgFiles, tempBuildInfoDir); err != nil {
    84  		return err
    85  	}
    86  	// set the path to the output archive, e.g. app.var
    87  	archiveName := params.Output
    88  	if archiveName == "" {
    89  		archiveName = filepath.Base(params.Dir)
    90  	}
    91  	if !strings.HasSuffix(archiveName, ".var") {
    92  		archiveName += ".var"
    93  	}
    94  	archivePath := filepath.Join(params.Dir, archiveName)
    95  
    96  	// zip build info directory along with vsql and wasm files
    97  	return coreutils.Zip(archivePath, tempBuildInfoDir)
    98  }
    99  
   100  // buildDir creates a directory structure with vsql and wasm files
   101  func buildDir(pkgFiles packageFiles, buildDirPath string) error {
   102  	for qpn, files := range pkgFiles {
   103  		pkgBuildDir := filepath.Join(buildDirPath, qpn)
   104  		if err := os.MkdirAll(pkgBuildDir, coreutils.FileMode_rwxrwxrwx); err != nil {
   105  			return err
   106  		}
   107  
   108  		for _, file := range files {
   109  			// copy vsql files
   110  			base := filepath.Base(file)
   111  			fileNameExtensionless := base[:len(base)-len(filepath.Ext(base))]
   112  			if err := coreutils.CopyFile(file, pkgBuildDir, coreutils.WithNewName(fileNameExtensionless+parser.VSqlExt)); err != nil {
   113  				return fmt.Errorf(errFmtCopyFile, file, err)
   114  			}
   115  
   116  			// building wasm files: if wasm directory exists, build wasm file and copy it to the temp build directory
   117  			fileDir := filepath.Dir(file)
   118  			wasmDirPath := filepath.Join(fileDir, wasmDirName)
   119  			exists, err := coreutils.Exists(wasmDirPath)
   120  			if err != nil {
   121  				return err
   122  			}
   123  			if exists {
   124  				appName := filepath.Base(fileDir)
   125  				wasmFilePath, err := execTinyGoBuild(wasmDirPath, appName)
   126  				if err != nil {
   127  					return err
   128  				}
   129  				if err := coreutils.CopyFile(wasmFilePath, pkgBuildDir); err != nil {
   130  					return fmt.Errorf(errFmtCopyFile, wasmFilePath, err)
   131  				}
   132  				// remove the wasm file after copying it to the build directory
   133  				if err := os.Remove(wasmFilePath); err != nil {
   134  					return err
   135  				}
   136  			}
   137  
   138  		}
   139  	}
   140  	return nil
   141  }
   142  
   143  // execTinyGoBuild builds the project using tinygo and returns the path to the resulting wasm file
   144  func execTinyGoBuild(dir, appName string) (wasmFilePath string, err error) {
   145  	var stdout io.Writer
   146  	if logger.IsVerbose() {
   147  		stdout = os.Stdout
   148  	}
   149  
   150  	wasmFileName := appName + ".wasm"
   151  	if err := new(exec.PipedExec).Command("tinygo", "build", "--no-debug", "-o", wasmFileName, "-scheduler=none", "-opt=2", "-gc=leaking", "-target=wasi", ".").WorkingDir(dir).Run(stdout, os.Stderr); err != nil {
   152  		return "", err
   153  	}
   154  	return filepath.Join(dir, wasmFileName), nil
   155  }