Skip to content

Commit

Permalink
RSDK-8906: Create golang stubs and template (#4455)
Browse files Browse the repository at this point in the history
  • Loading branch information
jckras authored Oct 25, 2024
1 parent 517e861 commit 8209427
Show file tree
Hide file tree
Showing 18 changed files with 757 additions and 75 deletions.
186 changes: 134 additions & 52 deletions cli/module_generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,51 +22,30 @@ import (
"go.viam.com/utils"
"golang.org/x/text/cases"
"golang.org/x/text/language"

"go.viam.com/rdk/cli/module_generate/common"
gen "go.viam.com/rdk/cli/module_generate/scripts"
)

//go:embed module_generate/scripts
var scripts embed.FS

//go:embed all:module_generate/templates/*
//go:embed all:module_generate/_templates/*
var templates embed.FS

const (
version = "0.1.0"
basePath = "module_generate"
templatePrefix = "tmpl-"
python = "python"
golang = "go"
)

var (
scriptsPath = filepath.Join(basePath, "scripts")
templatesPath = filepath.Join(basePath, "templates")
templatesPath = filepath.Join(basePath, "_templates")
)

// module contains the necessary information to fill out template files.
type moduleInputs struct {
ModuleName string `json:"module_name"`
IsPublic bool `json:"-"`
Namespace string `json:"namespace"`
OrgID string `json:"-"`
Language string `json:"language"`
Resource string `json:"-"`
ResourceType string `json:"resource_type"`
ResourceSubtype string `json:"resource_subtype"`
ModelName string `json:"model_name"`
EnableCloudBuild bool `json:"enable_cloud_build"`
RegisterOnApp bool `json:"-"`
GeneratorVersion string `json:"generator_version"`
GeneratedOn time.Time `json:"generated_on"`

ModulePascal string `json:"-"`
API string `json:"-"`
ResourceSubtypePascal string `json:"-"`
ModelPascal string `json:"-"`
ModelTriple string `json:"-"`

SDKVersion string `json:"-"`
}

// GenerateModuleAction runs the module generate cli and generates necessary module templates based on user input.
func GenerateModuleAction(cCtx *cli.Context) error {
c, err := newViamClient(cCtx)
Expand All @@ -77,12 +56,12 @@ func GenerateModuleAction(cCtx *cli.Context) error {
}

func (c *viamClient) generateModuleAction(cCtx *cli.Context) error {
var newModule *moduleInputs
var newModule *common.ModuleInputs
var err error
resourceType := cCtx.String(moduleFlagResourceType)
resourceSubtype := cCtx.String(moduleFlagResourceSubtype)
if resourceSubtype != "" && resourceType != "" {
newModule = &moduleInputs{
newModule = &common.ModuleInputs{
ModuleName: "my-module",
IsPublic: false,
Namespace: "my-org",
Expand All @@ -104,6 +83,7 @@ func (c *viamClient) generateModuleAction(cCtx *cli.Context) error {

SDKVersion: "0.0.0",
}
populateAdditionalInfo(newModule)
} else {
newModule, err = promptUser()
if err != nil {
Expand Down Expand Up @@ -198,9 +178,9 @@ func (c *viamClient) generateModuleAction(cCtx *cli.Context) error {
}

// Prompt the user for information regarding the module they want to create
// returns the moduleInputs struct that contains the information the user entered.
func promptUser() (*moduleInputs, error) {
var newModule moduleInputs
// returns the common.ModuleInputs struct that contains the information the user entered.
func promptUser() (*common.ModuleInputs, error) {
var newModule common.ModuleInputs
form := huh.NewForm(
huh.NewGroup(
huh.NewNote().
Expand Down Expand Up @@ -229,7 +209,7 @@ func promptUser() (*moduleInputs, error) {
Title("Specify the language for the module:").
Options(
huh.NewOption("Python", python),
// huh.NewOption("Go", "go"),
huh.NewOption("Go", golang),
).
Value(&newModule.Language),
huh.NewConfirm().
Expand Down Expand Up @@ -307,7 +287,7 @@ func promptUser() (*moduleInputs, error) {
return &newModule, nil
}

func wrapResolveOrg(cCtx *cli.Context, c *viamClient, newModule *moduleInputs) error {
func wrapResolveOrg(cCtx *cli.Context, c *viamClient, newModule *common.ModuleInputs) error {
match, err := regexp.MatchString("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", newModule.Namespace)
if !match || err != nil {
// If newModule.Namespace is NOT a UUID
Expand All @@ -328,7 +308,7 @@ func wrapResolveOrg(cCtx *cli.Context, c *viamClient, newModule *moduleInputs) e
return nil
}

func catchResolveOrgErr(cCtx *cli.Context, c *viamClient, newModule *moduleInputs, caughtErr error) error {
func catchResolveOrgErr(cCtx *cli.Context, c *viamClient, newModule *common.ModuleInputs, caughtErr error) error {
if strings.Contains(caughtErr.Error(), "not logged in") || strings.Contains(caughtErr.Error(), "error while refreshing token") {
originalWriter := cCtx.App.Writer
cCtx.App.Writer = io.Discard
Expand All @@ -347,19 +327,28 @@ func catchResolveOrgErr(cCtx *cli.Context, c *viamClient, newModule *moduleInput
}

// populateAdditionalInfo fills in additional info in newModule.
func populateAdditionalInfo(newModule *moduleInputs) {
func populateAdditionalInfo(newModule *common.ModuleInputs) {
newModule.GeneratedOn = time.Now().UTC()
newModule.GeneratorVersion = version
newModule.ResourceSubtype = strings.Split(newModule.Resource, " ")[0]
newModule.ResourceType = strings.Split(newModule.Resource, " ")[1]

titleCaser := cases.Title(language.Und)
replacer := strings.NewReplacer("_", "", "-", "")
newModule.ModulePascal = replacer.Replace(titleCaser.String(newModule.ModuleName))
replacer := strings.NewReplacer("_", " ", "-", " ")
spaceReplacer := strings.NewReplacer(" ", "", "_", "", "-", "")
newModule.ModulePascal = spaceReplacer.Replace(titleCaser.String(replacer.Replace(newModule.ModuleName)))
newModule.ModuleCamel = strings.ToLower(string(newModule.ModulePascal[0])) + newModule.ModulePascal[1:]
newModule.ModuleLowercase = strings.ToLower(newModule.ModulePascal)
newModule.API = fmt.Sprintf("rdk:%s:%s", newModule.ResourceType, newModule.ResourceSubtype)
newModule.ResourceSubtypePascal = replacer.Replace(titleCaser.String(newModule.ResourceSubtype))
newModule.ModelPascal = replacer.Replace(titleCaser.String(newModule.ModelName))
newModule.ResourceSubtypePascal = spaceReplacer.Replace(titleCaser.String(replacer.Replace(newModule.ResourceSubtype)))
if newModule.Language == golang {
newModule.ResourceSubtype = spaceReplacer.Replace(newModule.ResourceSubtype)
}
newModule.ResourceTypePascal = spaceReplacer.Replace(titleCaser.String(replacer.Replace(newModule.ResourceType)))
newModule.ModelPascal = spaceReplacer.Replace(titleCaser.String(replacer.Replace(newModule.ModelName)))
newModule.ModelTriple = fmt.Sprintf("%s:%s:%s", newModule.Namespace, newModule.ModuleName, newModule.ModelName)
newModule.ModelCamel = strings.ToLower(string(newModule.ModelPascal[0])) + newModule.ModelPascal[1:]
newModule.ModelLowercase = strings.ToLower(newModule.ModelPascal)
}

// Creates a new directory with moduleName.
Expand All @@ -372,7 +361,8 @@ func setupDirectories(c *cli.Context, moduleName string) error {
return nil
}

func renderCommonFiles(c *cli.Context, module moduleInputs) error {
func renderCommonFiles(c *cli.Context, module common.ModuleInputs) error {
debugf(c.App.Writer, c.Bool(debugFlag), module.ResourceSubtypePascal)
debugf(c.App.Writer, c.Bool(debugFlag), "Rendering common files")

// Render .viam-gen-info
Expand Down Expand Up @@ -508,7 +498,7 @@ func copyLanguageTemplate(c *cli.Context, language, moduleName string) error {
}

// Render all the files in the new directory.
func renderTemplate(c *cli.Context, module moduleInputs) error {
func renderTemplate(c *cli.Context, module common.ModuleInputs) error {
debugf(c.App.Writer, c.Bool(debugFlag), "Rendering template files")
languagePath := filepath.Join(templatesPath, module.Language)
tempDir, err := fs.Sub(templates, languagePath)
Expand Down Expand Up @@ -553,17 +543,84 @@ func renderTemplate(c *cli.Context, module moduleInputs) error {
}

// Generate stubs for the resource.
func generateStubs(c *cli.Context, module moduleInputs) error {
func generateStubs(c *cli.Context, module common.ModuleInputs) error {
debugf(c.App.Writer, c.Bool(debugFlag), "Generating %s stubs", module.Language)
switch module.Language {
case python:
return generatePythonStubs(module)
case golang:
return generateGolangStubs(module)
default:
return errors.Errorf("cannot generate stubs for language %s", module.Language)
}
}

func generatePythonStubs(module moduleInputs) error {
func generateGolangStubs(module common.ModuleInputs) error {
out, err := gen.RenderGoTemplates(module)
if err != nil {
return errors.Wrap(err, "cannot generate go stubs -- generator script encountered an error")
}
modulePath := filepath.Join(module.ModuleName, "models", "module.go")
//nolint:gosec
moduleFile, err := os.Create(modulePath)
if err != nil {
return errors.Wrap(err, "cannot generate go stubs -- unable to open file")
}
defer utils.UncheckedErrorFunc(moduleFile.Close)
_, err = moduleFile.Write(out)
if err != nil {
return errors.Wrap(err, "cannot generate go stubs -- unable to write to file")
}

// run goimports on module file out here
err = runGoImports(moduleFile)
if err != nil {
return errors.Wrap(err, "cannot generate go stubs -- unable to sort imports")
}

return nil
}

// run goimports to remove unused imports and add necessary imports.
func runGoImports(moduleFile *os.File) error {
// check if the gopath is set
goPath, err := checkGoPath()
if err != nil {
return err
}

// check if goimports exists in the bin directory
goImportsPath := fmt.Sprintf("%s/bin/goimports", goPath)
if _, err := os.Stat(goImportsPath); os.IsNotExist(err) {
// installing goimports
installCmd := exec.Command("go", "install", "golang.org/x/tools/cmd/goimports@latest")
if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install goimports: %w", err)
}
}

// goimports is installed. Run goimport on the module file
//nolint:gosec
formatCmd := exec.Command(goImportsPath, "-w", moduleFile.Name())
_, err = formatCmd.Output()
if err != nil {
return fmt.Errorf("failed to run goimports: %w", err)
}
return err
}

func checkGoPath() (string, error) {
goPathCmd := exec.Command("go", "env", "GOPATH")
goPathBytes, err := goPathCmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get GOPATH: %w", err)
}
goPath := strings.TrimSpace(string(goPathBytes))

return goPath, err
}

func generatePythonStubs(module common.ModuleInputs) error {
venvName := ".venv"
cmd := exec.Command("python3", "--version")
_, err := cmd.Output()
Expand Down Expand Up @@ -606,8 +663,13 @@ func generatePythonStubs(module moduleInputs) error {

func getLatestSDKTag(c *cli.Context, language string) (string, error) {
var repo string
if language == python {
switch language {
case python:
repo = "viam-python-sdk"
case golang:
repo = "rdk"
default:
return "", errors.New("cannot produce template -- unexpected language was selected")
}
debugf(c.App.Writer, c.Bool(debugFlag), "Getting the latest release tag for %s", repo)
url := fmt.Sprintf("https://api.github.com/repos/viamrobotics/%s/releases", repo)
Expand Down Expand Up @@ -640,9 +702,10 @@ func getLatestSDKTag(c *cli.Context, language string) (string, error) {
return version, nil
}

func generateCloudBuild(c *cli.Context, module moduleInputs) error {
debugf(c.App.Writer, c.Bool(debugFlag), "Setting cloud build functionality to %t", module.EnableCloudBuild)
if module.Language == python {
func generateCloudBuild(c *cli.Context, module common.ModuleInputs) error {
debugf(c.App.Writer, c.Bool(debugFlag), "Setting cloud build functionality to %s", module.EnableCloudBuild)
switch module.Language {
case python:
if module.EnableCloudBuild {
err := os.Remove(filepath.Join(module.ModuleName, "run.sh"))
if err != nil {
Expand All @@ -654,11 +717,18 @@ func generateCloudBuild(c *cli.Context, module moduleInputs) error {
return err
}
}
case golang:
if module.EnableCloudBuild {
err := os.Remove(filepath.Join(module.ModuleName, "run.sh"))
if err != nil {
return err
}
}
}
return nil
}

func createModuleAndManifest(cCtx *cli.Context, c *viamClient, module moduleInputs) error {
func createModuleAndManifest(cCtx *cli.Context, c *viamClient, module common.ModuleInputs) error {
var moduleID moduleID
if module.RegisterOnApp {
debugf(cCtx.App.Writer, cCtx.Bool(debugFlag), "Registering module with Viam")
Expand All @@ -683,7 +753,7 @@ func createModuleAndManifest(cCtx *cli.Context, c *viamClient, module moduleInpu
}

// Create the meta.json manifest.
func renderManifest(c *cli.Context, moduleID string, module moduleInputs) error {
func renderManifest(c *cli.Context, moduleID string, module common.ModuleInputs) error {
debugf(c.App.Writer, c.Bool(debugFlag), "Rendering module manifest")

visibility := moduleVisibilityPrivate
Expand All @@ -700,8 +770,8 @@ func renderManifest(c *cli.Context, moduleID string, module moduleInputs) error
{API: module.API, Model: module.ModelTriple},
},
}

if module.Language == python {
switch module.Language {
case python:
if module.EnableCloudBuild {
manifest.Build = &manifestBuildInfo{
Setup: "./setup.sh",
Expand All @@ -713,6 +783,18 @@ func renderManifest(c *cli.Context, moduleID string, module moduleInputs) error
} else {
manifest.Entrypoint = "./run.sh"
}
case golang:
if module.EnableCloudBuild {
manifest.Build = &manifestBuildInfo{
Setup: "make setup",
Build: "make module.tar.gz",
Path: "bin/module.tar.gz",
Arch: []string{"linux/amd64", "linux/arm64"},
}
manifest.Entrypoint = fmt.Sprintf("bin/%s", module.ModuleName)
} else {
manifest.Entrypoint = "./run.sh"
}
}

if err := writeManifest(filepath.Join(module.ModuleName, defaultManifestFilename), manifest); err != nil {
Expand Down
29 changes: 29 additions & 0 deletions cli/module_generate/_templates/go/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# This .gitignore file is based on the Go.gitignore from GitHub:
# https://github.com/github/gitignore/blob/main/Go.gitignore
#
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work
go.work.sum

# Build outputs
bin
*tar.gz
Loading

0 comments on commit 8209427

Please sign in to comment.