github.com/yourbase/yb@v0.7.1/cmd/yb/init.go (about)

     1  // Copyright 2020 YourBase Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //		 https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // SPDX-License-Identifier: Apache-2.0
    16  
    17  package main
    18  
    19  import (
    20  	"context"
    21  	"embed"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"io/fs"
    26  	"io/ioutil"
    27  	"os"
    28  	"path/filepath"
    29  	"strings"
    30  	"time"
    31  
    32  	"github.com/spf13/cobra"
    33  	"github.com/yourbase/yb"
    34  	"zombiezen.com/go/log"
    35  )
    36  
    37  type initCmd struct {
    38  	dir       string
    39  	language  string
    40  	outPath   string
    41  	overwrite bool
    42  	quiet     bool
    43  }
    44  
    45  func newInitCmd() *cobra.Command {
    46  	cmd := new(initCmd)
    47  	c := &cobra.Command{
    48  		Use:   "init [flags] [DIR]",
    49  		Short: "Initialize directory",
    50  		Long:  "Initialize package directory with a " + yb.PackageConfigFilename + " file.",
    51  		Args:  cobra.MaximumNArgs(1),
    52  		RunE: func(cc *cobra.Command, args []string) error {
    53  			if len(args) == 1 {
    54  				cmd.dir = args[0]
    55  			}
    56  			return cmd.run(cc.Context())
    57  		},
    58  		DisableFlagsInUseLine: true,
    59  	}
    60  	c.Flags().StringVar(&cmd.language, "lang", langDetectFlagValue, "Programming language to create for")
    61  	c.Flags().StringVarP(&cmd.outPath, "output", "o", "", "Output file (- for stdout)")
    62  	c.Flags().BoolVarP(&cmd.overwrite, "force", "f", false, "Overwrite existing file")
    63  	c.Flags().BoolVarP(&cmd.quiet, "quiet", "q", false, "Suppress non-error output")
    64  	return c
    65  }
    66  
    67  func (cmd *initCmd) run(ctx context.Context) (cmdErr error) {
    68  	const stdoutToken = "-"
    69  
    70  	if !cmd.quiet {
    71  		log.Infof(ctx, "Welcome to YourBase!")
    72  	}
    73  
    74  	// Find or create target directory.
    75  	dir, err := filepath.Abs(cmd.dir)
    76  	if err != nil {
    77  		return err
    78  	}
    79  	log.Debugf(ctx, "Directory: %s", dir)
    80  	if err := os.MkdirAll(dir, 0o777); err != nil {
    81  		return err
    82  	}
    83  
    84  	// Do a quick check to see if Docker works.
    85  	client, err := connectDockerClient(useContainer)
    86  	if err != nil {
    87  		return err
    88  	}
    89  	log.Debugf(ctx, "Checking Docker connection...")
    90  	pingCtx, cancelPing := context.WithTimeout(ctx, 5*time.Second)
    91  	err = client.PingWithContext(pingCtx)
    92  	cancelPing()
    93  	if err != nil {
    94  		log.Warnf(ctx, "yb can't connect to Docker. You might encounter issues during build. Error: %v", err)
    95  	} else if !cmd.quiet {
    96  		log.Infof(ctx, "yb connected to Docker successfully!")
    97  	}
    98  
    99  	// Find the appropriate template.
   100  	language := cmd.language
   101  	if language == langDetectFlagValue {
   102  		if !cmd.quiet {
   103  			log.Infof(ctx, "Detecting your programming language...")
   104  		}
   105  		language, err = detectLanguage(ctx, dir)
   106  		if err != nil {
   107  			return err
   108  		}
   109  		if language == "" {
   110  			log.Warnf(ctx, "Unable to detect language; generating a generic build configuration.")
   111  		} else if !cmd.quiet {
   112  			log.Infof(ctx, "Found %s!", language)
   113  		}
   114  	}
   115  	templateData, err := packageConfigTemplate(language)
   116  	if err != nil {
   117  		return err
   118  	}
   119  
   120  	// Write template to requested file.
   121  	var out io.Writer = os.Stdout
   122  	outPath := cmd.outPath
   123  	if outPath != stdoutToken {
   124  		if outPath == "" {
   125  			outPath = filepath.Join(dir, yb.PackageConfigFilename)
   126  		}
   127  		if !cmd.quiet {
   128  			log.Infof(ctx, "Writing package configuration to %s", outPath)
   129  		}
   130  		flags := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
   131  		if !cmd.overwrite {
   132  			flags |= os.O_EXCL
   133  		}
   134  		f, err := os.OpenFile(outPath, flags, 0o666)
   135  		if err != nil {
   136  			return err
   137  		}
   138  		out = f
   139  		defer func() {
   140  			if closeErr := f.Close(); closeErr != nil {
   141  				if cmdErr == nil {
   142  					cmdErr = closeErr
   143  				} else {
   144  					log.Warnf(ctx, "%v", closeErr)
   145  				}
   146  			}
   147  		}()
   148  	}
   149  	if _, err := io.WriteString(out, templateData); err != nil {
   150  		return fmt.Errorf("write %s: %w", outPath, err)
   151  	}
   152  	if !cmd.quiet && filepath.Base(outPath) == yb.PackageConfigFilename {
   153  		log.Infof(ctx, "All done! Try running `yb build` to build your project.")
   154  		log.Infof(ctx, "Edit %s to configure your build process.", outPath)
   155  	}
   156  	return nil
   157  }
   158  
   159  const (
   160  	langGenericFlagValue = ""
   161  	langDetectFlagValue  = "auto"
   162  
   163  	langPythonFlagValue = "python"
   164  	langRubyFlagValue   = "ruby"
   165  	langGoFlagValue     = "go"
   166  )
   167  
   168  func detectLanguage(ctx context.Context, dir string) (string, error) {
   169  	infos, err := ioutil.ReadDir(dir)
   170  	if err != nil {
   171  		return "", fmt.Errorf("detect project language: %w", err)
   172  	}
   173  	detected := langGenericFlagValue
   174  	for _, info := range infos {
   175  		prevDetected := detected
   176  		switch name := info.Name(); {
   177  		case name == "go.mod" || strings.HasSuffix(name, ".go"):
   178  			detected = langGoFlagValue
   179  		case name == "requirements.txt" || strings.HasSuffix(name, ".py"):
   180  			detected = langPythonFlagValue
   181  		case name == "Gemfile" || strings.HasSuffix(name, ".rb"):
   182  			detected = langRubyFlagValue
   183  		default:
   184  			continue
   185  		}
   186  		if prevDetected != langGenericFlagValue && detected != prevDetected {
   187  			log.Debugf(ctx, "Detected both %s and %s; returning generic", detected, prevDetected)
   188  			return langGenericFlagValue, nil
   189  		}
   190  	}
   191  	return detected, nil
   192  }
   193  
   194  //go:embed init_templates/*.yml
   195  var packageConfigTemplateFiles embed.FS
   196  
   197  func packageConfigTemplate(name string) (string, error) {
   198  	path := "init_templates/" + name + ".yml"
   199  	if name == langGenericFlagValue {
   200  		path = "init_templates/generic.yml"
   201  	}
   202  	f, err := packageConfigTemplateFiles.Open(path)
   203  	if errors.Is(err, fs.ErrNotExist) {
   204  		return "", fmt.Errorf("unknown language %q", name)
   205  	}
   206  	if err != nil {
   207  		return "", fmt.Errorf("load template for language %q: %w", name, err)
   208  	}
   209  	defer f.Close()
   210  	out := new(strings.Builder)
   211  	if _, err := io.Copy(out, f); err != nil {
   212  		return "", fmt.Errorf("load template for language %q: %w", name, err)
   213  	}
   214  	return out.String(), nil
   215  }