github.com/elmarschill/hugo_sample@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 }