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 }