diff options
Diffstat (limited to 'hugolib/hugo_sites.go')
-rw-r--r-- | hugolib/hugo_sites.go | 560 |
1 files changed, 156 insertions, 404 deletions
diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index f3f5c3eb2..80e754453 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2024 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,27 +17,25 @@ import ( "context" "fmt" "io" - "path/filepath" - "sort" "strings" "sync" "sync/atomic" "github.com/bep/logg" + "github.com/gohugoio/hugo/cache/dynacache" "github.com/gohugoio/hugo/config/allconfig" "github.com/gohugoio/hugo/hugofs/glob" + "github.com/gohugoio/hugo/hugolib/doctree" "github.com/fsnotify/fsnotify" - "github.com/gohugoio/hugo/identity" - - radix "github.com/armon/go-radix" - "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/parser/metadecoders" "github.com/gohugoio/hugo/common/hugo" + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/para" + "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/source" @@ -47,9 +45,7 @@ import ( "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/lazy" - "github.com/gohugoio/hugo/resources/kinds" "github.com/gohugoio/hugo/resources/page" - "github.com/gohugoio/hugo/resources/page/pagemeta" ) // HugoSites represents the sites to build. Each site represents a language. @@ -74,13 +70,19 @@ type HugoSites struct { // As loaded from the /data dirs data map[string]any - contentInit sync.Once - content *pageMaps + // Cache for page listings. + cachePages *dynacache.Partition[string, page.Pages] - postRenderInit sync.Once + // Before Hugo 0.122.0 we managed all translations in a map using a translationKey + // that could be overridden in front matter. + // Now the different page dimensions (e.g. language) are built-in to the page trees above. + // But we sill need to support the overridden translationKey, but that should + // be relatively rare and low volume. + translationKeyPages *maps.SliceCache[page.Page] - // Keeps track of bundle directories and symlinks to enable partial rebuilding. - ContentChanges *contentChangeMap + pageTrees *pageTrees + + postRenderInit sync.Once // File change events with filename stored in this map will be skipped. skipRebuildForFilenamesMu sync.Mutex @@ -88,11 +90,12 @@ type HugoSites struct { init *hugoSitesInit - workers *para.Workers - numWorkers int + workersSite *para.Workers + numWorkersSites int + numWorkers int *fatalErrorHandler - *testCounters + *buildCounters } // ShouldSkipFileChangeEvent allows skipping filesystem event early before @@ -103,31 +106,17 @@ func (h *HugoSites) ShouldSkipFileChangeEvent(ev fsnotify.Event) bool { return h.skipRebuildForFilenames[ev.Name] } -func (h *HugoSites) getContentMaps() *pageMaps { - h.contentInit.Do(func() { - h.content = newPageMaps(h) - }) - return h.content -} - // Only used in tests. -type testCounters struct { - contentRenderCounter uint64 - pageRenderCounter uint64 +type buildCounters struct { + contentRenderCounter atomic.Uint64 + pageRenderCounter atomic.Uint64 } -func (h *testCounters) IncrContentRender() { - if h == nil { - return +func (c *buildCounters) loggFields() logg.Fields { + return logg.Fields{ + {Name: "pages", Value: c.pageRenderCounter.Load()}, + {Name: "content", Value: c.contentRenderCounter.Load()}, } - atomic.AddUint64(&h.contentRenderCounter, 1) -} - -func (h *testCounters) IncrPageRender() { - if h == nil { - return - } - atomic.AddUint64(&h.pageRenderCounter, 1) } type fatalErrorHandler struct { @@ -172,16 +161,6 @@ type hugoSitesInit struct { // Loads the Git info and CODEOWNERS for all the pages if enabled. gitInfo *lazy.Init - - // Maps page translations. - translations *lazy.Init -} - -func (h *hugoSitesInit) Reset() { - h.data.Reset() - h.layouts.Reset() - h.gitInfo.Reset() - h.translations.Reset() } func (h *HugoSites) Data() map[string]any { @@ -192,6 +171,41 @@ func (h *HugoSites) Data() map[string]any { return h.data } +// Pages returns all pages for all sites. +func (h *HugoSites) Pages() page.Pages { + key := "pages" + v, err := h.cachePages.GetOrCreate(key, func(string) (page.Pages, error) { + var pages page.Pages + for _, s := range h.Sites { + pages = append(pages, s.Pages()...) + } + page.SortByDefault(pages) + return pages, nil + }) + if err != nil { + panic(err) + } + return v +} + +// Pages returns all regularpages for all sites. +func (h *HugoSites) RegularPages() page.Pages { + key := "regular-pages" + v, err := h.cachePages.GetOrCreate(key, func(string) (page.Pages, error) { + var pages page.Pages + for _, s := range h.Sites { + pages = append(pages, s.RegularPages()...) + } + page.SortByDefault(pages) + + return pages, nil + }) + if err != nil { + panic(err) + } + return v +} + func (h *HugoSites) gitInfoForPage(p page.Page) (source.GitInfo, error) { if _, err := h.init.gitInfo.Do(context.Background()); err != nil { return source.GitInfo{}, err @@ -283,16 +297,24 @@ func (h *HugoSites) PrintProcessingStats(w io.Writer) { func (h *HugoSites) GetContentPage(filename string) page.Page { var p page.Page - h.getContentMaps().walkBundles(func(b *contentNode) bool { - if b.p == nil || b.fi == nil { + h.withPage(func(s string, p2 *pageState) bool { + if p2.File() == nil { return false } - if b.fi.Meta().Filename == filename { - p = b.p + if p2.File().FileInfo().Meta().Filename == filename { + p = p2 return true } + for _, r := range p2.Resources().ByType(pageResourceType) { + p3 := r.(page.Page) + if p3.File() != nil && p3.File().FileInfo().Meta().Filename == filename { + p = p3 + return true + } + } + return false }) @@ -320,20 +342,10 @@ func (h *HugoSites) loadGitInfo() error { // Reset resets the sites and template caches etc., making it ready for a full rebuild. func (h *HugoSites) reset(config *BuildCfg) { - if config.ResetState { - for _, s := range h.Sites { - if r, ok := s.Fs.PublishDir.(hugofs.Reseter); ok { - r.Reset() - } - } - } - h.fatalErrorHandler = &fatalErrorHandler{ h: h, donec: make(chan bool), } - - h.init.Reset() } // resetLogs resets the log counters etc. Used to do a new build on the same sites. @@ -345,43 +357,42 @@ func (h *HugoSites) resetLogs() { } func (h *HugoSites) withSite(fn func(s *Site) error) error { - if h.workers == nil { - for _, s := range h.Sites { - if err := fn(s); err != nil { - return err - } + for _, s := range h.Sites { + if err := fn(s); err != nil { + return err } - return nil } + return nil +} - g, _ := h.workers.Start(context.Background()) - for _, s := range h.Sites { - s := s - g.Run(func() error { - return fn(s) - }) - } - return g.Wait() +func (h *HugoSites) withPage(fn func(s string, p *pageState) bool) { + h.withSite(func(s *Site) error { + w := &doctree.NodeShiftTreeWalker[contentNodeI]{ + Tree: s.pageMap.treePages, + LockType: doctree.LockTypeRead, + Handle: func(s string, n contentNodeI, match doctree.DimensionFlag) (bool, error) { + return fn(s, n.(*pageState)), nil + }, + } + return w.Walk(context.Background()) + }) } // BuildCfg holds build options used to, as an example, skip the render step. type BuildCfg struct { - // Reset site state before build. Use to force full rebuilds. - ResetState bool // Skip rendering. Useful for testing. SkipRender bool // Use this to indicate what changed (for rebuilds). whatChanged *whatChanged - // This is a partial re-render of some selected pages. This means - // we should skip most of the processing. + // This is a partial re-render of some selected pages. PartialReRender bool // Set in server mode when the last build failed for some reason. ErrRecovery bool // Recently visited URLs. This is used for partial re-rendering. - RecentlyVisited map[string]bool + RecentlyVisited *types.EvictingStringQueue // Can be set to build only with a sub set of the content source. ContentInclusionFilter *glob.FilenameFilter @@ -389,174 +400,95 @@ type BuildCfg struct { // Set when the buildlock is already acquired (e.g. the archetype content builder). NoBuildLock bool - testCounters *testCounters + testCounters *buildCounters } -// shouldRender is used in the Fast Render Mode to determine if we need to re-render -// a Page: If it is recently visited (the home pages will always be in this set) or changed. -// Note that a page does not have to have a content page / file. -// For regular builds, this will always return true. -// TODO(bep) rename/work this. +// shouldRender returns whether this output format should be rendered or not. func (cfg *BuildCfg) shouldRender(p *pageState) bool { - if p == nil { - return false - } - - if p.forceRender { - return true - } - - if len(cfg.RecentlyVisited) == 0 { - return true - } - - if cfg.RecentlyVisited[p.RelPermalink()] { + if !p.renderOnce { return true } - if cfg.whatChanged != nil && !p.File().IsZero() { - return cfg.whatChanged.files[p.File().Filename()] - } - - return false -} - -func (h *HugoSites) renderCrossSitesSitemap() error { - if h.Conf.IsMultihost() || !(h.Conf.DefaultContentLanguageInSubdir() || h.Conf.IsMultiLingual()) { - return nil - } - - sitemapEnabled := false - for _, s := range h.Sites { - if s.conf.IsKindEnabled(kinds.KindSitemap) { - sitemapEnabled = true - break - } - } - - if !sitemapEnabled { - return nil - } + // The render state is incremented on render and reset when a related change is detected. + // Note that this is set per output format. + shouldRender := p.renderState == 0 - s := h.Sites[0] - // We don't have any page context to pass in here. - ctx := context.Background() - - templ := s.lookupLayouts("sitemapindex.xml", "_default/sitemapindex.xml", "_internal/_default/sitemapindex.xml") - return s.renderAndWriteXML(ctx, &s.PathSpec.ProcessingStats.Sitemaps, "sitemapindex", - s.conf.Sitemap.Filename, h.Sites, templ) -} - -func (h *HugoSites) renderCrossSitesRobotsTXT() error { - if h.Configs.IsMultihost { - return nil - } - if !h.Configs.Base.EnableRobotsTXT { - return nil + if !shouldRender { + return false } - s := h.Sites[0] + fastRenderMode := cfg.RecentlyVisited.Len() > 0 - p, err := newPageStandalone(&pageMeta{ - s: s, - kind: kinds.KindRobotsTXT, - urlPaths: pagemeta.URLPath{ - URL: "robots.txt", - }, - }, - output.RobotsTxtFormat) - if err != nil { - return err + if !fastRenderMode { + // Not in fast render mode or first time render. + return shouldRender } if !p.render { - return nil + // Not be to rendered for this output format. + return false } - templ := s.lookupLayouts("robots.txt", "_default/robots.txt", "_internal/_default/robots.txt") - - return s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "Robots Txt", "robots.txt", p, templ) -} - -func (h *HugoSites) removePageByFilename(filename string) { - h.getContentMaps().withMaps(func(m *pageMap) error { - m.deleteBundleMatching(func(b *contentNode) bool { - if b.p == nil { - return false - } - - if b.fi == nil { - return false - } - - return b.fi.Meta().Filename == filename - }) - return nil - }) -} + if p.outputFormat().IsHTML { + // This is fast render mode and the output format is HTML, + // rerender if this page is one of the recently visited. + return cfg.RecentlyVisited.Contains(p.RelPermalink()) + } -func (h *HugoSites) createPageCollections() error { - allPages := newLazyPagesFactory(func() page.Pages { - var pages page.Pages - for _, s := range h.Sites { - pages = append(pages, s.Pages()...) + // In fast render mode, we want to avoid re-rendering the sitemaps etc. and + // other big listings whenever we e.g. change a content file, + // but we want partial renders of the recently visited pages to also include + // alternative formats of the same HTML page (e.g. RSS, JSON). + for _, po := range p.pageOutputs { + if po.render && po.f.IsHTML && cfg.RecentlyVisited.Contains(po.RelPermalink()) { + return true } - - page.SortByDefault(pages) - - return pages - }) - - allRegularPages := newLazyPagesFactory(func() page.Pages { - return h.findPagesByKindIn(kinds.KindPage, allPages.get()) - }) - - for _, s := range h.Sites { - s.PageCollections.allPages = allPages - s.PageCollections.allRegularPages = allRegularPages } - return nil + return false } func (s *Site) preparePagesForRender(isRenderingSite bool, idx int) error { var err error - s.pageMap.withEveryBundlePage(func(p *pageState) bool { - if err = p.initOutputFormat(isRenderingSite, idx); err != nil { - return true + + initPage := func(p *pageState) error { + if err = p.shiftToOutputFormat(isRenderingSite, idx); err != nil { + return err } - return false - }) - return nil -} + return nil + } -// Pages returns all pages for all sites. -func (h *HugoSites) Pages() page.Pages { - return h.Sites[0].AllPages() + return s.pageMap.forEeachPageIncludingBundledPages(nil, + func(p *pageState) (bool, error) { + return false, initPage(p) + }, + ) } -func (h *HugoSites) loadData(fis []hugofs.FileMetaInfo) (err error) { - spec := source.NewSourceSpec(h.PathSpec, nil, nil) - +func (h *HugoSites) loadData() error { h.data = make(map[string]any) - for _, fi := range fis { - basePath := fi.Meta().Path - fileSystem := spec.NewFilesystemFromFileMetaInfo(fi) - files, err := fileSystem.Files() - if err != nil { - return err - } - for _, r := range files { - if err := h.handleDataFile(basePath, r); err != nil { - return err - } - } - } + w := hugofs.NewWalkway( + hugofs.WalkwayConfig{ + Fs: h.PathSpec.BaseFs.Data.Fs, + WalkFn: func(path string, fi hugofs.FileMetaInfo) error { + if fi.IsDir() { + return nil + } + pi := fi.Meta().PathInfo + if pi == nil { + panic("no path info") + } + return h.handleDataFile(source.NewFileInfo(fi)) + }, + }) - return + if err := w.Walk(); err != nil { + return err + } + return nil } -func (h *HugoSites) handleDataFile(basePath string, r source.File) error { +func (h *HugoSites) handleDataFile(r *source.File) error { var current map[string]any f, err := r.FileInfo().Meta().Open() @@ -567,8 +499,8 @@ func (h *HugoSites) handleDataFile(basePath string, r source.File) error { // Crawl in data tree to insert data current = h.data - dataPath := filepath.Join(basePath, r.Dir()) - keyParts := strings.Split(dataPath, helpers.FilePathSeparator) + dataPath := r.FileInfo().Meta().PathInfo.Dir()[1:] + keyParts := strings.Split(dataPath, "/") for _, key := range keyParts { if key != "" { @@ -635,17 +567,12 @@ func (h *HugoSites) handleDataFile(basePath string, r source.File) error { return nil } -func (h *HugoSites) errWithFileContext(err error, f source.File) error { - fim, ok := f.FileInfo().(hugofs.FileMetaInfo) - if !ok { - return err - } - realFilename := fim.Meta().Filename - - return herrors.NewFileErrorFromFile(err, realFilename, h.SourceSpec.Fs.Source, nil) +func (h *HugoSites) errWithFileContext(err error, f *source.File) error { + realFilename := f.FileInfo().Meta().Filename + return herrors.NewFileErrorFromFile(err, realFilename, h.Fs.Source, nil) } -func (h *HugoSites) readData(f source.File) (any, error) { +func (h *HugoSites) readData(f *source.File) (any, error) { file, err := f.FileInfo().Meta().Open() if err != nil { return nil, fmt.Errorf("readData: failed to open data file: %w", err) @@ -656,178 +583,3 @@ func (h *HugoSites) readData(f source.File) (any, error) { format := metadecoders.FormatFromString(f.Ext()) return metadecoders.Default.Unmarshal(content, format) } - -func (h *HugoSites) findPagesByKindIn(kind string, inPages page.Pages) page.Pages { - return h.Sites[0].findPagesByKindIn(kind, inPages) -} - -func (h *HugoSites) resetPageState() { - h.getContentMaps().walkBundles(func(n *contentNode) bool { - if n.p == nil { - return false - } - p := n.p - for _, po := range p.pageOutputs { - if po.cp == nil { - continue - } - po.cp.Reset() - } - - return false - }) -} - -func (h *HugoSites) resetPageStateFromEvents(idset identity.Identities) { - h.getContentMaps().walkBundles(func(n *contentNode) bool { - if n.p == nil { - return false - } - p := n.p - OUTPUTS: - for _, po := range p.pageOutputs { - if po.cp == nil { - continue - } - for id := range idset { - if po.cp.dependencyTracker.Search(id) != nil { - po.cp.Reset() - continue OUTPUTS - } - } - } - - if p.shortcodeState == nil { - return false - } - - for _, s := range p.shortcodeState.shortcodes { - for _, templ := range s.templs { - sid := templ.(identity.Manager) - for id := range idset { - if sid.Search(id) != nil { - for _, po := range p.pageOutputs { - if po.cp != nil { - po.cp.Reset() - } - } - return false - } - } - } - } - return false - }) -} - -// Used in partial reloading to determine if the change is in a bundle. -type contentChangeMap struct { - mu sync.RWMutex - - // Holds directories with leaf bundles. - leafBundles *radix.Tree - - // Holds directories with branch bundles. - branchBundles map[string]bool - - pathSpec *helpers.PathSpec - - // Hugo supports symlinked content (both directories and files). This - // can lead to situations where the same file can be referenced from several - // locations in /content -- which is really cool, but also means we have to - // go an extra mile to handle changes. - // This map is only used in watch mode. - // It maps either file to files or the real dir to a set of content directories - // where it is in use. - symContentMu sync.Mutex - symContent map[string]map[string]bool -} - -func (m *contentChangeMap) add(dirname string, tp bundleDirType) { - m.mu.Lock() - if !strings.HasSuffix(dirname, helpers.FilePathSeparator) { - dirname += helpers.FilePathSeparator - } - switch tp { - case bundleBranch: - m.branchBundles[dirname] = true - case bundleLeaf: - m.leafBundles.Insert(dirname, true) - default: - m.mu.Unlock() - panic("invalid bundle type") - } - m.mu.Unlock() -} - -func (m *contentChangeMap) resolveAndRemove(filename string) (string, bundleDirType) { - m.mu.RLock() - defer m.mu.RUnlock() - - // Bundles share resources, so we need to start from the virtual root. - relFilename := m.pathSpec.RelContentDir(filename) - dir, name := filepath.Split(relFilename) - if !strings.HasSuffix(dir, helpers.FilePathSeparator) { - dir += helpers.FilePathSeparator - } - - if _, found := m.branchBundles[dir]; found { - delete(m.branchBundles, dir) - return dir, bundleBranch - } - - if key, _, found := m.leafBundles.LongestPrefix(dir); found { - m.leafBundles.Delete(key) - dir = string(key) - return dir, bundleLeaf - } - - fileTp, isContent := classifyBundledFile(name) - if isContent && fileTp != bundleNot { - // A new bundle. - return dir, fileTp - } - - return dir, bundleNot -} - -func (m *contentChangeMap) addSymbolicLinkMapping(fim hugofs.FileMetaInfo) { - meta := fim.Meta() - if !meta.IsSymlink { - return - } - m.symContentMu.Lock() - - from, to := meta.Filename, meta.OriginalFilename - if fim.IsDir() { - if !strings.HasSuffix(from, helpers.FilePathSeparator) { - from += helpers.FilePathSeparator - } - } - - mm, found := m.symContent[from] - - if !found { - mm = make(map[string]bool) - m.symContent[from] = mm - } - mm[to] = true - m.symContentMu.Unlock() -} - -func (m *contentChangeMap) GetSymbolicLinkMappings(dir string) []string { - mm, found := m.symContent[dir] - if !found { - return nil - } - dirs := make([]string, len(mm)) - i := 0 - for dir := range mm { - dirs[i] = dir - i++ - } - - sort.Strings(dirs) - - return dirs -} |