aboutsummaryrefslogtreecommitdiffhomepage
path: root/hugolib
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <[email protected]>2024-06-08 11:52:22 +0200
committerBjørn Erik Pedersen <[email protected]>2024-06-23 11:25:47 +0200
commit6cd0784e447f18e009cbbf30de471e486f7cf356 (patch)
tree0684d05d7e487ebe93463636ed8b5a1bb78e4704 /hugolib
parent8731d8822216dd3c7587769e3cf5d98690717b0c (diff)
downloadhugo-6cd0784e447f18e009cbbf30de471e486f7cf356.tar.gz
hugo-6cd0784e447f18e009cbbf30de471e486f7cf356.zip
Implement defer
Closes #8086 Closes #12589
Diffstat (limited to 'hugolib')
-rw-r--r--hugolib/content_map_page.go77
-rw-r--r--hugolib/filesystems/basefs.go2
-rw-r--r--hugolib/hugo_sites_build.go204
-rw-r--r--hugolib/hugo_sites_build_errors_test.go17
-rw-r--r--hugolib/site.go10
-rw-r--r--hugolib/site_new.go18
-rw-r--r--hugolib/site_render.go4
-rw-r--r--hugolib/site_sections.go4
8 files changed, 260 insertions, 76 deletions
diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go
index f9709df15..0a9063e23 100644
--- a/hugolib/content_map_page.go
+++ b/hugolib/content_map_page.go
@@ -33,6 +33,7 @@ import (
"github.com/gohugoio/hugo/common/rungroup"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files"
+ "github.com/gohugoio/hugo/hugofs/glob"
"github.com/gohugoio/hugo/hugolib/doctree"
"github.com/gohugoio/hugo/hugolib/pagesfromdata"
"github.com/gohugoio/hugo/identity"
@@ -1002,7 +1003,7 @@ func (m *pageMap) debugPrint(prefix string, maxLevel int, w io.Writer) {
}
const indentStr = " "
p := n.(*pageState)
- s := strings.TrimPrefix(keyPage, paths.CommonDir(prevKey, keyPage))
+ s := strings.TrimPrefix(keyPage, paths.CommonDirPath(prevKey, keyPage))
lenIndent := len(keyPage) - len(s)
fmt.Fprint(w, strings.Repeat(indentStr, lenIndent))
info := fmt.Sprintf("%s lm: %s (%s)", s, p.Lastmod().Format("2006-01-02"), p.Kind())
@@ -1047,6 +1048,59 @@ func (m *pageMap) debugPrint(prefix string, maxLevel int, w io.Writer) {
}
}
+func (h *HugoSites) dynacacheGCFilenameIfNotWatchedAndDrainMatching(filename string) {
+ cpss := h.BaseFs.ResolvePaths(filename)
+ if len(cpss) == 0 {
+ return
+ }
+ // Compile cache busters.
+ var cacheBusters []func(string) bool
+ for _, cps := range cpss {
+ if cps.Watch {
+ continue
+ }
+ np := glob.NormalizePath(path.Join(cps.Component, cps.Path))
+ g, err := h.ResourceSpec.BuildConfig().MatchCacheBuster(h.Log, np)
+ if err == nil && g != nil {
+ cacheBusters = append(cacheBusters, g)
+ }
+ }
+ if len(cacheBusters) == 0 {
+ return
+ }
+ cacheBusterOr := func(s string) bool {
+ for _, cb := range cacheBusters {
+ if cb(s) {
+ return true
+ }
+ }
+ return false
+ }
+
+ h.dynacacheGCCacheBuster(cacheBusterOr)
+
+ // We want to avoid that evicted items in the above is considered in the next step server change.
+ _ = h.MemCache.DrainEvictedIdentitiesMatching(func(ki dynacache.KeyIdentity) bool {
+ return cacheBusterOr(ki.Key.(string))
+ })
+}
+
+func (h *HugoSites) dynacacheGCCacheBuster(cachebuster func(s string) bool) {
+ if cachebuster == nil {
+ return
+ }
+ shouldDelete := func(k, v any) bool {
+ var b bool
+ if s, ok := k.(string); ok {
+ b = cachebuster(s)
+ }
+
+ return b
+ }
+
+ h.MemCache.ClearMatching(nil, shouldDelete)
+}
+
func (h *HugoSites) resolveAndClearStateForIdentities(
ctx context.Context,
l logg.LevelLogger,
@@ -1095,25 +1149,10 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
// 1. Handle the cache busters first, as those may produce identities for the page reset step.
// 2. Then reset the page outputs, which may mark some resources as stale.
// 3. Then GC the cache.
- // TOOD1
if cachebuster != nil {
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
ll := l.WithField("substep", "gc dynacache cachebuster")
-
- shouldDelete := func(k, v any) bool {
- if cachebuster == nil {
- return false
- }
- var b bool
- if s, ok := k.(string); ok {
- b = cachebuster(s)
- }
-
- return b
- }
-
- h.MemCache.ClearMatching(nil, shouldDelete)
-
+ h.dynacacheGCCacheBuster(cachebuster)
return ll, nil
}); err != nil {
return err
@@ -1123,7 +1162,9 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
// Drain the cache eviction stack.
evicted := h.Deps.MemCache.DrainEvictedIdentities()
if len(evicted) < 200 {
- changes = append(changes, evicted...)
+ for _, c := range evicted {
+ changes = append(changes, c.Identity)
+ }
} else {
// Mass eviction, we might as well invalidate everything.
changes = []identity.Identity{identity.GenghisKhan}
diff --git a/hugolib/filesystems/basefs.go b/hugolib/filesystems/basefs.go
index b3e3284d5..cb7846cd1 100644
--- a/hugolib/filesystems/basefs.go
+++ b/hugolib/filesystems/basefs.go
@@ -720,7 +720,7 @@ func (b *sourceFilesystemsBuilder) createOverlayFs(
ModuleOrdinal: md.ordinal,
IsProject: md.isMainProject,
Meta: &hugofs.FileMeta{
- Watch: md.Watch(),
+ Watch: !mount.DisableWatch && md.Watch(),
Weight: mountWeight,
InclusionFilter: inclusionFilter,
},
diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go
index 12eb6a5f8..65ce946e9 100644
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -26,6 +26,7 @@ import (
"time"
"github.com/bep/logg"
+ "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugofs/files"
@@ -173,6 +174,16 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
h.SendError(fmt.Errorf("postRenderOnce: %w", err))
}
+ // Make sure to write any build stats to disk first so it's available
+ // to the post processors.
+ if err := h.writeBuildStats(); err != nil {
+ return err
+ }
+
+ if err := h.renderDeferred(infol); err != nil {
+ h.SendError(fmt.Errorf("renderDeferred: %w", err))
+ }
+
if err := h.postProcess(infol); err != nil {
h.SendError(fmt.Errorf("postProcess: %w", err))
}
@@ -352,47 +363,172 @@ func (h *HugoSites) render(l logg.LevelLogger, config *BuildCfg) error {
continue
}
- siteRenderContext.outIdx = siteOutIdx
- siteRenderContext.sitesOutIdx = i
- i++
-
- select {
- case <-h.Done():
- return nil
- default:
- for _, s2 := range h.Sites {
- // We render site by site, but since the content is lazily rendered
- // and a site can "borrow" content from other sites, every site
- // needs this set.
- s2.rc = &siteRenderingContext{Format: renderFormat}
-
- if err := s2.preparePagesForRender(s == s2, siteRenderContext.sitesOutIdx); err != nil {
- return err
+ if err := func() error {
+ rc := tpl.RenderingContext{Site: s, SiteOutIdx: siteOutIdx}
+ h.BuildState.StartStageRender(rc)
+ defer h.BuildState.StopStageRender(rc)
+
+ siteRenderContext.outIdx = siteOutIdx
+ siteRenderContext.sitesOutIdx = i
+ i++
+
+ select {
+ case <-h.Done():
+ return nil
+ default:
+ for _, s2 := range h.Sites {
+ if err := s2.preparePagesForRender(s == s2, siteRenderContext.sitesOutIdx); err != nil {
+ return err
+ }
+ }
+ if !config.SkipRender {
+ ll := l.WithField("substep", "pages").
+ WithField("site", s.language.Lang).
+ WithField("outputFormat", renderFormat.Name)
+
+ start := time.Now()
+
+ if config.PartialReRender {
+ if err := s.renderPages(siteRenderContext); err != nil {
+ return err
+ }
+ } else {
+ if err := s.render(siteRenderContext); err != nil {
+ return err
+ }
+ }
+ loggers.TimeTrackf(ll, start, nil, "")
}
}
- if !config.SkipRender {
- ll := l.WithField("substep", "pages").
- WithField("site", s.language.Lang).
- WithField("outputFormat", renderFormat.Name)
+ return nil
+ }(); err != nil {
+ return err
+ }
- start := time.Now()
+ }
+ }
- if config.PartialReRender {
- if err := s.renderPages(siteRenderContext); err != nil {
- return err
- }
- } else {
- if err := s.render(siteRenderContext); err != nil {
+ return nil
+}
+
+func (h *HugoSites) renderDeferred(l logg.LevelLogger) error {
+ l = l.WithField("step", "render deferred")
+ start := time.Now()
+
+ var deferredCount int
+
+ for rc, de := range h.Deps.BuildState.DeferredExecutionsGroupedByRenderingContext {
+ if de.FilenamesWithPostPrefix.Len() == 0 {
+ continue
+ }
+
+ deferredCount += de.FilenamesWithPostPrefix.Len()
+
+ s := rc.Site.(*Site)
+ for _, s2 := range h.Sites {
+ if err := s2.preparePagesForRender(s == s2, rc.SiteOutIdx); err != nil {
+ return err
+ }
+ }
+ if err := s.executeDeferredTemplates(de); err != nil {
+ return herrors.ImproveRenderErr(err)
+ }
+ }
+
+ loggers.TimeTrackf(l, start, logg.Fields{
+ logg.Field{Name: "count", Value: deferredCount},
+ }, "")
+
+ return nil
+}
+
+func (s *Site) executeDeferredTemplates(de *deps.DeferredExecutions) error {
+ handleFile := func(filename string) error {
+ content, err := afero.ReadFile(s.BaseFs.PublishFs, filename)
+ if err != nil {
+ return err
+ }
+
+ k := 0
+ changed := false
+
+ for {
+ if k >= len(content) {
+ break
+ }
+ l := bytes.Index(content[k:], []byte(tpl.HugoDeferredTemplatePrefix))
+ if l == -1 {
+ break
+ }
+ m := bytes.Index(content[k+l:], []byte(tpl.HugoDeferredTemplateSuffix)) + len(tpl.HugoDeferredTemplateSuffix)
+
+ low, high := k+l, k+l+m
+
+ forward := l + m
+ id := string(content[low:high])
+
+ if err := func() error {
+ deferred, found := de.Executions.Get(id)
+ if !found {
+ panic(fmt.Sprintf("deferred execution with id %q not found", id))
+ }
+ deferred.Mu.Lock()
+ defer deferred.Mu.Unlock()
+
+ if !deferred.Executed {
+ tmpl := s.Deps.Tmpl()
+ templ, found := tmpl.Lookup(deferred.TemplateName)
+ if !found {
+ panic(fmt.Sprintf("template %q not found", deferred.TemplateName))
+ }
+
+ if err := func() error {
+ buf := bufferpool.GetBuffer()
+ defer bufferpool.PutBuffer(buf)
+
+ err = tmpl.ExecuteWithContext(deferred.Ctx, templ, buf, deferred.Data)
+ if err != nil {
return err
}
+ deferred.Result = buf.String()
+ deferred.Executed = true
+
+ return nil
+ }(); err != nil {
+ return err
}
- loggers.TimeTrackf(ll, start, nil, "")
}
+
+ content = append(content[:low], append([]byte(deferred.Result), content[high:]...)...)
+ changed = true
+
+ return nil
+ }(); err != nil {
+ return err
}
+
+ k += forward
}
+
+ if changed {
+ return afero.WriteFile(s.BaseFs.PublishFs, filename, content, 0o666)
+ }
+
+ return nil
}
- return nil
+ g := rungroup.Run[string](context.Background(), rungroup.Config[string]{
+ NumWorkers: s.h.numWorkers,
+ Handle: func(ctx context.Context, filename string) error {
+ return handleFile(filename)
+ },
+ })
+
+ de.FilenamesWithPostPrefix.ForEeach(func(filename string, _ bool) {
+ g.Enqueue(filename)
+ })
+
+ return g.Wait()
}
// / postRenderOnce runs some post processing that only needs to be done once, e.g. printing of unused templates.
@@ -428,12 +564,6 @@ func (h *HugoSites) postProcess(l logg.LevelLogger) error {
l = l.WithField("step", "postProcess")
defer loggers.TimeTrackf(l, time.Now(), nil, "")
- // Make sure to write any build stats to disk first so it's available
- // to the post processors.
- if err := h.writeBuildStats(); err != nil {
- return err
- }
-
// This will only be set when js.Build have been triggered with
// imports that resolves to the project or a module.
// Write a jsconfig.json file to the project's /asset directory
@@ -600,6 +730,10 @@ func (h *HugoSites) writeBuildStats() error {
}
}
+ // This step may be followed by a post process step that may
+ // rebuild e.g. CSS, so clear any cache that's defined for the hugo_stats.json.
+ h.dynacacheGCFilenameIfNotWatchedAndDrainMatching(filename)
+
return nil
}
diff --git a/hugolib/hugo_sites_build_errors_test.go b/hugolib/hugo_sites_build_errors_test.go
index 5a8b9f76f..71afe6767 100644
--- a/hugolib/hugo_sites_build_errors_test.go
+++ b/hugolib/hugo_sites_build_errors_test.go
@@ -628,3 +628,20 @@ title: "A page"
b.CreateSites().BuildFail(BuildCfg{})
}
+
+func TestErrorTemplateRuntime(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/index.html --
+Home.
+{{ .ThisDoesNotExist }}
+ `
+
+ b, err := TestE(t, files)
+
+ b.Assert(err, qt.Not(qt.IsNil))
+ b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`/layouts/index.html:2:3`))
+ b.Assert(err.Error(), qt.Contains, `can't evaluate field ThisDoesNotExist`)
+}
diff --git a/hugolib/site.go b/hugolib/site.go
index b4b89975d..2113c4f20 100644
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -62,7 +62,7 @@ import (
)
func (s *Site) Taxonomies() page.TaxonomyList {
- s.checkReady()
+ s.CheckReady()
s.init.taxonomies.Do(context.Background())
return s.taxonomies
}
@@ -200,12 +200,8 @@ func (s *Site) prepareInits() {
})
}
-type siteRenderingContext struct {
- output.Format
-}
-
func (s *Site) Menus() navigation.Menus {
- s.checkReady()
+ s.CheckReady()
s.init.menus.Do(context.Background())
return s.menus
}
@@ -810,7 +806,7 @@ func (s *Site) errorCollator(results <-chan error, errs chan<- error) {
// as possible for existing sites. Most sites will use {{ .Site.GetPage "section" "my/section" }},
// i.e. 2 arguments, so we test for that.
func (s *Site) GetPage(ref ...string) (page.Page, error) {
- s.checkReady()
+ s.CheckReady()
p, err := s.s.getPageForRefs(ref...)
if p == nil {
diff --git a/hugolib/site_new.go b/hugolib/site_new.go
index 2ba5ef2fb..cb6630cb3 100644
--- a/hugolib/site_new.go
+++ b/hugolib/site_new.go
@@ -88,10 +88,6 @@ type Site struct {
publisher publisher.Publisher
frontmatterHandler pagemeta.FrontMatterHandler
- // We render each site for all the relevant output formats in serial with
- // this rendering context pointing to the current one.
- rc *siteRenderingContext
-
// The output formats that we need to render this site in. This slice
// will be fixed once set.
// This will be the union of Site.Pages' outputFormats.
@@ -439,7 +435,7 @@ func (s *Site) Current() page.Site {
// MainSections returns the list of main sections.
func (s *Site) MainSections() []string {
- s.checkReady()
+ s.CheckReady()
return s.conf.C.MainSections
}
@@ -458,7 +454,7 @@ func (s *Site) BaseURL() string {
// Deprecated: Use .Site.Lastmod instead.
func (s *Site) LastChange() time.Time {
- s.checkReady()
+ s.CheckReady()
hugo.Deprecate(".Site.LastChange", "Use .Site.Lastmod instead.", "v0.123.0")
return s.lastmod
}
@@ -547,7 +543,7 @@ func (s *Site) ForEeachIdentityByName(name string, f func(identity.Identity) boo
// Pages returns all pages.
// This is for the current language only.
func (s *Site) Pages() page.Pages {
- s.checkReady()
+ s.CheckReady()
return s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
@@ -564,7 +560,7 @@ func (s *Site) Pages() page.Pages {
// RegularPages returns all the regular pages.
// This is for the current language only.
func (s *Site) RegularPages() page.Pages {
- s.checkReady()
+ s.CheckReady()
return s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
@@ -579,17 +575,17 @@ func (s *Site) RegularPages() page.Pages {
// AllPages returns all pages for all sites.
func (s *Site) AllPages() page.Pages {
- s.checkReady()
+ s.CheckReady()
return s.h.Pages()
}
// AllRegularPages returns all regular pages for all sites.
func (s *Site) AllRegularPages() page.Pages {
- s.checkReady()
+ s.CheckReady()
return s.h.RegularPages()
}
-func (s *Site) checkReady() {
+func (s *Site) CheckReady() {
if s.state != siteStateReady {
panic("this method cannot be called before the site is fully initialized")
}
diff --git a/hugolib/site_render.go b/hugolib/site_render.go
index a7ecf89af..83f2fce89 100644
--- a/hugolib/site_render.go
+++ b/hugolib/site_render.go
@@ -111,7 +111,7 @@ func (s *Site) renderPages(ctx *siteRenderContext) error {
err := <-errs
if err != nil {
- return fmt.Errorf("failed to render pages: %w", herrors.ImproveIfNilPointer(err))
+ return fmt.Errorf("failed to render pages: %w", herrors.ImproveRenderErr(err))
}
return nil
}
@@ -226,7 +226,7 @@ func (s *Site) renderPaginator(p *pageState, templ tpl.Template) error {
paginatePath := s.Conf.Pagination().Path
d := p.targetPathDescriptor
- f := p.s.rc.Format
+ f := p.outputFormat()
d.Type = f
if p.paginator.current == nil || p.paginator.current != p.paginator.current.First() {
diff --git a/hugolib/site_sections.go b/hugolib/site_sections.go
index 03d662b9f..385f3f291 100644
--- a/hugolib/site_sections.go
+++ b/hugolib/site_sections.go
@@ -19,12 +19,12 @@ import (
// Sections returns the top level sections.
func (s *Site) Sections() page.Pages {
- s.checkReady()
+ s.CheckReady()
return s.Home().Sections()
}
// Home is a shortcut to the home page, equivalent to .Site.GetPage "home".
func (s *Site) Home() page.Page {
- s.checkReady()
+ s.CheckReady()
return s.s.home
}