pkg/devmode/devmode.go (101 lines of code) (raw):

// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package devmode contains helpers to configure Development Mode. package devmode import ( "fmt" "os" "path/filepath" "strings" "github.com/GoogleCloudPlatform/buildpacks/pkg/env" gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack" "github.com/buildpacks/libcnb/v2" ) const ( watchexecLayer = "watchexec" watchexecVersion = "1.12.0" watchexecURL = "https://github.com/watchexec/watchexec/releases/download/%[1]s/watchexec-%[1]s-x86_64-unknown-linux-gnu.tar.xz" scriptsLayer = "devmode_scripts" buildAndRun = "build_and_run.sh" versionKey = "version" // WatchAndRun is the name of the script that watches source files and runs the // build_and_run.sh script when those files change. WatchAndRun = "watch_and_run.sh" ) // SyncRule represents a sync rule. type SyncRule struct { // Src is a glob, and assumed to be a path relative to the user's workspace. Src string `toml:"src"` // Dest is the destination root folder where changed files are copied. // Relative directory structure is preserved while copying. Dest string `toml:"dest"` } // Enabled indicates that the builder is running in Development mode. func Enabled(ctx *gcp.Context) bool { enabled, err := env.IsDevMode() if err != nil { ctx.Warnf("Dev mode not enabled: %v", err) return false } return enabled } // metadata represents metadata stored for a devmode layer. type metadata struct { WatchexecVersion string `toml:"version"` } // Config describes the dev mode for a given language. type Config struct { BuildCmd []string RunCmd []string // Ext lists the file extensions that trigger a restart. Ext []string } // AddFileWatcherProcess installs and configures a file watcher as the entrypoint. func AddFileWatcherProcess(ctx *gcp.Context, cfg Config) error { installFileWatcher(ctx) sl, err := ctx.Layer(scriptsLayer) if err != nil { return fmt.Errorf("creating %v layer: %w", scriptsLayer, err) } writeBuildAndRunScript(ctx, sl, cfg) // Override the web process. ctx.AddWebProcess([]string{WatchAndRun}) return nil } // writeBuildAndRunScript writes the contents of a file that builds code and then runs the resulting program func writeBuildAndRunScript(ctx *gcp.Context, sl *libcnb.Layer, cfg Config) error { sl.Launch = true binDir := filepath.Join(sl.Path, "bin") if err := ctx.MkdirAll(binDir, 0755); err != nil { return err } var cmd []string if cfg.BuildCmd != nil { cmd = append(cmd, strings.Join(cfg.BuildCmd, " ")) } if cfg.RunCmd != nil { cmd = append(cmd, strings.Join(cfg.RunCmd, " ")) } c := fmt.Sprintf("#!/bin/sh\n%s", strings.Join(cmd, " && ")) br := filepath.Join(binDir, buildAndRun) if err := ctx.WriteFile(br, []byte(c), os.FileMode(0755)); err != nil { return err } c = fmt.Sprintf("#!/bin/sh\nwatchexec -r -e %s %s", strings.Join(cfg.Ext, ","), br) wr := filepath.Join(binDir, WatchAndRun) if err := ctx.WriteFile(wr, []byte(c), os.FileMode(0755)); err != nil { return err } return nil } // installFileWatcher installs the `watchexec` file watcher. func installFileWatcher(ctx *gcp.Context) error { wxl, err := ctx.Layer(watchexecLayer, gcp.CacheLayer, gcp.LaunchLayer) if err != nil { return fmt.Errorf("creating %v layer: %w", watchexecLayer, err) } // Check metadata layer to see if correct version of watchexec is already installed. metaWatchexecVersion := ctx.GetMetadata(wxl, versionKey) if metaWatchexecVersion == watchexecVersion { ctx.CacheHit(watchexecLayer) } else { ctx.CacheMiss(watchexecLayer) // Clear layer data to avoid files from multiple versions of watchexec. if err := ctx.ClearLayer(wxl); err != nil { return fmt.Errorf("clearing layer %q: %w", wxl.Name, err) } binDir := filepath.Join(wxl.Path, "bin") if err := ctx.MkdirAll(binDir, 0755); err != nil { return err } // Download and install watchexec in layer. ctx.Logf("Installing watchexec v%s", watchexecVersion) archiveURL := fmt.Sprintf(watchexecURL, watchexecVersion) command := fmt.Sprintf("curl --fail --show-error --silent --location --retry 3 %s | tar xJ --directory %s --strip-components=1 --wildcards \"*watchexec\"", archiveURL, binDir) if _, err := ctx.Exec([]string{"bash", "-c", command}, gcp.WithUserAttribution); err != nil { return err } ctx.SetMetadata(wxl, versionKey, watchexecVersion) } return nil }