summaryrefslogtreecommitdiffstats
path: root/commands/hugobuilder.go
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2023-01-04 18:24:36 +0100
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2023-05-16 18:01:29 +0200
commit241b21b0fd34d91fccb2ce69874110dceae6f926 (patch)
treed4e0118eac7e9c42f065815447a70805f8d6ad3e /commands/hugobuilder.go
parent6aededf6b42011c3039f5f66487a89a8dd65e0e7 (diff)
Create a struct with all of Hugo's config options
Primary motivation is documentation, but it will also hopefully simplify the code. Also, * Lower case the default output format names; this is in line with the custom ones (map keys) and how it's treated all the places. This avoids doing `stringds.EqualFold` everywhere. Closes #10896 Closes #10620
Diffstat (limited to 'commands/hugobuilder.go')
-rw-r--r--commands/hugobuilder.go993
1 files changed, 993 insertions, 0 deletions
diff --git a/commands/hugobuilder.go b/commands/hugobuilder.go
new file mode 100644
index 000000000..7c6dbee35
--- /dev/null
+++ b/commands/hugobuilder.go
@@ -0,0 +1,993 @@
+// Copyright 2023 The Hugo Authors. All rights reserved.
+//
+// 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 commands
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "runtime"
+ "runtime/pprof"
+ "runtime/trace"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/bep/simplecobra"
+ "github.com/fsnotify/fsnotify"
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/htime"
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/common/terminal"
+ "github.com/gohugoio/hugo/common/types"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/hugolib/filesystems"
+ "github.com/gohugoio/hugo/livereload"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/watcher"
+ "github.com/spf13/fsync"
+ "golang.org/x/sync/errgroup"
+ "golang.org/x/sync/semaphore"
+)
+
+type hugoBuilder struct {
+ r *rootCommand
+
+ cunfMu sync.Mutex
+ conf_ *commonConfig
+
+ // May be nil.
+ s *serverCommand
+
+ // Currently only set when in "fast render mode".
+ changeDetector *fileChangeDetector
+ visitedURLs *types.EvictingStringQueue
+
+ fullRebuildSem *semaphore.Weighted
+ debounce func(f func())
+
+ onConfigLoaded func(reloaded bool) error
+
+ fastRenderMode bool
+ buildWatch bool
+ showErrorInBrowser bool
+
+ errState hugoBuilderErrState
+}
+
+func (c *hugoBuilder) conf() *commonConfig {
+ c.cunfMu.Lock()
+ defer c.cunfMu.Unlock()
+ return c.conf_
+}
+
+func (c *hugoBuilder) setConf(conf *commonConfig) {
+ c.cunfMu.Lock()
+ defer c.cunfMu.Unlock()
+ c.conf_ = conf
+}
+
+type hugoBuilderErrState struct {
+ mu sync.Mutex
+ paused bool
+ builderr error
+ waserr bool
+}
+
+func (e *hugoBuilderErrState) setPaused(p bool) {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ e.paused = p
+}
+
+func (e *hugoBuilderErrState) isPaused() bool {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ return e.paused
+}
+
+func (e *hugoBuilderErrState) setBuildErr(err error) {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ e.builderr = err
+}
+
+func (e *hugoBuilderErrState) buildErr() error {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ return e.builderr
+}
+
+func (e *hugoBuilderErrState) setWasErr(w bool) {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ e.waserr = w
+}
+
+func (e *hugoBuilderErrState) wasErr() bool {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ return e.waserr
+}
+
+func (c *hugoBuilder) errCount() int {
+ return int(c.r.logger.LogCounters().ErrorCounter.Count())
+}
+
+// getDirList provides NewWatcher() with a list of directories to watch for changes.
+func (c *hugoBuilder) getDirList() ([]string, error) {
+ var filenames []string
+
+ walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error {
+ if err != nil {
+ c.r.logger.Errorln("walker: ", err)
+ return nil
+ }
+
+ if fi.IsDir() {
+ if fi.Name() == ".git" ||
+ fi.Name() == "node_modules" || fi.Name() == "bower_components" {
+ return filepath.SkipDir
+ }
+
+ filenames = append(filenames, fi.Meta().Filename)
+ }
+
+ return nil
+ }
+
+ watchFiles := c.hugo().PathSpec.BaseFs.WatchDirs()
+ for _, fi := range watchFiles {
+ if !fi.IsDir() {
+ filenames = append(filenames, fi.Meta().Filename)
+ continue
+ }
+
+ w := hugofs.NewWalkway(hugofs.WalkwayConfig{Logger: c.r.logger, Info: fi, WalkFn: walkFn})
+ if err := w.Walk(); err != nil {
+ c.r.logger.Errorln("walker: ", err)
+ }
+ }
+
+ filenames = helpers.UniqueStringsSorted(filenames)
+
+ return filenames, nil
+}
+
+func (c *hugoBuilder) initCPUProfile() (func(), error) {
+ if c.r.cpuprofile == "" {
+ return nil, nil
+ }
+
+ f, err := os.Create(c.r.cpuprofile)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create CPU profile: %w", err)
+ }
+ if err := pprof.StartCPUProfile(f); err != nil {
+ return nil, fmt.Errorf("failed to start CPU profile: %w", err)
+ }
+ return func() {
+ pprof.StopCPUProfile()
+ f.Close()
+ }, nil
+}
+
+func (c *hugoBuilder) initMemProfile() {
+ if c.r.memprofile == "" {
+ return
+ }
+
+ f, err := os.Create(c.r.memprofile)
+ if err != nil {
+ c.r.logger.Errorf("could not create memory profile: ", err)
+ }
+ defer f.Close()
+ runtime.GC() // get up-to-date statistics
+ if err := pprof.WriteHeapProfile(f); err != nil {
+ c.r.logger.Errorf("could not write memory profile: ", err)
+ }
+}
+
+func (c *hugoBuilder) initMemTicker() func() {
+ memticker := time.NewTicker(5 * time.Second)
+ quit := make(chan struct{})
+ printMem := func() {
+ var m runtime.MemStats
+ runtime.ReadMemStats(&m)
+ fmt.Printf("\n\nAlloc = %v\nTotalAlloc = %v\nSys = %v\nNumGC = %v\n\n", formatByteCount(m.Alloc), formatByteCount(m.TotalAlloc), formatByteCount(m.Sys), m.NumGC)
+ }
+
+ go func() {
+ for {
+ select {
+ case <-memticker.C:
+ printMem()
+ case <-quit:
+ memticker.Stop()
+ printMem()
+ return
+ }
+ }
+ }()
+
+ return func() {
+ close(quit)
+ }
+}
+
+func (c *hugoBuilder) initMutexProfile() (func(), error) {
+ if c.r.mutexprofile == "" {
+ return nil, nil
+ }
+
+ f, err := os.Create(c.r.mutexprofile)
+ if err != nil {
+ return nil, err
+ }
+
+ runtime.SetMutexProfileFraction(1)
+
+ return func() {
+ pprof.Lookup("mutex").WriteTo(f, 0)
+ f.Close()
+ }, nil
+}
+
+func (c *hugoBuilder) initProfiling() (func(), error) {
+ stopCPUProf, err := c.initCPUProfile()
+ if err != nil {
+ return nil, err
+ }
+
+ stopMutexProf, err := c.initMutexProfile()
+ if err != nil {
+ return nil, err
+ }
+
+ stopTraceProf, err := c.initTraceProfile()
+ if err != nil {
+ return nil, err
+ }
+
+ var stopMemTicker func()
+ if c.r.printm {
+ stopMemTicker = c.initMemTicker()
+ }
+
+ return func() {
+ c.initMemProfile()
+
+ if stopCPUProf != nil {
+ stopCPUProf()
+ }
+ if stopMutexProf != nil {
+ stopMutexProf()
+ }
+
+ if stopTraceProf != nil {
+ stopTraceProf()
+ }
+
+ if stopMemTicker != nil {
+ stopMemTicker()
+ }
+ }, nil
+}
+
+func (c *hugoBuilder) initTraceProfile() (func(), error) {
+ if c.r.traceprofile == "" {
+ return nil, nil
+ }
+
+ f, err := os.Create(c.r.traceprofile)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create trace file: %w", err)
+ }
+
+ if err := trace.Start(f); err != nil {
+ return nil, fmt.Errorf("failed to start trace: %w", err)
+ }
+
+ return func() {
+ trace.Stop()
+ f.Close()
+ }, nil
+}
+
+// newWatcher creates a new watcher to watch filesystem events.
+func (c *hugoBuilder) newWatcher(pollIntervalStr string, dirList ...string) (*watcher.Batcher, error) {
+ staticSyncer := &staticSyncer{c: c}
+
+ var pollInterval time.Duration
+ poll := pollIntervalStr != ""
+ if poll {
+ pollInterval, err := types.ToDurationE(pollIntervalStr)
+ if err != nil {
+ return nil, fmt.Errorf("invalid value for flag poll: %s", err)
+ }
+ c.r.logger.Printf("Use watcher with poll interval %v", pollInterval)
+ }
+
+ if pollInterval == 0 {
+ pollInterval = 500 * time.Millisecond
+ }
+
+ watcher, err := watcher.New(500*time.Millisecond, pollInterval, poll)
+ if err != nil {
+ return nil, err
+ }
+
+ spec := c.hugo().Deps.SourceSpec
+
+ for _, d := range dirList {
+ if d != "" {
+ if spec.IgnoreFile(d) {
+ continue
+ }
+ _ = watcher.Add(d)
+ }
+ }
+
+ // Identifies changes to config (config.toml) files.
+ configSet := make(map[string]bool)
+ configFiles := c.conf().configs.LoadingInfo.ConfigFiles
+
+ c.r.logger.Println("Watching for config changes in", strings.Join(configFiles, ", "))
+ for _, configFile := range configFiles {
+ watcher.Add(configFile)
+ configSet[configFile] = true
+ }
+
+ go func() {
+ for {
+ select {
+ case evs := <-watcher.Events:
+ unlock, err := c.hugo().LockBuild()
+ if err != nil {
+ c.r.logger.Errorln("Failed to acquire a build lock: %s", err)
+ return
+ }
+ c.handleEvents(watcher, staticSyncer, evs, configSet)
+ if c.showErrorInBrowser && c.errCount() > 0 {
+ // Need to reload browser to show the error
+ livereload.ForceRefresh()
+ }
+ unlock()
+ case err := <-watcher.Errors():
+ if err != nil && !herrors.IsNotExist(err) {
+ c.r.logger.Errorln("Error while watching:", err)
+ }
+ }
+ }
+ }()
+
+ return watcher, nil
+}
+
+func (c *hugoBuilder) build() error {
+ stopProfiling, err := c.initProfiling()
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ if stopProfiling != nil {
+ stopProfiling()
+ }
+ }()
+
+ if err := c.fullBuild(false); err != nil {
+ return err
+ }
+
+ if !c.r.quiet {
+ c.r.Println()
+ c.hugo().PrintProcessingStats(os.Stdout)
+ c.r.Println()
+ }
+
+ return nil
+}
+
+func (c *hugoBuilder) buildSites(noBuildLock bool) (err error) {
+ return c.hugo().Build(hugolib.BuildCfg{NoBuildLock: noBuildLock})
+}
+
+func (c *hugoBuilder) copyStatic() (map[string]uint64, error) {
+ m, err := c.doWithPublishDirs(c.copyStaticTo)
+ if err == nil || herrors.IsNotExist(err) {
+ return m, nil
+ }
+ return m, err
+}
+
+func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
+ publishDir := helpers.FilePathSeparator
+
+ if sourceFs.PublishFolder != "" {
+ publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
+ }
+
+ fs := &countingStatFs{Fs: sourceFs.Fs}
+
+ syncer := fsync.NewSyncer()
+ syncer.NoTimes = c.conf().configs.Base.NoTimes
+ syncer.NoChmod = c.conf().configs.Base.NoChmod
+ syncer.ChmodFilter = chmodFilter
+ syncer.SrcFs = fs
+ syncer.DestFs = c.conf().fs.PublishDirStatic
+ // Now that we are using a unionFs for the static directories
+ // We can effectively clean the publishDir on initial sync
+ syncer.Delete = c.conf().configs.Base.CleanDestinationDir
+
+ if syncer.Delete {
+ c.r.logger.Infoln("removing all files from destination that don't exist in static dirs")
+
+ syncer.DeleteFilter = func(f os.FileInfo) bool {
+ return f.IsDir() && strings.HasPrefix(f.Name(), ".")
+ }
+ }
+ c.r.logger.Infoln("syncing static files to", publishDir)
+
+ // because we are using a baseFs (to get the union right).
+ // set sync src to root
+ err := syncer.Sync(publishDir, helpers.FilePathSeparator)
+ if err != nil {
+ return 0, err
+ }
+
+ // Sync runs Stat 3 times for every source file (which sounds much)
+ numFiles := fs.statCounter / 3
+
+ return numFiles, err
+}
+
+func (c *hugoBuilder) doWithPublishDirs(f func(sourceFs *filesystems.SourceFilesystem) (uint64, error)) (map[string]uint64, error) {
+ langCount := make(map[string]uint64)
+
+ staticFilesystems := c.hugo().BaseFs.SourceFilesystems.Static
+
+ if len(staticFilesystems) == 0 {
+ c.r.logger.Infoln("No static directories found to sync")
+ return langCount, nil
+ }
+
+ for lang, fs := range staticFilesystems {
+ cnt, err := f(fs)
+ if err != nil {
+ return langCount, err
+ }
+ if lang == "" {
+ // Not multihost
+ for _, l := range c.conf().configs.Languages {
+ langCount[l.Lang] = cnt
+ }
+ } else {
+ langCount[lang] = cnt
+ }
+ }
+
+ return langCount, nil
+}
+
+func (c *hugoBuilder) fullBuild(noBuildLock bool) error {
+ var (
+ g errgroup.Group
+ langCount map[string]uint64
+ )
+
+ if !c.r.quiet {
+ fmt.Println("Start building sites … ")
+ fmt.Println(hugo.BuildVersionString())
+ if terminal.IsTerminal(os.Stdout) {
+ defer func() {
+ fmt.Print(showCursor + clearLine)
+ }()
+ }
+ }
+
+ copyStaticFunc := func() error {
+ cnt, err := c.copyStatic()
+ if err != nil {
+ return fmt.Errorf("error copying static files: %w", err)
+ }
+ langCount = cnt
+ return nil
+ }
+ buildSitesFunc := func() error {
+ if err := c.buildSites(noBuildLock); err != nil {
+ return fmt.Errorf("error building site: %w", err)
+ }
+ return nil
+ }
+ // Do not copy static files and build sites in parallel if cleanDestinationDir is enabled.
+ // This flag deletes all static resources in /public folder that are missing in /static,
+ // and it does so at the end of copyStatic() call.
+ if c.conf().configs.Base.CleanDestinationDir {
+ if err := copyStaticFunc(); err != nil {
+ return err
+ }
+ if err := buildSitesFunc(); err != nil {
+ return err
+ }
+ } else {
+ g.Go(copyStaticFunc)
+ g.Go(buildSitesFunc)
+ if err := g.Wait(); err != nil {
+ return err
+ }
+ }
+
+ for _, s := range c.hugo().Sites {
+ s.ProcessingStats.Static = langCount[s.Language().Lang]
+ }
+
+ if c.r.gc {
+ count, err := c.hugo().GC()
+ if err != nil {
+ return err
+ }
+ for _, s := range c.hugo().Sites {
+ // We have no way of knowing what site the garbage belonged to.
+ s.ProcessingStats.Cleaned = uint64(count)
+ }
+ }
+
+ return nil
+}
+
+func (c *hugoBuilder) fullRebuild(changeType string) {
+ if changeType == configChangeGoMod {
+ // go.mod may be changed during the build itself, and
+ // we really want to prevent superfluous builds.
+ if !c.fullRebuildSem.TryAcquire(1) {
+ return
+ }
+ c.fullRebuildSem.Release(1)
+ }
+
+ c.fullRebuildSem.Acquire(context.Background(), 1)
+
+ go func() {
+ defer c.fullRebuildSem.Release(1)
+
+ c.printChangeDetected(changeType)
+
+ defer func() {
+ // Allow any file system events to arrive basimplecobra.
+ // This will block any rebuild on config changes for the
+ // duration of the sleep.
+ time.Sleep(2 * time.Second)
+ }()
+
+ defer c.r.timeTrack(time.Now(), "Rebuilt")
+
+ err := c.reloadConfig()
+ if err != nil {
+ // Set the processing on pause until the state is recovered.
+ c.errState.setPaused(true)
+ c.handleBuildErr(err, "Failed to reload config")
+ } else {
+ c.errState.setPaused(false)
+ }
+
+ if !c.errState.isPaused() {
+ _, err := c.copyStatic()
+ if err != nil {
+ c.r.logger.Errorln(err)
+ return
+ }
+ err = c.buildSites(false)
+ if err != nil {
+ c.r.logger.Errorln(err)
+ } else if c.s != nil && c.s.doLiveReload {
+ livereload.ForceRefresh()
+ }
+ }
+ }()
+}
+
+func (c *hugoBuilder) handleBuildErr(err error, msg string) {
+ c.errState.setBuildErr(err)
+ c.r.logger.Errorln(msg + ": " + cleanErrorLog(err.Error()))
+}
+
+func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
+ staticSyncer *staticSyncer,
+ evs []fsnotify.Event,
+ configSet map[string]bool) {
+ defer func() {
+ c.errState.setWasErr(false)
+ }()
+
+ var isHandled bool
+
+ for _, ev := range evs {
+ isConfig := configSet[ev.Name]
+ configChangeType := configChangeConfig
+ if isConfig {
+ if strings.Contains(ev.Name, "go.mod") {
+ configChangeType = configChangeGoMod
+ }
+ if strings.Contains(ev.Name, ".work") {
+ configChangeType = configChangeGoWork
+ }
+ }
+ if !isConfig {
+ // It may be one of the /config folders
+ dirname := filepath.Dir(ev.Name)
+ if dirname != "." && configSet[dirname] {
+ isConfig = true
+ }
+ }
+
+ if isConfig {
+ isHandled = true
+
+ if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
+ continue
+ }
+
+ if ev.Op&fsnotify.Remove == fsnotify.Remove || ev.Op&fsnotify.Rename == fsnotify.Rename {
+ configFiles := c.conf().configs.LoadingInfo.ConfigFiles
+ for _, configFile := range configFiles {
+ counter := 0
+ for watcher.Add(configFile) != nil {
+ counter++
+ if counter >= 100 {
+ break
+ }
+ time.Sleep(100 * time.Millisecond)
+ }
+ }
+ }
+
+ // Config file(s) changed. Need full rebuild.
+ c.fullRebuild(configChangeType)
+
+ return
+ }
+ }
+
+ if isHandled {
+ return
+ }
+
+ if c.errState.isPaused() {
+ // Wait for the server to get into a consistent state before
+ // we continue with processing.
+ return
+ }
+
+ if len(evs) > 50 {
+ // This is probably a mass edit of the content dir.
+ // Schedule a full rebuild for when it slows down.
+ c.debounce(func() {
+ c.fullRebuild("")
+ })
+ return
+ }
+
+ c.r.logger.Infoln("Received System Events:", evs)
+
+ staticEvents := []fsnotify.Event{}
+ dynamicEvents := []fsnotify.Event{}
+
+ filtered := []fsnotify.Event{}
+ for _, ev := range evs {
+ if c.hugo().ShouldSkipFileChangeEvent(ev) {
+ continue
+ }
+ // Check the most specific first, i.e. files.
+ contentMapped := c.hugo().ContentChanges.GetSymbolicLinkMappings(ev.Name)
+ if len(contentMapped) > 0 {
+ for _, mapped := range contentMapped {
+ filtered = append(filtered, fsnotify.Event{Name: mapped, Op: ev.Op})
+ }
+ continue
+ }
+
+ // Check for any symbolic directory mapping.
+
+ dir, name := filepath.Split(ev.Name)
+
+ contentMapped = c.hugo().ContentChanges.GetSymbolicLinkMappings(dir)
+
+ if len(contentMapped) == 0 {
+ filtered = append(filtered, ev)
+ continue
+ }
+
+ for _, mapped := range contentMapped {
+ mappedFilename := filepath.Join(mapped, name)
+ filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op})
+ }
+ }
+
+ evs = filtered
+
+ for _, ev := range evs {
+ ext := filepath.Ext(ev.Name)
+ baseName := filepath.Base(ev.Name)
+ istemp := strings.HasSuffix(ext, "~") ||
+ (ext == ".swp") || // vim
+ (ext == ".swx") || // vim
+ (ext == ".tmp") || // generic temp file
+ (ext == ".DS_Store") || // OSX Thumbnail
+ baseName == "4913" || // vim
+ strings.HasPrefix(ext, ".goutputstream") || // gnome
+ strings.HasSuffix(ext, "jb_old___") || // intelliJ
+ strings.HasSuffix(ext, "jb_tmp___") || // intelliJ
+ strings.HasSuffix(ext, "jb_bak___") || // intelliJ
+ strings.HasPrefix(ext, ".sb-") || // byword
+ strings.HasPrefix(baseName, ".#") || // emacs
+ strings.HasPrefix(baseName, "#") // emacs
+ if istemp {
+ continue
+ }
+ if c.hugo().Deps.SourceSpec.IgnoreFile(ev.Name) {
+ continue
+ }
+ // Sometimes during rm -rf operations a '"": REMOVE' is triggered. Just ignore these
+ if ev.Name == "" {
+ continue
+ }
+
+ // Write and rename operations are often followed by CHMOD.
+ // There may be valid use cases for rebuilding the site on CHMOD,
+ // but that will require more complex logic than this simple conditional.
+ // On OS X this seems to be related to Spotlight, see:
+ // https://github.com/go-fsnotify/fsnotify/issues/15
+ // A workaround is to put your site(s) on the Spotlight exception list,
+ // but that may be a little mysterious for most end users.
+ // So, for now, we skip reload on CHMOD.
+ // We do have to check for WRITE though. On slower laptops a Chmod
+ // could be aggregated with other important events, and we still want
+ // to rebuild on those
+ if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod {
+ continue
+ }
+
+ walkAdder := func(path string, f hugofs.FileMetaInfo, err error) error {
+ if f.IsDir() {
+ c.r.logger.Println("adding created directory to watchlist", path)
+ if err := watcher.Add(path); err != nil {
+ return err
+ }
+ } else if !staticSyncer.isStatic(path) {
+ // Hugo's rebuilding logic is entirely file based. When you drop a new folder into
+ // /content on OSX, the above logic will handle future watching of those files,
+ // but the initial CREATE is lost.
+ dynamicEvents = append(dynamicEvents, fsnotify.Event{Name: path, Op: fsnotify.Create})
+ }
+ return nil
+ }
+
+ // recursively add new directories to watch list
+ // When mkdir -p is used, only the top directory triggers an event (at least on OSX)
+ if ev.Op&fsnotify.Create == fsnotify.Create {
+ if s, err := c.conf().fs.Source.Stat(ev.Name); err == nil && s.Mode().IsDir() {
+ _ = helpers.SymbolicWalk(c.conf().fs.Source, ev.Name, walkAdder)
+ }
+ }
+
+ if staticSyncer.isStatic(ev.Name) {
+ staticEvents = append(staticEvents, ev)
+ } else {
+ dynamicEvents = append(dynamicEvents, ev)
+ }
+ }
+
+ if len(staticEvents) > 0 {
+ c.printChangeDetected("Static files")
+
+ if c.r.forceSyncStatic {
+ c.r.logger.Printf("Syncing all static files\n")
+ _, err := c.copyStatic()
+ if err != nil {
+ c.r.logger.Errorln("Error copying static files to publish dir:", err)
+ return
+ }
+ } else {
+ if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil {
+ c.r.logger.Errorln("Error syncing static files to publish dir:", err)
+ return
+ }
+ }
+
+ if c.s != nil && c.s.doLiveReload {
+ // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized
+
+ // force refresh when more than one file
+ if !c.errState.wasErr() && len(staticEvents) == 1 {
+ ev := staticEvents[0]
+ h := c.hugo()
+ path := h.BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name)
+ path = h.RelURL(helpers.ToSlashTrimLeading(path), false)
+
+ livereload.RefreshPath(path)
+ } else {
+ livereload.ForceRefresh()
+ }
+ }
+ }
+
+ if len(dynamicEvents) > 0 {
+ partitionedEvents := partitionDynamicEvents(
+ c.hugo().BaseFs.SourceFilesystems,
+ dynamicEvents)
+
+ onePageName := pickOneWriteOrCreatePath(partitionedEvents.ContentEvents)
+
+ c.printChangeDetected("")
+ c.changeDetector.PrepareNew()
+
+ func() {
+ defer c.r.timeTrack(time.Now(), "Total")
+ if err := c.rebuildSites(dynamicEvents); err != nil {
+ c.handleBuildErr(err, "Rebuild failed")
+ }
+ }()
+
+ if c.s != nil && c.s.doLiveReload {
+ if len(partitionedEvents.ContentEvents) == 0 && len(partitionedEvents.AssetEvents) > 0 {
+ if c.errState.wasErr() {
+ livereload.ForceRefresh()
+ return
+ }
+ changed := c.changeDetector.changed()
+ if c.changeDetector != nil && len(changed) == 0 {
+ // Nothing has changed.
+ return
+ } else if len(changed) == 1 {
+ pathToRefresh := c.hugo().PathSpec.RelURL(helpers.ToSlashTrimLeading(changed[0]), false)
+ livereload.RefreshPath(pathToRefresh)
+ } else {
+ livereload.ForceRefresh()
+ }
+ }
+
+ if len(partitionedEvents.ContentEvents) > 0 {
+ navigate := c.s != nil && c.s.navigateToChanged
+ // We have fetched the same page above, but it may have
+ // changed.
+ var p page.Page
+
+ if navigate {
+ if onePageName != "" {
+ p = c.hugo().GetContentPage(onePageName)
+ }
+ }
+
+ if p != nil {
+ livereload.NavigateToPathForPort(p.RelPermalink(), p.Site().ServerPort())
+ } else {
+ livereload.ForceRefresh()
+ }
+ }
+ }
+ }
+}
+
+func (c *hugoBuilder) hugo() *hugolib.HugoSites {
+ h, err := c.r.HugFromConfig(c.conf())
+ if err != nil {
+ panic(err)
+ }
+ if c.s != nil {
+ // A running server, register the media types.
+ for _, s := range h.Sites {
+ s.RegisterMediaTypes()
+ }
+ }
+ return h
+}
+
+func (c *hugoBuilder) hugoTry() *hugolib.HugoSites {
+ h, _ := c.r.HugFromConfig(c.conf())
+ return h
+}
+
+func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error {
+ cfg := config.New()
+ cfg.Set("renderToDisk", (c.s == nil && !c.r.renderToMemory) || (c.s != nil && c.s.renderToDisk))
+ watch := c.r.buildWatch || (c.s != nil && c.s.serverWatch)
+ cfg.Set("environment", c.r.environment)
+
+ cfg.Set("internal", maps.Params{
+ "running": running,
+ "watch": watch,
+ "verbose": c.r.verbose,
+ })
+
+ conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, cfg))
+ if err != nil {
+ return err
+ }
+ c.setConf(conf)
+ if c.onConfigLoaded != nil {
+ if err := c.onConfigLoaded(false); err != nil {
+ return err
+ }
+ }
+
+ return nil
+
+}
+
+func (c *hugoBuilder) printChangeDetected(typ string) {
+ msg := "\nChange"
+ if typ != "" {
+ msg += " of " + typ
+ }
+ msg += " detected, rebuilding site."
+
+ c.r.logger.Println(msg)
+ const layout = "2006-01-02 15:04:05.000 -0700"
+ c.r.logger.Println(htime.Now().Format(layout))
+}
+
+func (c *hugoBuilder) rebuildSites(events []fsnotify.Event) error {
+ if err := c.errState.buildErr(); err != nil {
+ ferrs := herrors.UnwrapFileErrorsWithErrorContext(err)
+ for _, err := range ferrs {
+ events = append(events, fsnotify.Event{Name: err.Position().Filename, Op: fsnotify.Write})
+ }
+ }
+ c.errState.setBuildErr(nil)
+ visited := c.visitedURLs.PeekAllSet()
+ h := c.hugo()
+ if c.fastRenderMode {
+ // Make sure we always render the home pages
+ for _, l := range c.conf().configs.Languages {
+ langPath := h.GetLangSubDir(l.Lang)
+ if langPath != "" {
+ langPath = langPath + "/"
+ }
+ home := h.PrependBasePath("/"+langPath, false)
+ visited[home] = true
+ }
+ }
+ return h.Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyVisited: visited, ErrRecovery: c.errState.wasErr()}, events...)
+}
+
+func (c *hugoBuilder) reloadConfig() error {
+ c.r.Reset()
+ c.r.configVersionID.Add(1)
+ oldConf := c.conf()
+ conf, err := c.r.ConfigFromConfig(c.r.configVersionID.Load(), c.conf())
+ if err != nil {
+ return err
+ }
+ sameLen := len(oldConf.configs.Languages) == len(conf.configs.Languages)
+ if !sameLen {
+ if oldConf.configs.IsMultihost || conf.configs.IsMultihost {
+ return errors.New("multihost change detected, please restart server")
+ }
+ }
+ c.setConf(conf)
+ if c.onConfigLoaded != nil {
+ if err := c.onConfigLoaded(true); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}