github.com/gohugoio/hugo@v0.88.1/hugolib/hugo_sites_build.go (about) 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 hugolib 15 16 import ( 17 "bytes" 18 "context" 19 "encoding/json" 20 "fmt" 21 "os" 22 "path/filepath" 23 "runtime/trace" 24 "strings" 25 26 "github.com/gohugoio/hugo/publisher" 27 28 "github.com/gohugoio/hugo/hugofs" 29 30 "github.com/gohugoio/hugo/common/para" 31 "github.com/gohugoio/hugo/config" 32 "github.com/gohugoio/hugo/resources/postpub" 33 34 "github.com/spf13/afero" 35 36 "github.com/gohugoio/hugo/output" 37 38 "github.com/pkg/errors" 39 40 "github.com/fsnotify/fsnotify" 41 "github.com/gohugoio/hugo/helpers" 42 ) 43 44 // Build builds all sites. If filesystem events are provided, 45 // this is considered to be a potential partial rebuild. 46 func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error { 47 if h.running { 48 // Make sure we don't trigger rebuilds in parallel. 49 h.runningMu.Lock() 50 defer h.runningMu.Unlock() 51 } else { 52 defer func() { 53 h.Close() 54 }() 55 } 56 57 ctx, task := trace.NewTask(context.Background(), "Build") 58 defer task.End() 59 60 errCollector := h.StartErrorCollector() 61 errs := make(chan error) 62 63 go func(from, to chan error) { 64 var errors []error 65 i := 0 66 for e := range from { 67 i++ 68 if i > 50 { 69 break 70 } 71 errors = append(errors, e) 72 } 73 to <- h.pickOneAndLogTheRest(errors) 74 75 close(to) 76 }(errCollector, errs) 77 78 if h.Metrics != nil { 79 h.Metrics.Reset() 80 } 81 82 h.testCounters = config.testCounters 83 84 // Need a pointer as this may be modified. 85 conf := &config 86 87 if conf.whatChanged == nil { 88 // Assume everything has changed 89 conf.whatChanged = &whatChanged{source: true} 90 } 91 92 var prepareErr error 93 94 if !config.PartialReRender { 95 prepare := func() error { 96 init := func(conf *BuildCfg) error { 97 for _, s := range h.Sites { 98 s.Deps.BuildStartListeners.Notify() 99 } 100 101 if len(events) > 0 { 102 // Rebuild 103 if err := h.initRebuild(conf); err != nil { 104 return errors.Wrap(err, "initRebuild") 105 } 106 } else { 107 if err := h.initSites(conf); err != nil { 108 return errors.Wrap(err, "initSites") 109 } 110 } 111 112 return nil 113 } 114 115 var err error 116 117 f := func() { 118 err = h.process(conf, init, events...) 119 } 120 trace.WithRegion(ctx, "process", f) 121 if err != nil { 122 return errors.Wrap(err, "process") 123 } 124 125 f = func() { 126 err = h.assemble(conf) 127 } 128 trace.WithRegion(ctx, "assemble", f) 129 if err != nil { 130 return err 131 } 132 133 return nil 134 } 135 136 f := func() { 137 prepareErr = prepare() 138 } 139 trace.WithRegion(ctx, "prepare", f) 140 if prepareErr != nil { 141 h.SendError(prepareErr) 142 } 143 144 } 145 146 if prepareErr == nil { 147 var err error 148 f := func() { 149 err = h.render(conf) 150 } 151 trace.WithRegion(ctx, "render", f) 152 if err != nil { 153 h.SendError(err) 154 } 155 156 if err = h.postProcess(); err != nil { 157 h.SendError(err) 158 } 159 } 160 161 if h.Metrics != nil { 162 var b bytes.Buffer 163 h.Metrics.WriteMetrics(&b) 164 165 h.Log.Printf("\nTemplate Metrics:\n\n") 166 h.Log.Println(b.String()) 167 } 168 169 select { 170 // Make sure the channel always gets something. 171 case errCollector <- nil: 172 default: 173 } 174 close(errCollector) 175 176 err := <-errs 177 if err != nil { 178 return err 179 } 180 181 if err := h.fatalErrorHandler.getErr(); err != nil { 182 return err 183 } 184 185 errorCount := h.Log.LogCounters().ErrorCounter.Count() 186 if errorCount > 0 { 187 return fmt.Errorf("logged %d error(s)", errorCount) 188 } 189 190 return nil 191 } 192 193 // Build lifecycle methods below. 194 // The order listed matches the order of execution. 195 196 func (h *HugoSites) initSites(config *BuildCfg) error { 197 h.reset(config) 198 199 if config.NewConfig != nil { 200 if err := h.createSitesFromConfig(config.NewConfig); err != nil { 201 return err 202 } 203 } 204 205 return nil 206 } 207 208 func (h *HugoSites) initRebuild(config *BuildCfg) error { 209 if config.NewConfig != nil { 210 return errors.New("rebuild does not support 'NewConfig'") 211 } 212 213 if config.ResetState { 214 return errors.New("rebuild does not support 'ResetState'") 215 } 216 217 if !h.running { 218 return errors.New("rebuild called when not in watch mode") 219 } 220 221 for _, s := range h.Sites { 222 s.resetBuildState(config.whatChanged.source) 223 } 224 225 h.reset(config) 226 h.resetLogs() 227 helpers.InitLoggers() 228 229 return nil 230 } 231 232 func (h *HugoSites) process(config *BuildCfg, init func(config *BuildCfg) error, events ...fsnotify.Event) error { 233 // We should probably refactor the Site and pull up most of the logic from there to here, 234 // but that seems like a daunting task. 235 // So for now, if there are more than one site (language), 236 // we pre-process the first one, then configure all the sites based on that. 237 238 firstSite := h.Sites[0] 239 240 if len(events) > 0 { 241 // This is a rebuild 242 return firstSite.processPartial(config, init, events) 243 } 244 245 return firstSite.process(*config) 246 } 247 248 func (h *HugoSites) assemble(bcfg *BuildCfg) error { 249 if len(h.Sites) > 1 { 250 // The first is initialized during process; initialize the rest 251 for _, site := range h.Sites[1:] { 252 if err := site.initializeSiteInfo(); err != nil { 253 return err 254 } 255 } 256 } 257 258 if !bcfg.whatChanged.source { 259 return nil 260 } 261 262 if err := h.getContentMaps().AssemblePages(); err != nil { 263 return err 264 } 265 266 if err := h.createPageCollections(); err != nil { 267 return err 268 } 269 270 return nil 271 } 272 273 func (h *HugoSites) render(config *BuildCfg) error { 274 if _, err := h.init.layouts.Do(); err != nil { 275 return err 276 } 277 278 siteRenderContext := &siteRenderContext{cfg: config, multihost: h.multihost} 279 280 if !config.PartialReRender { 281 h.renderFormats = output.Formats{} 282 h.withSite(func(s *Site) error { 283 s.initRenderFormats() 284 return nil 285 }) 286 287 for _, s := range h.Sites { 288 h.renderFormats = append(h.renderFormats, s.renderFormats...) 289 } 290 } 291 292 i := 0 293 for _, s := range h.Sites { 294 for siteOutIdx, renderFormat := range s.renderFormats { 295 siteRenderContext.outIdx = siteOutIdx 296 siteRenderContext.sitesOutIdx = i 297 i++ 298 299 select { 300 case <-h.Done(): 301 return nil 302 default: 303 for _, s2 := range h.Sites { 304 // We render site by site, but since the content is lazily rendered 305 // and a site can "borrow" content from other sites, every site 306 // needs this set. 307 s2.rc = &siteRenderingContext{Format: renderFormat} 308 309 if err := s2.preparePagesForRender(s == s2, siteRenderContext.sitesOutIdx); err != nil { 310 return err 311 } 312 } 313 314 if !config.SkipRender { 315 if config.PartialReRender { 316 if err := s.renderPages(siteRenderContext); err != nil { 317 return err 318 } 319 } else { 320 if err := s.render(siteRenderContext); err != nil { 321 return err 322 } 323 } 324 } 325 } 326 327 } 328 } 329 330 if !config.SkipRender { 331 if err := h.renderCrossSitesSitemap(); err != nil { 332 return err 333 } 334 if err := h.renderCrossSitesRobotsTXT(); err != nil { 335 return err 336 } 337 } 338 339 return nil 340 } 341 342 func (h *HugoSites) postProcess() error { 343 // Make sure to write any build stats to disk first so it's available 344 // to the post processors. 345 if err := h.writeBuildStats(); err != nil { 346 return err 347 } 348 349 // This will only be set when js.Build have been triggered with 350 // imports that resolves to the project or a module. 351 // Write a jsconfig.json file to the project's /asset directory 352 // to help JS intellisense in VS Code etc. 353 if !h.ResourceSpec.BuildConfig.NoJSConfigInAssets && h.BaseFs.Assets.Dirs != nil { 354 fi, err := h.BaseFs.Assets.Fs.Stat("") 355 if err != nil { 356 h.Log.Warnf("Failed to resolve jsconfig.json dir: %s", err) 357 } else { 358 m := fi.(hugofs.FileMetaInfo).Meta() 359 assetsDir := m.SourceRoot 360 if strings.HasPrefix(assetsDir, h.ResourceSpec.WorkingDir) { 361 if jsConfig := h.ResourceSpec.JSConfigBuilder.Build(assetsDir); jsConfig != nil { 362 363 b, err := json.MarshalIndent(jsConfig, "", " ") 364 if err != nil { 365 h.Log.Warnf("Failed to create jsconfig.json: %s", err) 366 } else { 367 filename := filepath.Join(assetsDir, "jsconfig.json") 368 if h.running { 369 h.skipRebuildForFilenamesMu.Lock() 370 h.skipRebuildForFilenames[filename] = true 371 h.skipRebuildForFilenamesMu.Unlock() 372 } 373 // Make sure it's written to the OS fs as this is used by 374 // editors. 375 if err := afero.WriteFile(hugofs.Os, filename, b, 0666); err != nil { 376 h.Log.Warnf("Failed to write jsconfig.json: %s", err) 377 } 378 } 379 } 380 } 381 382 } 383 } 384 385 var toPostProcess []postpub.PostPublishedResource 386 for _, r := range h.ResourceSpec.PostProcessResources { 387 toPostProcess = append(toPostProcess, r) 388 } 389 390 if len(toPostProcess) == 0 { 391 // Nothing more to do. 392 return nil 393 } 394 395 workers := para.New(config.GetNumWorkerMultiplier()) 396 g, _ := workers.Start(context.Background()) 397 398 handleFile := func(filename string) error { 399 content, err := afero.ReadFile(h.BaseFs.PublishFs, filename) 400 if err != nil { 401 return err 402 } 403 404 k := 0 405 changed := false 406 407 for { 408 l := bytes.Index(content[k:], []byte(postpub.PostProcessPrefix)) 409 if l == -1 { 410 break 411 } 412 m := bytes.Index(content[k+l:], []byte(postpub.PostProcessSuffix)) + len(postpub.PostProcessSuffix) 413 414 low, high := k+l, k+l+m 415 416 field := content[low:high] 417 418 forward := l + m 419 420 for i, r := range toPostProcess { 421 if r == nil { 422 panic(fmt.Sprintf("resource %d to post process is nil", i+1)) 423 } 424 v, ok := r.GetFieldString(string(field)) 425 if ok { 426 content = append(content[:low], append([]byte(v), content[high:]...)...) 427 changed = true 428 forward = len(v) 429 break 430 } 431 } 432 433 k += forward 434 } 435 436 if changed { 437 return afero.WriteFile(h.BaseFs.PublishFs, filename, content, 0666) 438 } 439 440 return nil 441 } 442 443 _ = afero.Walk(h.BaseFs.PublishFs, "", func(path string, info os.FileInfo, err error) error { 444 if info == nil || info.IsDir() { 445 return nil 446 } 447 448 if !strings.HasSuffix(path, "html") { 449 return nil 450 } 451 452 g.Run(func() error { 453 return handleFile(path) 454 }) 455 456 return nil 457 }) 458 459 // Prepare for a new build. 460 for _, s := range h.Sites { 461 s.ResourceSpec.PostProcessResources = make(map[string]postpub.PostPublishedResource) 462 } 463 464 return g.Wait() 465 } 466 467 type publishStats struct { 468 CSSClasses string `json:"cssClasses"` 469 } 470 471 func (h *HugoSites) writeBuildStats() error { 472 if !h.ResourceSpec.BuildConfig.WriteStats { 473 return nil 474 } 475 476 htmlElements := &publisher.HTMLElements{} 477 for _, s := range h.Sites { 478 stats := s.publisher.PublishStats() 479 htmlElements.Merge(stats.HTMLElements) 480 } 481 482 htmlElements.Sort() 483 484 stats := publisher.PublishStats{ 485 HTMLElements: *htmlElements, 486 } 487 488 js, err := json.MarshalIndent(stats, "", " ") 489 if err != nil { 490 return err 491 } 492 493 filename := filepath.Join(h.WorkingDir, "hugo_stats.json") 494 495 // Make sure it's always written to the OS fs. 496 if err := afero.WriteFile(hugofs.Os, filename, js, 0666); err != nil { 497 return err 498 } 499 500 // Write to the destination, too, if a mem fs is in play. 501 if h.Fs.Source != hugofs.Os { 502 if err := afero.WriteFile(h.Fs.Destination, filename, js, 0666); err != nil { 503 return err 504 } 505 } 506 507 return nil 508 }