github.com/fighterlyt/hugo@v0.47.1/commands/commandeer.go (about)

     1  // Copyright 2018 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
    15  
    16  import (
    17  	"os"
    18  	"path/filepath"
    19  	"regexp"
    20  	"strings"
    21  	"sync"
    22  	"time"
    23  
    24  	"github.com/gohugoio/hugo/config"
    25  
    26  	"github.com/spf13/cobra"
    27  
    28  	"github.com/spf13/afero"
    29  
    30  	"github.com/gohugoio/hugo/hugolib"
    31  
    32  	"github.com/bep/debounce"
    33  	"github.com/gohugoio/hugo/common/types"
    34  	"github.com/gohugoio/hugo/deps"
    35  	"github.com/gohugoio/hugo/helpers"
    36  	"github.com/gohugoio/hugo/hugofs"
    37  	"github.com/gohugoio/hugo/langs"
    38  )
    39  
    40  type commandeerHugoState struct {
    41  	*deps.DepsCfg
    42  	hugo     *hugolib.HugoSites
    43  	fsCreate sync.Once
    44  }
    45  
    46  type commandeer struct {
    47  	*commandeerHugoState
    48  
    49  	// Currently only set when in "fast render mode". But it seems to
    50  	// be fast enough that we could maybe just add it for all server modes.
    51  	changeDetector *fileChangeDetector
    52  
    53  	// We need to reuse this on server rebuilds.
    54  	destinationFs afero.Fs
    55  
    56  	h    *hugoBuilderCommon
    57  	ftch flagsToConfigHandler
    58  
    59  	visitedURLs *types.EvictingStringQueue
    60  
    61  	doWithCommandeer func(c *commandeer) error
    62  
    63  	// We watch these for changes.
    64  	configFiles []string
    65  
    66  	// Used in cases where we get flooded with events in server mode.
    67  	debounce func(f func())
    68  
    69  	serverPorts         []int
    70  	languagesConfigured bool
    71  	languages           langs.Languages
    72  
    73  	configured bool
    74  	paused     bool
    75  }
    76  
    77  func (c *commandeer) Set(key string, value interface{}) {
    78  	if c.configured {
    79  		panic("commandeer cannot be changed")
    80  	}
    81  	c.Cfg.Set(key, value)
    82  }
    83  
    84  func (c *commandeer) initFs(fs *hugofs.Fs) error {
    85  	c.destinationFs = fs.Destination
    86  	c.DepsCfg.Fs = fs
    87  
    88  	return nil
    89  }
    90  
    91  func newCommandeer(mustHaveConfigFile, running bool, h *hugoBuilderCommon, f flagsToConfigHandler, doWithCommandeer func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
    92  
    93  	var rebuildDebouncer func(f func())
    94  	if running {
    95  		// The time value used is tested with mass content replacements in a fairly big Hugo site.
    96  		// It is better to wait for some seconds in those cases rather than get flooded
    97  		// with rebuilds.
    98  		rebuildDebouncer, _, _ = debounce.New(4 * time.Second)
    99  	}
   100  
   101  	c := &commandeer{
   102  		h:                   h,
   103  		ftch:                f,
   104  		commandeerHugoState: &commandeerHugoState{},
   105  		doWithCommandeer:    doWithCommandeer,
   106  		visitedURLs:         types.NewEvictingStringQueue(10),
   107  		debounce:            rebuildDebouncer,
   108  	}
   109  
   110  	return c, c.loadConfig(mustHaveConfigFile, running)
   111  }
   112  
   113  type fileChangeDetector struct {
   114  	sync.Mutex
   115  	current map[string]string
   116  	prev    map[string]string
   117  
   118  	irrelevantRe *regexp.Regexp
   119  }
   120  
   121  func (f *fileChangeDetector) OnFileClose(name, md5sum string) {
   122  	f.Lock()
   123  	defer f.Unlock()
   124  	f.current[name] = md5sum
   125  }
   126  
   127  func (f *fileChangeDetector) changed() []string {
   128  	if f == nil {
   129  		return nil
   130  	}
   131  	f.Lock()
   132  	defer f.Unlock()
   133  	var c []string
   134  	for k, v := range f.current {
   135  		vv, found := f.prev[k]
   136  		if !found || v != vv {
   137  			c = append(c, k)
   138  		}
   139  	}
   140  
   141  	return f.filterIrrelevant(c)
   142  }
   143  
   144  func (f *fileChangeDetector) filterIrrelevant(in []string) []string {
   145  	var filtered []string
   146  	for _, v := range in {
   147  		if !f.irrelevantRe.MatchString(v) {
   148  			filtered = append(filtered, v)
   149  		}
   150  	}
   151  	return filtered
   152  }
   153  
   154  func (f *fileChangeDetector) PrepareNew() {
   155  	if f == nil {
   156  		return
   157  	}
   158  
   159  	f.Lock()
   160  	defer f.Unlock()
   161  
   162  	if f.current == nil {
   163  		f.current = make(map[string]string)
   164  		f.prev = make(map[string]string)
   165  		return
   166  	}
   167  
   168  	f.prev = make(map[string]string)
   169  	for k, v := range f.current {
   170  		f.prev[k] = v
   171  	}
   172  	f.current = make(map[string]string)
   173  }
   174  
   175  func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
   176  
   177  	if c.DepsCfg == nil {
   178  		c.DepsCfg = &deps.DepsCfg{}
   179  	}
   180  
   181  	cfg := c.DepsCfg
   182  	c.configured = false
   183  	cfg.Running = running
   184  
   185  	var dir string
   186  	if c.h.source != "" {
   187  		dir, _ = filepath.Abs(c.h.source)
   188  	} else {
   189  		dir, _ = os.Getwd()
   190  	}
   191  
   192  	var sourceFs afero.Fs = hugofs.Os
   193  	if c.DepsCfg.Fs != nil {
   194  		sourceFs = c.DepsCfg.Fs.Source
   195  	}
   196  
   197  	doWithConfig := func(cfg config.Provider) error {
   198  
   199  		if c.ftch != nil {
   200  			c.ftch.flagsToConfig(cfg)
   201  		}
   202  
   203  		cfg.Set("workingDir", dir)
   204  
   205  		return nil
   206  	}
   207  
   208  	doWithCommandeer := func(cfg config.Provider) error {
   209  		c.Cfg = cfg
   210  		if c.doWithCommandeer == nil {
   211  			return nil
   212  		}
   213  		err := c.doWithCommandeer(c)
   214  		return err
   215  	}
   216  
   217  	config, configFiles, err := hugolib.LoadConfig(
   218  		hugolib.ConfigSourceDescriptor{Fs: sourceFs, Path: c.h.source, WorkingDir: dir, Filename: c.h.cfgFile},
   219  		doWithCommandeer,
   220  		doWithConfig)
   221  
   222  	if err != nil {
   223  		if mustHaveConfigFile {
   224  			return err
   225  		}
   226  		if err != hugolib.ErrNoConfigFile {
   227  			return err
   228  		}
   229  
   230  	}
   231  
   232  	c.configFiles = configFiles
   233  
   234  	if l, ok := c.Cfg.Get("languagesSorted").(langs.Languages); ok {
   235  		c.languagesConfigured = true
   236  		c.languages = l
   237  	}
   238  
   239  	// This is potentially double work, but we need to do this one more time now
   240  	// that all the languages have been configured.
   241  	if c.doWithCommandeer != nil {
   242  		if err := c.doWithCommandeer(c); err != nil {
   243  			return err
   244  		}
   245  	}
   246  
   247  	logger, err := c.createLogger(config)
   248  	if err != nil {
   249  		return err
   250  	}
   251  
   252  	cfg.Logger = logger
   253  
   254  	createMemFs := config.GetBool("renderToMemory")
   255  
   256  	if createMemFs {
   257  		// Rendering to memoryFS, publish to Root regardless of publishDir.
   258  		config.Set("publishDir", "/")
   259  	}
   260  
   261  	c.fsCreate.Do(func() {
   262  		fs := hugofs.NewFrom(sourceFs, config)
   263  
   264  		if c.destinationFs != nil {
   265  			// Need to reuse the destination on server rebuilds.
   266  			fs.Destination = c.destinationFs
   267  		} else if createMemFs {
   268  			// Hugo writes the output to memory instead of the disk.
   269  			fs.Destination = new(afero.MemMapFs)
   270  		}
   271  
   272  		doLiveReload := !c.h.buildWatch && !config.GetBool("disableLiveReload")
   273  		fastRenderMode := doLiveReload && !config.GetBool("disableFastRender")
   274  
   275  		if fastRenderMode {
   276  			// For now, fast render mode only. It should, however, be fast enough
   277  			// for the full variant, too.
   278  			changeDetector := &fileChangeDetector{
   279  				// We use this detector to decide to do a Hot reload of a single path or not.
   280  				// We need to filter out source maps and possibly some other to be able
   281  				// to make that decision.
   282  				irrelevantRe: regexp.MustCompile(`\.map$`),
   283  			}
   284  			changeDetector.PrepareNew()
   285  			fs.Destination = hugofs.NewHashingFs(fs.Destination, changeDetector)
   286  			c.changeDetector = changeDetector
   287  		}
   288  
   289  		err = c.initFs(fs)
   290  		if err != nil {
   291  			return
   292  		}
   293  
   294  		var h *hugolib.HugoSites
   295  
   296  		h, err = hugolib.NewHugoSites(*c.DepsCfg)
   297  		c.hugo = h
   298  
   299  	})
   300  
   301  	if err != nil {
   302  		return err
   303  	}
   304  
   305  	cacheDir := config.GetString("cacheDir")
   306  	if cacheDir != "" {
   307  		if helpers.FilePathSeparator != cacheDir[len(cacheDir)-1:] {
   308  			cacheDir = cacheDir + helpers.FilePathSeparator
   309  		}
   310  		isDir, err := helpers.DirExists(cacheDir, sourceFs)
   311  		checkErr(cfg.Logger, err)
   312  		if !isDir {
   313  			mkdir(cacheDir)
   314  		}
   315  		config.Set("cacheDir", cacheDir)
   316  	} else {
   317  		config.Set("cacheDir", helpers.GetTempDir("hugo_cache", sourceFs))
   318  	}
   319  
   320  	cfg.Logger.INFO.Println("Using config file:", config.ConfigFileUsed())
   321  
   322  	themeDir := c.hugo.PathSpec.GetFirstThemeDir()
   323  	if themeDir != "" {
   324  		if _, err := sourceFs.Stat(themeDir); os.IsNotExist(err) {
   325  			return newSystemError("Unable to find theme Directory:", themeDir)
   326  		}
   327  	}
   328  
   329  	dir, themeVersionMismatch, minVersion := c.isThemeVsHugoVersionMismatch(sourceFs)
   330  
   331  	if themeVersionMismatch {
   332  		name := filepath.Base(dir)
   333  		cfg.Logger.ERROR.Printf("%s theme does not support Hugo version %s. Minimum version required is %s\n",
   334  			strings.ToUpper(name), helpers.CurrentHugoVersion.ReleaseVersion(), minVersion)
   335  	}
   336  
   337  	return nil
   338  
   339  }