hugo.go (30957B)
1 // Copyright 2019 The Hugo Authors. All rights reserved.
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 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13
14 // Package commands defines and implements command-line commands and flags
15 // used by Hugo. Commands and flags are implemented using Cobra.
16 package commands
17
18 import (
19 "context"
20 "fmt"
21 "io/ioutil"
22 "os"
23 "os/signal"
24 "path/filepath"
25 "runtime"
26 "runtime/pprof"
27 "runtime/trace"
28 "strings"
29 "sync/atomic"
30 "syscall"
31 "time"
32
33 "github.com/gohugoio/hugo/hugofs/files"
34 "github.com/gohugoio/hugo/tpl"
35
36 "github.com/gohugoio/hugo/common/herrors"
37 "github.com/gohugoio/hugo/common/htime"
38 "github.com/gohugoio/hugo/common/types"
39
40 "github.com/gohugoio/hugo/hugofs"
41
42 "github.com/gohugoio/hugo/resources/page"
43
44 "github.com/gohugoio/hugo/common/hugo"
45 "github.com/gohugoio/hugo/common/loggers"
46 "github.com/gohugoio/hugo/common/terminal"
47
48 "github.com/gohugoio/hugo/hugolib/filesystems"
49
50 "golang.org/x/sync/errgroup"
51
52 "github.com/gohugoio/hugo/config"
53
54 flag "github.com/spf13/pflag"
55
56 "github.com/fsnotify/fsnotify"
57 "github.com/gohugoio/hugo/helpers"
58 "github.com/gohugoio/hugo/hugolib"
59 "github.com/gohugoio/hugo/livereload"
60 "github.com/gohugoio/hugo/watcher"
61 "github.com/spf13/afero"
62 "github.com/spf13/cobra"
63 "github.com/spf13/fsync"
64 jww "github.com/spf13/jwalterweatherman"
65 )
66
67 // The Response value from Execute.
68 type Response struct {
69 // The build Result will only be set in the hugo build command.
70 Result *hugolib.HugoSites
71
72 // Err is set when the command failed to execute.
73 Err error
74
75 // The command that was executed.
76 Cmd *cobra.Command
77 }
78
79 // IsUserError returns true is the Response error is a user error rather than a
80 // system error.
81 func (r Response) IsUserError() bool {
82 return r.Err != nil && isUserError(r.Err)
83 }
84
85 // Execute adds all child commands to the root command HugoCmd and sets flags appropriately.
86 // The args are usually filled with os.Args[1:].
87 func Execute(args []string) Response {
88 hugoCmd := newCommandsBuilder().addAll().build()
89 cmd := hugoCmd.getCommand()
90 cmd.SetArgs(args)
91
92 c, err := cmd.ExecuteC()
93
94 var resp Response
95
96 if c == cmd && hugoCmd.c != nil {
97 // Root command executed
98 resp.Result = hugoCmd.c.hugo()
99 }
100
101 if err == nil {
102 errCount := int(loggers.GlobalErrorCounter.Count())
103 if errCount > 0 {
104 err = fmt.Errorf("logged %d errors", errCount)
105 } else if resp.Result != nil {
106 errCount = resp.Result.NumLogErrors()
107 if errCount > 0 {
108 err = fmt.Errorf("logged %d errors", errCount)
109 }
110 }
111
112 }
113
114 resp.Err = err
115 resp.Cmd = c
116
117 return resp
118 }
119
120 // InitializeConfig initializes a config file with sensible default configuration flags.
121 func initializeConfig(mustHaveConfigFile, failOnInitErr, running bool,
122 h *hugoBuilderCommon,
123 f flagsToConfigHandler,
124 cfgInit func(c *commandeer) error) (*commandeer, error) {
125 c, err := newCommandeer(mustHaveConfigFile, failOnInitErr, running, h, f, cfgInit)
126 if err != nil {
127 return nil, err
128 }
129
130 if h := c.hugoTry(); h != nil {
131 for _, s := range h.Sites {
132 s.RegisterMediaTypes()
133 }
134 }
135
136 return c, nil
137 }
138
139 func (c *commandeer) createLogger(cfg config.Provider) (loggers.Logger, error) {
140 var (
141 logHandle = ioutil.Discard
142 logThreshold = jww.LevelWarn
143 logFile = cfg.GetString("logFile")
144 outHandle = ioutil.Discard
145 stdoutThreshold = jww.LevelWarn
146 )
147
148 if !c.h.quiet {
149 outHandle = os.Stdout
150 }
151
152 if c.h.verboseLog || c.h.logging || (c.h.logFile != "") {
153 var err error
154 if logFile != "" {
155 logHandle, err = os.OpenFile(logFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
156 if err != nil {
157 return nil, newSystemError("Failed to open log file:", logFile, err)
158 }
159 } else {
160 logHandle, err = ioutil.TempFile("", "hugo")
161 if err != nil {
162 return nil, newSystemError(err)
163 }
164 }
165 } else if !c.h.quiet && cfg.GetBool("verbose") {
166 stdoutThreshold = jww.LevelInfo
167 }
168
169 if cfg.GetBool("debug") {
170 stdoutThreshold = jww.LevelDebug
171 }
172
173 if c.h.verboseLog {
174 logThreshold = jww.LevelInfo
175 if cfg.GetBool("debug") {
176 logThreshold = jww.LevelDebug
177 }
178 }
179
180 loggers.InitGlobalLogger(stdoutThreshold, logThreshold, outHandle, logHandle)
181 helpers.InitLoggers()
182
183 return loggers.NewLogger(stdoutThreshold, logThreshold, outHandle, logHandle, c.running), nil
184 }
185
186 func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
187 persFlagKeys := []string{
188 "debug",
189 "verbose",
190 "logFile",
191 // Moved from vars
192 }
193 flagKeys := []string{
194 "cleanDestinationDir",
195 "buildDrafts",
196 "buildFuture",
197 "buildExpired",
198 "clock",
199 "uglyURLs",
200 "canonifyURLs",
201 "enableRobotsTXT",
202 "enableGitInfo",
203 "pluralizeListTitles",
204 "preserveTaxonomyNames",
205 "ignoreCache",
206 "forceSyncStatic",
207 "noTimes",
208 "noChmod",
209 "noBuildLock",
210 "ignoreVendorPaths",
211 "templateMetrics",
212 "templateMetricsHints",
213
214 // Moved from vars.
215 "baseURL",
216 "buildWatch",
217 "cacheDir",
218 "cfgFile",
219 "confirm",
220 "contentDir",
221 "debug",
222 "destination",
223 "disableKinds",
224 "dryRun",
225 "force",
226 "gc",
227 "printI18nWarnings",
228 "printUnusedTemplates",
229 "invalidateCDN",
230 "layoutDir",
231 "logFile",
232 "maxDeletes",
233 "quiet",
234 "renderToMemory",
235 "source",
236 "target",
237 "theme",
238 "themesDir",
239 "verbose",
240 "verboseLog",
241 "duplicateTargetPaths",
242 }
243
244 for _, key := range persFlagKeys {
245 setValueFromFlag(cmd.PersistentFlags(), key, cfg, "", false)
246 }
247 for _, key := range flagKeys {
248 setValueFromFlag(cmd.Flags(), key, cfg, "", false)
249 }
250
251 setValueFromFlag(cmd.Flags(), "minify", cfg, "minifyOutput", true)
252
253 // Set some "config aliases"
254 setValueFromFlag(cmd.Flags(), "destination", cfg, "publishDir", false)
255 setValueFromFlag(cmd.Flags(), "printI18nWarnings", cfg, "logI18nWarnings", false)
256 setValueFromFlag(cmd.Flags(), "printPathWarnings", cfg, "logPathWarnings", false)
257 }
258
259 func setValueFromFlag(flags *flag.FlagSet, key string, cfg config.Provider, targetKey string, force bool) {
260 key = strings.TrimSpace(key)
261 if (force && flags.Lookup(key) != nil) || flags.Changed(key) {
262 f := flags.Lookup(key)
263 configKey := key
264 if targetKey != "" {
265 configKey = targetKey
266 }
267 // Gotta love this API.
268 switch f.Value.Type() {
269 case "bool":
270 bv, _ := flags.GetBool(key)
271 cfg.Set(configKey, bv)
272 case "string":
273 cfg.Set(configKey, f.Value.String())
274 case "stringSlice":
275 bv, _ := flags.GetStringSlice(key)
276 cfg.Set(configKey, bv)
277 case "int":
278 iv, _ := flags.GetInt(key)
279 cfg.Set(configKey, iv)
280 default:
281 panic(fmt.Sprintf("update switch with %s", f.Value.Type()))
282 }
283
284 }
285 }
286
287 func (c *commandeer) fullBuild(noBuildLock bool) error {
288 var (
289 g errgroup.Group
290 langCount map[string]uint64
291 )
292
293 if !c.h.quiet {
294 fmt.Println("Start building sites … ")
295 fmt.Println(hugo.BuildVersionString())
296 if terminal.IsTerminal(os.Stdout) {
297 defer func() {
298 fmt.Print(showCursor + clearLine)
299 }()
300 }
301 }
302
303 copyStaticFunc := func() error {
304 cnt, err := c.copyStatic()
305 if err != nil {
306 return fmt.Errorf("Error copying static files: %w", err)
307 }
308 langCount = cnt
309 return nil
310 }
311 buildSitesFunc := func() error {
312 if err := c.buildSites(noBuildLock); err != nil {
313 return fmt.Errorf("Error building site: %w", err)
314 }
315 return nil
316 }
317 // Do not copy static files and build sites in parallel if cleanDestinationDir is enabled.
318 // This flag deletes all static resources in /public folder that are missing in /static,
319 // and it does so at the end of copyStatic() call.
320 if c.Cfg.GetBool("cleanDestinationDir") {
321 if err := copyStaticFunc(); err != nil {
322 return err
323 }
324 if err := buildSitesFunc(); err != nil {
325 return err
326 }
327 } else {
328 g.Go(copyStaticFunc)
329 g.Go(buildSitesFunc)
330 if err := g.Wait(); err != nil {
331 return err
332 }
333 }
334
335 for _, s := range c.hugo().Sites {
336 s.ProcessingStats.Static = langCount[s.Language().Lang]
337 }
338
339 if c.h.gc {
340 count, err := c.hugo().GC()
341 if err != nil {
342 return err
343 }
344 for _, s := range c.hugo().Sites {
345 // We have no way of knowing what site the garbage belonged to.
346 s.ProcessingStats.Cleaned = uint64(count)
347 }
348 }
349
350 return nil
351 }
352
353 func (c *commandeer) initCPUProfile() (func(), error) {
354 if c.h.cpuprofile == "" {
355 return nil, nil
356 }
357
358 f, err := os.Create(c.h.cpuprofile)
359 if err != nil {
360 return nil, fmt.Errorf("failed to create CPU profile: %w", err)
361 }
362 if err := pprof.StartCPUProfile(f); err != nil {
363 return nil, fmt.Errorf("failed to start CPU profile: %w", err)
364 }
365 return func() {
366 pprof.StopCPUProfile()
367 f.Close()
368 }, nil
369 }
370
371 func (c *commandeer) initMemProfile() {
372 if c.h.memprofile == "" {
373 return
374 }
375
376 f, err := os.Create(c.h.memprofile)
377 if err != nil {
378 c.logger.Errorf("could not create memory profile: ", err)
379 }
380 defer f.Close()
381 runtime.GC() // get up-to-date statistics
382 if err := pprof.WriteHeapProfile(f); err != nil {
383 c.logger.Errorf("could not write memory profile: ", err)
384 }
385 }
386
387 func (c *commandeer) initTraceProfile() (func(), error) {
388 if c.h.traceprofile == "" {
389 return nil, nil
390 }
391
392 f, err := os.Create(c.h.traceprofile)
393 if err != nil {
394 return nil, fmt.Errorf("failed to create trace file: %w", err)
395 }
396
397 if err := trace.Start(f); err != nil {
398 return nil, fmt.Errorf("failed to start trace: %w", err)
399 }
400
401 return func() {
402 trace.Stop()
403 f.Close()
404 }, nil
405 }
406
407 func (c *commandeer) initMutexProfile() (func(), error) {
408 if c.h.mutexprofile == "" {
409 return nil, nil
410 }
411
412 f, err := os.Create(c.h.mutexprofile)
413 if err != nil {
414 return nil, err
415 }
416
417 runtime.SetMutexProfileFraction(1)
418
419 return func() {
420 pprof.Lookup("mutex").WriteTo(f, 0)
421 f.Close()
422 }, nil
423 }
424
425 func (c *commandeer) initMemTicker() func() {
426 memticker := time.NewTicker(5 * time.Second)
427 quit := make(chan struct{})
428 printMem := func() {
429 var m runtime.MemStats
430 runtime.ReadMemStats(&m)
431 fmt.Printf("\n\nAlloc = %v\nTotalAlloc = %v\nSys = %v\nNumGC = %v\n\n", formatByteCount(m.Alloc), formatByteCount(m.TotalAlloc), formatByteCount(m.Sys), m.NumGC)
432 }
433
434 go func() {
435 for {
436 select {
437 case <-memticker.C:
438 printMem()
439 case <-quit:
440 memticker.Stop()
441 printMem()
442 return
443 }
444 }
445 }()
446
447 return func() {
448 close(quit)
449 }
450 }
451
452 func (c *commandeer) initProfiling() (func(), error) {
453 stopCPUProf, err := c.initCPUProfile()
454 if err != nil {
455 return nil, err
456 }
457
458 stopMutexProf, err := c.initMutexProfile()
459 if err != nil {
460 return nil, err
461 }
462
463 stopTraceProf, err := c.initTraceProfile()
464 if err != nil {
465 return nil, err
466 }
467
468 var stopMemTicker func()
469 if c.h.printm {
470 stopMemTicker = c.initMemTicker()
471 }
472
473 return func() {
474 c.initMemProfile()
475
476 if stopCPUProf != nil {
477 stopCPUProf()
478 }
479 if stopMutexProf != nil {
480 stopMutexProf()
481 }
482
483 if stopTraceProf != nil {
484 stopTraceProf()
485 }
486
487 if stopMemTicker != nil {
488 stopMemTicker()
489 }
490 }, nil
491 }
492
493 func (c *commandeer) build() error {
494 stopProfiling, err := c.initProfiling()
495 if err != nil {
496 return err
497 }
498
499 defer func() {
500 if stopProfiling != nil {
501 stopProfiling()
502 }
503 }()
504
505 if err := c.fullBuild(false); err != nil {
506 return err
507 }
508
509 if !c.h.quiet {
510 fmt.Println()
511 c.hugo().PrintProcessingStats(os.Stdout)
512 fmt.Println()
513
514 if createCounter, ok := c.publishDirFs.(hugofs.DuplicatesReporter); ok {
515 dupes := createCounter.ReportDuplicates()
516 if dupes != "" {
517 c.logger.Warnln("Duplicate target paths:", dupes)
518 }
519 }
520
521 unusedTemplates := c.hugo().Tmpl().(tpl.UnusedTemplatesProvider).UnusedTemplates()
522 for _, unusedTemplate := range unusedTemplates {
523 c.logger.Warnf("Template %s is unused, source file %s", unusedTemplate.Name(), unusedTemplate.Filename())
524 }
525 }
526
527 if c.h.buildWatch {
528 watchDirs, err := c.getDirList()
529 if err != nil {
530 return err
531 }
532
533 baseWatchDir := c.Cfg.GetString("workingDir")
534 rootWatchDirs := getRootWatchDirsStr(baseWatchDir, watchDirs)
535
536 c.logger.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs)
537 c.logger.Println("Press Ctrl+C to stop")
538 watcher, err := c.newWatcher(c.h.poll, watchDirs...)
539 checkErr(c.Logger, err)
540 defer watcher.Close()
541
542 sigs := make(chan os.Signal, 1)
543 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
544
545 <-sigs
546 }
547
548 return nil
549 }
550
551 func (c *commandeer) serverBuild() error {
552 stopProfiling, err := c.initProfiling()
553 if err != nil {
554 return err
555 }
556
557 defer func() {
558 if stopProfiling != nil {
559 stopProfiling()
560 }
561 }()
562
563 if err := c.fullBuild(false); err != nil {
564 return err
565 }
566
567 // TODO(bep) Feedback?
568 if !c.h.quiet {
569 fmt.Println()
570 c.hugo().PrintProcessingStats(os.Stdout)
571 fmt.Println()
572 }
573
574 return nil
575 }
576
577 func (c *commandeer) copyStatic() (map[string]uint64, error) {
578 m, err := c.doWithPublishDirs(c.copyStaticTo)
579 if err == nil || os.IsNotExist(err) {
580 return m, nil
581 }
582 return m, err
583 }
584
585 func (c *commandeer) doWithPublishDirs(f func(sourceFs *filesystems.SourceFilesystem) (uint64, error)) (map[string]uint64, error) {
586 langCount := make(map[string]uint64)
587
588 staticFilesystems := c.hugo().BaseFs.SourceFilesystems.Static
589
590 if len(staticFilesystems) == 0 {
591 c.logger.Infoln("No static directories found to sync")
592 return langCount, nil
593 }
594
595 for lang, fs := range staticFilesystems {
596 cnt, err := f(fs)
597 if err != nil {
598 return langCount, err
599 }
600
601 if lang == "" {
602 // Not multihost
603 for _, l := range c.languages {
604 langCount[l.Lang] = cnt
605 }
606 } else {
607 langCount[lang] = cnt
608 }
609 }
610
611 return langCount, nil
612 }
613
614 type countingStatFs struct {
615 afero.Fs
616 statCounter uint64
617 }
618
619 func (fs *countingStatFs) Stat(name string) (os.FileInfo, error) {
620 f, err := fs.Fs.Stat(name)
621 if err == nil {
622 if !f.IsDir() {
623 atomic.AddUint64(&fs.statCounter, 1)
624 }
625 }
626 return f, err
627 }
628
629 func chmodFilter(dst, src os.FileInfo) bool {
630 // Hugo publishes data from multiple sources, potentially
631 // with overlapping directory structures. We cannot sync permissions
632 // for directories as that would mean that we might end up with write-protected
633 // directories inside /public.
634 // One example of this would be syncing from the Go Module cache,
635 // which have 0555 directories.
636 return src.IsDir()
637 }
638
639 func (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
640 publishDir := helpers.FilePathSeparator
641
642 if sourceFs.PublishFolder != "" {
643 publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
644 }
645
646 fs := &countingStatFs{Fs: sourceFs.Fs}
647
648 syncer := fsync.NewSyncer()
649 syncer.NoTimes = c.Cfg.GetBool("noTimes")
650 syncer.NoChmod = c.Cfg.GetBool("noChmod")
651 syncer.ChmodFilter = chmodFilter
652 syncer.SrcFs = fs
653 syncer.DestFs = c.Fs.PublishDir
654 if c.renderStaticToDisk {
655 syncer.DestFs = c.Fs.PublishDirStatic
656 }
657 // Now that we are using a unionFs for the static directories
658 // We can effectively clean the publishDir on initial sync
659 syncer.Delete = c.Cfg.GetBool("cleanDestinationDir")
660
661 if syncer.Delete {
662 c.logger.Infoln("removing all files from destination that don't exist in static dirs")
663
664 syncer.DeleteFilter = func(f os.FileInfo) bool {
665 return f.IsDir() && strings.HasPrefix(f.Name(), ".")
666 }
667 }
668 c.logger.Infoln("syncing static files to", publishDir)
669
670 // because we are using a baseFs (to get the union right).
671 // set sync src to root
672 err := syncer.Sync(publishDir, helpers.FilePathSeparator)
673 if err != nil {
674 return 0, err
675 }
676
677 // Sync runs Stat 3 times for every source file (which sounds much)
678 numFiles := fs.statCounter / 3
679
680 return numFiles, err
681 }
682
683 func (c *commandeer) firstPathSpec() *helpers.PathSpec {
684 return c.hugo().Sites[0].PathSpec
685 }
686
687 func (c *commandeer) timeTrack(start time.Time, name string) {
688 // Note the use of time.Since here and time.Now in the callers.
689 // We have a htime.Sinnce, but that may be adjusted to the future,
690 // and that does not make sense here, esp. when used before the
691 // global Clock is initialized.
692 elapsed := time.Since(start)
693 c.logger.Printf("%s in %v ms", name, int(1000*elapsed.Seconds()))
694 }
695
696 // getDirList provides NewWatcher() with a list of directories to watch for changes.
697 func (c *commandeer) getDirList() ([]string, error) {
698 var filenames []string
699
700 walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error {
701 if err != nil {
702 c.logger.Errorln("walker: ", err)
703 return nil
704 }
705
706 if fi.IsDir() {
707 if fi.Name() == ".git" ||
708 fi.Name() == "node_modules" || fi.Name() == "bower_components" {
709 return filepath.SkipDir
710 }
711
712 filenames = append(filenames, fi.Meta().Filename)
713 }
714
715 return nil
716 }
717
718 watchFiles := c.hugo().PathSpec.BaseFs.WatchDirs()
719 for _, fi := range watchFiles {
720 if !fi.IsDir() {
721 filenames = append(filenames, fi.Meta().Filename)
722 continue
723 }
724
725 w := hugofs.NewWalkway(hugofs.WalkwayConfig{Logger: c.logger, Info: fi, WalkFn: walkFn})
726 if err := w.Walk(); err != nil {
727 c.logger.Errorln("walker: ", err)
728 }
729 }
730
731 filenames = helpers.UniqueStringsSorted(filenames)
732
733 return filenames, nil
734 }
735
736 func (c *commandeer) buildSites(noBuildLock bool) (err error) {
737 return c.hugo().Build(hugolib.BuildCfg{NoBuildLock: noBuildLock})
738 }
739
740 func (c *commandeer) handleBuildErr(err error, msg string) {
741 c.buildErr = err
742 c.logger.Errorln(msg + ": " + cleanErrorLog(err.Error()))
743 }
744
745 func (c *commandeer) rebuildSites(events []fsnotify.Event) error {
746 if c.buildErr != nil {
747 ferrs := herrors.UnwrapFileErrorsWithErrorContext(c.buildErr)
748 for _, err := range ferrs {
749 events = append(events, fsnotify.Event{Name: err.Position().Filename, Op: fsnotify.Write})
750 }
751 }
752 c.buildErr = nil
753 visited := c.visitedURLs.PeekAllSet()
754 if c.fastRenderMode {
755 // Make sure we always render the home pages
756 for _, l := range c.languages {
757 langPath := c.hugo().PathSpec.GetLangSubDir(l.Lang)
758 if langPath != "" {
759 langPath = langPath + "/"
760 }
761 home := c.hugo().PathSpec.PrependBasePath("/"+langPath, false)
762 visited[home] = true
763 }
764 }
765 return c.hugo().Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyVisited: visited, ErrRecovery: c.wasError}, events...)
766 }
767
768 func (c *commandeer) partialReRender(urls ...string) error {
769 defer func() {
770 c.wasError = false
771 }()
772 c.buildErr = nil
773 visited := make(map[string]bool)
774 for _, url := range urls {
775 visited[url] = true
776 }
777
778 // Note: We do not set NoBuildLock as the file lock is not acquired at this stage.
779 return c.hugo().Build(hugolib.BuildCfg{NoBuildLock: false, RecentlyVisited: visited, PartialReRender: true, ErrRecovery: c.wasError})
780 }
781
782 func (c *commandeer) fullRebuild(changeType string) {
783 if changeType == configChangeGoMod {
784 // go.mod may be changed during the build itself, and
785 // we really want to prevent superfluous builds.
786 if !c.fullRebuildSem.TryAcquire(1) {
787 return
788 }
789 c.fullRebuildSem.Release(1)
790 }
791
792 c.fullRebuildSem.Acquire(context.Background(), 1)
793
794 go func() {
795 defer c.fullRebuildSem.Release(1)
796
797 c.printChangeDetected(changeType)
798
799 defer func() {
800 // Allow any file system events to arrive back.
801 // This will block any rebuild on config changes for the
802 // duration of the sleep.
803 time.Sleep(2 * time.Second)
804 }()
805
806 defer c.timeTrack(time.Now(), "Rebuilt")
807
808 c.commandeerHugoState = newCommandeerHugoState()
809 err := c.loadConfig()
810 if err != nil {
811 // Set the processing on pause until the state is recovered.
812 c.paused = true
813 c.handleBuildErr(err, "Failed to reload config")
814
815 } else {
816 c.paused = false
817 }
818
819 if !c.paused {
820 _, err := c.copyStatic()
821 if err != nil {
822 c.logger.Errorln(err)
823 return
824 }
825
826 err = c.buildSites(true)
827 if err != nil {
828 c.logger.Errorln(err)
829 } else if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
830 livereload.ForceRefresh()
831 }
832 }
833 }()
834 }
835
836 // newWatcher creates a new watcher to watch filesystem events.
837 func (c *commandeer) newWatcher(pollIntervalStr string, dirList ...string) (*watcher.Batcher, error) {
838 if runtime.GOOS == "darwin" {
839 tweakLimit()
840 }
841
842 staticSyncer, err := newStaticSyncer(c)
843 if err != nil {
844 return nil, err
845 }
846
847 var pollInterval time.Duration
848 poll := pollIntervalStr != ""
849 if poll {
850 pollInterval, err = types.ToDurationE(pollIntervalStr)
851 if err != nil {
852 return nil, fmt.Errorf("invalid value for flag poll: %s", err)
853 }
854 c.logger.Printf("Use watcher with poll interval %v", pollInterval)
855 }
856
857 if pollInterval == 0 {
858 pollInterval = 500 * time.Millisecond
859 }
860
861 watcher, err := watcher.New(500*time.Millisecond, pollInterval, poll)
862 if err != nil {
863 return nil, err
864 }
865
866 spec := c.hugo().Deps.SourceSpec
867
868 for _, d := range dirList {
869 if d != "" {
870 if spec.IgnoreFile(d) {
871 continue
872 }
873 _ = watcher.Add(d)
874 }
875 }
876
877 // Identifies changes to config (config.toml) files.
878 configSet := make(map[string]bool)
879
880 c.logger.Println("Watching for config changes in", strings.Join(c.configFiles, ", "))
881 for _, configFile := range c.configFiles {
882 watcher.Add(configFile)
883 configSet[configFile] = true
884 }
885
886 go func() {
887 for {
888 select {
889 case evs := <-watcher.Events:
890 unlock, err := c.buildLock()
891 if err != nil {
892 c.logger.Errorln("Failed to acquire a build lock: %s", err)
893 return
894 }
895 c.handleEvents(watcher, staticSyncer, evs, configSet)
896 if c.showErrorInBrowser && c.errCount() > 0 {
897 // Need to reload browser to show the error
898 livereload.ForceRefresh()
899 }
900 unlock()
901 case err := <-watcher.Errors():
902 if err != nil && !os.IsNotExist(err) {
903 c.logger.Errorln("Error while watching:", err)
904 }
905 }
906 }
907 }()
908
909 return watcher, nil
910 }
911
912 func (c *commandeer) printChangeDetected(typ string) {
913 msg := "\nChange"
914 if typ != "" {
915 msg += " of " + typ
916 }
917 msg += " detected, rebuilding site."
918
919 c.logger.Println(msg)
920 const layout = "2006-01-02 15:04:05.000 -0700"
921 c.logger.Println(htime.Now().Format(layout))
922 }
923
924 const (
925 configChangeConfig = "config file"
926 configChangeGoMod = "go.mod file"
927 )
928
929 func (c *commandeer) handleEvents(watcher *watcher.Batcher,
930 staticSyncer *staticSyncer,
931 evs []fsnotify.Event,
932 configSet map[string]bool) {
933 defer func() {
934 c.wasError = false
935 }()
936
937 var isHandled bool
938
939 for _, ev := range evs {
940 isConfig := configSet[ev.Name]
941 configChangeType := configChangeConfig
942 if isConfig {
943 if strings.Contains(ev.Name, "go.mod") {
944 configChangeType = configChangeGoMod
945 }
946 }
947 if !isConfig {
948 // It may be one of the /config folders
949 dirname := filepath.Dir(ev.Name)
950 if dirname != "." && configSet[dirname] {
951 isConfig = true
952 }
953 }
954
955 if isConfig {
956 isHandled = true
957
958 if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
959 continue
960 }
961
962 if ev.Op&fsnotify.Remove == fsnotify.Remove || ev.Op&fsnotify.Rename == fsnotify.Rename {
963 for _, configFile := range c.configFiles {
964 counter := 0
965 for watcher.Add(configFile) != nil {
966 counter++
967 if counter >= 100 {
968 break
969 }
970 time.Sleep(100 * time.Millisecond)
971 }
972 }
973 }
974
975 // Config file(s) changed. Need full rebuild.
976 c.fullRebuild(configChangeType)
977
978 return
979 }
980 }
981
982 if isHandled {
983 return
984 }
985
986 if c.paused {
987 // Wait for the server to get into a consistent state before
988 // we continue with processing.
989 return
990 }
991
992 if len(evs) > 50 {
993 // This is probably a mass edit of the content dir.
994 // Schedule a full rebuild for when it slows down.
995 c.debounce(func() {
996 c.fullRebuild("")
997 })
998 return
999 }
1000
1001 c.logger.Infoln("Received System Events:", evs)
1002
1003 staticEvents := []fsnotify.Event{}
1004 dynamicEvents := []fsnotify.Event{}
1005
1006 filtered := []fsnotify.Event{}
1007 for _, ev := range evs {
1008 if c.hugo().ShouldSkipFileChangeEvent(ev) {
1009 continue
1010 }
1011 // Check the most specific first, i.e. files.
1012 contentMapped := c.hugo().ContentChanges.GetSymbolicLinkMappings(ev.Name)
1013 if len(contentMapped) > 0 {
1014 for _, mapped := range contentMapped {
1015 filtered = append(filtered, fsnotify.Event{Name: mapped, Op: ev.Op})
1016 }
1017 continue
1018 }
1019
1020 // Check for any symbolic directory mapping.
1021
1022 dir, name := filepath.Split(ev.Name)
1023
1024 contentMapped = c.hugo().ContentChanges.GetSymbolicLinkMappings(dir)
1025
1026 if len(contentMapped) == 0 {
1027 filtered = append(filtered, ev)
1028 continue
1029 }
1030
1031 for _, mapped := range contentMapped {
1032 mappedFilename := filepath.Join(mapped, name)
1033 filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op})
1034 }
1035 }
1036
1037 evs = filtered
1038
1039 for _, ev := range evs {
1040 ext := filepath.Ext(ev.Name)
1041 baseName := filepath.Base(ev.Name)
1042 istemp := strings.HasSuffix(ext, "~") ||
1043 (ext == ".swp") || // vim
1044 (ext == ".swx") || // vim
1045 (ext == ".tmp") || // generic temp file
1046 (ext == ".DS_Store") || // OSX Thumbnail
1047 baseName == "4913" || // vim
1048 strings.HasPrefix(ext, ".goutputstream") || // gnome
1049 strings.HasSuffix(ext, "jb_old___") || // intelliJ
1050 strings.HasSuffix(ext, "jb_tmp___") || // intelliJ
1051 strings.HasSuffix(ext, "jb_bak___") || // intelliJ
1052 strings.HasPrefix(ext, ".sb-") || // byword
1053 strings.HasPrefix(baseName, ".#") || // emacs
1054 strings.HasPrefix(baseName, "#") // emacs
1055 if istemp {
1056 continue
1057 }
1058 if c.hugo().Deps.SourceSpec.IgnoreFile(ev.Name) {
1059 continue
1060 }
1061 // Sometimes during rm -rf operations a '"": REMOVE' is triggered. Just ignore these
1062 if ev.Name == "" {
1063 continue
1064 }
1065
1066 // Write and rename operations are often followed by CHMOD.
1067 // There may be valid use cases for rebuilding the site on CHMOD,
1068 // but that will require more complex logic than this simple conditional.
1069 // On OS X this seems to be related to Spotlight, see:
1070 // https://github.com/go-fsnotify/fsnotify/issues/15
1071 // A workaround is to put your site(s) on the Spotlight exception list,
1072 // but that may be a little mysterious for most end users.
1073 // So, for now, we skip reload on CHMOD.
1074 // We do have to check for WRITE though. On slower laptops a Chmod
1075 // could be aggregated with other important events, and we still want
1076 // to rebuild on those
1077 if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod {
1078 continue
1079 }
1080
1081 walkAdder := func(path string, f hugofs.FileMetaInfo, err error) error {
1082 if f.IsDir() {
1083 c.logger.Println("adding created directory to watchlist", path)
1084 if err := watcher.Add(path); err != nil {
1085 return err
1086 }
1087 } else if !staticSyncer.isStatic(path) {
1088 // Hugo's rebuilding logic is entirely file based. When you drop a new folder into
1089 // /content on OSX, the above logic will handle future watching of those files,
1090 // but the initial CREATE is lost.
1091 dynamicEvents = append(dynamicEvents, fsnotify.Event{Name: path, Op: fsnotify.Create})
1092 }
1093 return nil
1094 }
1095
1096 // recursively add new directories to watch list
1097 // When mkdir -p is used, only the top directory triggers an event (at least on OSX)
1098 if ev.Op&fsnotify.Create == fsnotify.Create {
1099 if s, err := c.Fs.Source.Stat(ev.Name); err == nil && s.Mode().IsDir() {
1100 _ = helpers.SymbolicWalk(c.Fs.Source, ev.Name, walkAdder)
1101 }
1102 }
1103
1104 if staticSyncer.isStatic(ev.Name) {
1105 staticEvents = append(staticEvents, ev)
1106 } else {
1107 dynamicEvents = append(dynamicEvents, ev)
1108 }
1109 }
1110
1111 if len(staticEvents) > 0 {
1112 c.printChangeDetected("Static files")
1113
1114 if c.Cfg.GetBool("forceSyncStatic") {
1115 c.logger.Printf("Syncing all static files\n")
1116 _, err := c.copyStatic()
1117 if err != nil {
1118 c.logger.Errorln("Error copying static files to publish dir:", err)
1119 return
1120 }
1121 } else {
1122 if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil {
1123 c.logger.Errorln("Error syncing static files to publish dir:", err)
1124 return
1125 }
1126 }
1127
1128 if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
1129 // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized
1130
1131 // force refresh when more than one file
1132 if !c.wasError && len(staticEvents) == 1 {
1133 ev := staticEvents[0]
1134 path := c.hugo().BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name)
1135 path = c.firstPathSpec().RelURL(helpers.ToSlashTrimLeading(path), false)
1136
1137 livereload.RefreshPath(path)
1138 } else {
1139 livereload.ForceRefresh()
1140 }
1141 }
1142 }
1143
1144 if len(dynamicEvents) > 0 {
1145 partitionedEvents := partitionDynamicEvents(
1146 c.firstPathSpec().BaseFs.SourceFilesystems,
1147 dynamicEvents)
1148
1149 doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload")
1150 onePageName := pickOneWriteOrCreatePath(partitionedEvents.ContentEvents)
1151
1152 c.printChangeDetected("")
1153 c.changeDetector.PrepareNew()
1154
1155 func() {
1156 defer c.timeTrack(time.Now(), "Total")
1157 if err := c.rebuildSites(dynamicEvents); err != nil {
1158 c.handleBuildErr(err, "Rebuild failed")
1159 }
1160 }()
1161
1162 if doLiveReload {
1163 if len(partitionedEvents.ContentEvents) == 0 && len(partitionedEvents.AssetEvents) > 0 {
1164 if c.wasError {
1165 livereload.ForceRefresh()
1166 return
1167 }
1168 changed := c.changeDetector.changed()
1169 if c.changeDetector != nil && len(changed) == 0 {
1170 // Nothing has changed.
1171 return
1172 } else if len(changed) == 1 {
1173 pathToRefresh := c.firstPathSpec().RelURL(helpers.ToSlashTrimLeading(changed[0]), false)
1174 livereload.RefreshPath(pathToRefresh)
1175 } else {
1176 livereload.ForceRefresh()
1177 }
1178 }
1179
1180 if len(partitionedEvents.ContentEvents) > 0 {
1181
1182 navigate := c.Cfg.GetBool("navigateToChanged")
1183 // We have fetched the same page above, but it may have
1184 // changed.
1185 var p page.Page
1186
1187 if navigate {
1188 if onePageName != "" {
1189 p = c.hugo().GetContentPage(onePageName)
1190 }
1191 }
1192
1193 if p != nil {
1194 livereload.NavigateToPathForPort(p.RelPermalink(), p.Site().ServerPort())
1195 } else {
1196 livereload.ForceRefresh()
1197 }
1198 }
1199 }
1200 }
1201 }
1202
1203 // dynamicEvents contains events that is considered dynamic, as in "not static".
1204 // Both of these categories will trigger a new build, but the asset events
1205 // does not fit into the "navigate to changed" logic.
1206 type dynamicEvents struct {
1207 ContentEvents []fsnotify.Event
1208 AssetEvents []fsnotify.Event
1209 }
1210
1211 func partitionDynamicEvents(sourceFs *filesystems.SourceFilesystems, events []fsnotify.Event) (de dynamicEvents) {
1212 for _, e := range events {
1213 if sourceFs.IsAsset(e.Name) {
1214 de.AssetEvents = append(de.AssetEvents, e)
1215 } else {
1216 de.ContentEvents = append(de.ContentEvents, e)
1217 }
1218 }
1219 return
1220 }
1221
1222 func pickOneWriteOrCreatePath(events []fsnotify.Event) string {
1223 name := ""
1224
1225 for _, ev := range events {
1226 if ev.Op&fsnotify.Write == fsnotify.Write || ev.Op&fsnotify.Create == fsnotify.Create {
1227 if files.IsIndexContentFile(ev.Name) {
1228 return ev.Name
1229 }
1230
1231 if files.IsContentFile(ev.Name) {
1232 name = ev.Name
1233 }
1234
1235 }
1236 }
1237
1238 return name
1239 }
1240
1241 func formatByteCount(b uint64) string {
1242 const unit = 1000
1243 if b < unit {
1244 return fmt.Sprintf("%d B", b)
1245 }
1246 div, exp := int64(unit), 0
1247 for n := b / unit; n >= unit; n /= unit {
1248 div *= unit
1249 exp++
1250 }
1251 return fmt.Sprintf("%.1f %cB",
1252 float64(b)/float64(div), "kMGTPE"[exp])
1253 }