github.com/neohugo/neohugo@v0.123.8/hugolib/filesystems/basefs.go (about)

     1  // Copyright 2024 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 filesystems provides the fine grained file systems used by Hugo. These
    15  // are typically virtual filesystems that are composites of project and theme content.
    16  package filesystems
    17  
    18  import (
    19  	"fmt"
    20  	"io"
    21  	"os"
    22  	"path/filepath"
    23  	"strings"
    24  	"sync"
    25  
    26  	"github.com/bep/overlayfs"
    27  	"github.com/neohugo/neohugo/config"
    28  	"github.com/neohugo/neohugo/htesting"
    29  	"github.com/neohugo/neohugo/hugofs/glob"
    30  
    31  	"github.com/neohugo/neohugo/common/herrors"
    32  	"github.com/neohugo/neohugo/common/loggers"
    33  	"github.com/neohugo/neohugo/common/types"
    34  
    35  	"github.com/rogpeppe/go-internal/lockedfile"
    36  
    37  	"github.com/neohugo/neohugo/hugofs/files"
    38  
    39  	"github.com/neohugo/neohugo/modules"
    40  
    41  	hpaths "github.com/neohugo/neohugo/common/paths"
    42  	"github.com/neohugo/neohugo/hugofs"
    43  	"github.com/neohugo/neohugo/hugolib/paths"
    44  	"github.com/spf13/afero"
    45  )
    46  
    47  const (
    48  	// Used to control concurrency between multiple Hugo instances, e.g.
    49  	// a running server and building new content with 'hugo new'.
    50  	// It's placed in the project root.
    51  	lockFileBuild = ".hugo_build.lock"
    52  )
    53  
    54  var filePathSeparator = string(filepath.Separator)
    55  
    56  // BaseFs contains the core base filesystems used by Hugo. The name "base" is used
    57  // to underline that even if they can be composites, they all have a base path set to a specific
    58  // resource folder, e.g "/my-project/content". So, no absolute filenames needed.
    59  type BaseFs struct {
    60  	// SourceFilesystems contains the different source file systems.
    61  	*SourceFilesystems
    62  
    63  	// The source filesystem (needs absolute filenames).
    64  	SourceFs afero.Fs
    65  
    66  	// The project source.
    67  	ProjectSourceFs afero.Fs
    68  
    69  	// The filesystem used to publish the rendered site.
    70  	// This usually maps to /my-project/public.
    71  	PublishFs afero.Fs
    72  
    73  	// The filesystem used for static files.
    74  	PublishFsStatic afero.Fs
    75  
    76  	// A read-only filesystem starting from the project workDir.
    77  	WorkDir afero.Fs
    78  
    79  	theBigFs *filesystemsCollector
    80  
    81  	workingDir string
    82  
    83  	// Locks.
    84  	buildMu Lockable // <project>/.hugo_build.lock
    85  }
    86  
    87  type Lockable interface {
    88  	Lock() (unlock func(), err error)
    89  }
    90  
    91  type fakeLockfileMutex struct {
    92  	mu sync.Mutex
    93  }
    94  
    95  func (f *fakeLockfileMutex) Lock() (func(), error) {
    96  	f.mu.Lock()
    97  	return func() { f.mu.Unlock() }, nil
    98  }
    99  
   100  // Tries to acquire a build lock.
   101  func (b *BaseFs) LockBuild() (unlock func(), err error) {
   102  	return b.buildMu.Lock()
   103  }
   104  
   105  func (b *BaseFs) WatchFilenames() []string {
   106  	var filenames []string
   107  	sourceFs := b.SourceFs
   108  
   109  	for _, rfs := range b.RootFss {
   110  		for _, component := range files.ComponentFolders {
   111  			fis, err := rfs.Mounts(component)
   112  			if err != nil {
   113  				continue
   114  			}
   115  
   116  			for _, fim := range fis {
   117  				meta := fim.Meta()
   118  				if !meta.Watch {
   119  					continue
   120  				}
   121  
   122  				if !fim.IsDir() {
   123  					filenames = append(filenames, meta.Filename)
   124  					continue
   125  				}
   126  
   127  				w := hugofs.NewWalkway(hugofs.WalkwayConfig{
   128  					Fs:   sourceFs,
   129  					Root: meta.Filename,
   130  					WalkFn: func(path string, fi hugofs.FileMetaInfo) error {
   131  						if !fi.IsDir() {
   132  							return nil
   133  						}
   134  						if fi.Name() == ".git" ||
   135  							fi.Name() == "node_modules" || fi.Name() == "bower_components" {
   136  							return filepath.SkipDir
   137  						}
   138  						filenames = append(filenames, fi.Meta().Filename)
   139  						return nil
   140  					},
   141  				})
   142  
   143  				w.Walk() // nolint
   144  			}
   145  
   146  		}
   147  	}
   148  
   149  	return filenames
   150  }
   151  
   152  func (b *BaseFs) mountsForComponent(component string) []hugofs.FileMetaInfo {
   153  	var result []hugofs.FileMetaInfo
   154  	for _, rfs := range b.RootFss {
   155  		dirs, err := rfs.Mounts(component)
   156  		if err == nil {
   157  			result = append(result, dirs...)
   158  		}
   159  	}
   160  	return result
   161  }
   162  
   163  // AbsProjectContentDir tries to construct a filename below the most
   164  // relevant content directory.
   165  func (b *BaseFs) AbsProjectContentDir(filename string) (string, string, error) {
   166  	isAbs := filepath.IsAbs(filename)
   167  	for _, fi := range b.mountsForComponent(files.ComponentFolderContent) {
   168  		if !fi.IsDir() {
   169  			continue
   170  		}
   171  		meta := fi.Meta()
   172  		if !meta.IsProject {
   173  			continue
   174  		}
   175  
   176  		if isAbs {
   177  			if strings.HasPrefix(filename, meta.Filename) {
   178  				return strings.TrimPrefix(filename, meta.Filename+filePathSeparator), filename, nil
   179  			}
   180  		} else {
   181  			contentDir := strings.TrimPrefix(strings.TrimPrefix(meta.Filename, meta.BaseDir), filePathSeparator) + filePathSeparator
   182  
   183  			if strings.HasPrefix(filename, contentDir) {
   184  				relFilename := strings.TrimPrefix(filename, contentDir)
   185  				absFilename := filepath.Join(meta.Filename, relFilename)
   186  				return relFilename, absFilename, nil
   187  			}
   188  		}
   189  
   190  	}
   191  
   192  	if !isAbs {
   193  		// A filename on the form "posts/mypage.md", put it inside
   194  		// the first content folder, usually <workDir>/content.
   195  		// Pick the first project dir (which is probably the most important one).
   196  		for _, dir := range b.SourceFilesystems.Content.mounts() {
   197  			if !dir.IsDir() {
   198  				continue
   199  			}
   200  			meta := dir.Meta()
   201  			if meta.IsProject {
   202  				return filename, filepath.Join(meta.Filename, filename), nil
   203  			}
   204  		}
   205  	}
   206  
   207  	return "", "", fmt.Errorf("could not determine content directory for %q", filename)
   208  }
   209  
   210  // ResolveJSConfigFile resolves the JS-related config file to a absolute
   211  // filename. One example of such would be postcss.config.js.
   212  func (b *BaseFs) ResolveJSConfigFile(name string) string {
   213  	// First look in assets/_jsconfig
   214  	fi, err := b.Assets.Fs.Stat(filepath.Join(files.FolderJSConfig, name))
   215  	if err == nil {
   216  		return fi.(hugofs.FileMetaInfo).Meta().Filename
   217  	}
   218  	// Fall back to the work dir.
   219  	fi, err = b.Work.Stat(name)
   220  	if err == nil {
   221  		return fi.(hugofs.FileMetaInfo).Meta().Filename
   222  	}
   223  
   224  	return ""
   225  }
   226  
   227  // SourceFilesystems contains the different source file systems. These can be
   228  // composite file systems (theme and project etc.), and they have all root
   229  // set to the source type the provides: data, i18n, static, layouts.
   230  type SourceFilesystems struct {
   231  	Content    *SourceFilesystem
   232  	Data       *SourceFilesystem
   233  	I18n       *SourceFilesystem
   234  	Layouts    *SourceFilesystem
   235  	Archetypes *SourceFilesystem
   236  	Assets     *SourceFilesystem
   237  
   238  	AssetsWithDuplicatesPreserved *SourceFilesystem
   239  
   240  	RootFss []*hugofs.RootMappingFs
   241  
   242  	// Writable filesystem on top the project's resources directory,
   243  	// with any sub module's resource fs layered below.
   244  	ResourcesCache afero.Fs
   245  
   246  	// The work folder (may be a composite of project and theme components).
   247  	Work afero.Fs
   248  
   249  	// When in multihost we have one static filesystem per language. The sync
   250  	// static files is currently done outside of the Hugo build (where there is
   251  	// a concept of a site per language).
   252  	// When in non-multihost mode there will be one entry in this map with a blank key.
   253  	Static map[string]*SourceFilesystem
   254  
   255  	conf config.AllProvider
   256  }
   257  
   258  // A SourceFilesystem holds the filesystem for a given source type in Hugo (data,
   259  // i18n, layouts, static) and additional metadata to be able to use that filesystem
   260  // in server mode.
   261  type SourceFilesystem struct {
   262  	// Name matches one in files.ComponentFolders
   263  	Name string
   264  
   265  	// This is a virtual composite filesystem. It expects path relative to a context.
   266  	Fs afero.Fs
   267  
   268  	// The source filesystem (usually the OS filesystem).
   269  	SourceFs afero.Fs
   270  
   271  	// When syncing a source folder to the target (e.g. /public), this may
   272  	// be set to publish into a subfolder. This is used for static syncing
   273  	// in multihost mode.
   274  	PublishFolder string
   275  }
   276  
   277  // StaticFs returns the static filesystem for the given language.
   278  // This can be a composite filesystem.
   279  func (s SourceFilesystems) StaticFs(lang string) afero.Fs {
   280  	var staticFs afero.Fs = hugofs.NoOpFs
   281  
   282  	if fs, ok := s.Static[lang]; ok {
   283  		staticFs = fs.Fs
   284  	} else if fs, ok := s.Static[""]; ok {
   285  		staticFs = fs.Fs
   286  	}
   287  
   288  	return staticFs
   289  }
   290  
   291  // StatResource looks for a resource in these filesystems in order: static, assets and finally content.
   292  // If found in any of them, it returns FileInfo and the relevant filesystem.
   293  // Any non herrors.IsNotExist error will be returned.
   294  // An herrors.IsNotExist error will be returned only if all filesystems return such an error.
   295  // Note that if we only wanted to find the file, we could create a composite Afero fs,
   296  // but we also need to know which filesystem root it lives in.
   297  func (s SourceFilesystems) StatResource(lang, filename string) (fi os.FileInfo, fs afero.Fs, err error) {
   298  	for _, fsToCheck := range []afero.Fs{s.StaticFs(lang), s.Assets.Fs, s.Content.Fs} {
   299  		fs = fsToCheck
   300  		fi, err = fs.Stat(filename)
   301  		if err == nil || !herrors.IsNotExist(err) {
   302  			return
   303  		}
   304  	}
   305  	// Not found.
   306  	return
   307  }
   308  
   309  // IsStatic returns true if the given filename is a member of one of the static
   310  // filesystems.
   311  func (s SourceFilesystems) IsStatic(filename string) bool {
   312  	for _, staticFs := range s.Static {
   313  		if staticFs.Contains(filename) {
   314  			return true
   315  		}
   316  	}
   317  	return false
   318  }
   319  
   320  // IsContent returns true if the given filename is a member of the content filesystem.
   321  func (s SourceFilesystems) IsContent(filename string) bool {
   322  	return s.Content.Contains(filename)
   323  }
   324  
   325  // ResolvePaths resolves the given filename to a list of paths in the filesystems.
   326  func (s *SourceFilesystems) ResolvePaths(filename string) []hugofs.ComponentPath {
   327  	var cpss []hugofs.ComponentPath
   328  	for _, rfs := range s.RootFss {
   329  		cps, err := rfs.ReverseLookup(filename)
   330  		if err != nil {
   331  			panic(err)
   332  		}
   333  		cpss = append(cpss, cps...)
   334  	}
   335  	return cpss
   336  }
   337  
   338  // MakeStaticPathRelative makes an absolute static filename into a relative one.
   339  // It will return an empty string if the filename is not a member of a static filesystem.
   340  func (s SourceFilesystems) MakeStaticPathRelative(filename string) string {
   341  	for _, staticFs := range s.Static {
   342  		rel, _ := staticFs.MakePathRelative(filename, true)
   343  		if rel != "" {
   344  			return rel
   345  		}
   346  	}
   347  	return ""
   348  }
   349  
   350  // MakePathRelative creates a relative path from the given filename.
   351  func (d *SourceFilesystem) MakePathRelative(filename string, checkExists bool) (string, bool) {
   352  	cps, err := d.ReverseLookup(filename, checkExists)
   353  	if err != nil {
   354  		panic(err)
   355  	}
   356  	if len(cps) == 0 {
   357  		return "", false
   358  	}
   359  
   360  	return filepath.FromSlash(cps[0].Path), true
   361  }
   362  
   363  // ReverseLookup returns the component paths for the given filename.
   364  func (d *SourceFilesystem) ReverseLookup(filename string, checkExists bool) ([]hugofs.ComponentPath, error) {
   365  	var cps []hugofs.ComponentPath
   366  	hugofs.WalkFilesystems(d.Fs, func(fs afero.Fs) bool {
   367  		if rfs, ok := fs.(hugofs.ReverseLookupProvder); ok {
   368  			if c, err := rfs.ReverseLookupComponent(d.Name, filename); err == nil {
   369  				if checkExists {
   370  					n := 0
   371  					for _, cp := range c {
   372  						if _, err := d.Fs.Stat(filepath.FromSlash(cp.Path)); err == nil {
   373  							c[n] = cp
   374  							n++
   375  						}
   376  					}
   377  					c = c[:n]
   378  				}
   379  				cps = append(cps, c...)
   380  			}
   381  		}
   382  		return false
   383  	})
   384  	return cps, nil
   385  }
   386  
   387  func (d *SourceFilesystem) mounts() []hugofs.FileMetaInfo {
   388  	var m []hugofs.FileMetaInfo
   389  	hugofs.WalkFilesystems(d.Fs, func(fs afero.Fs) bool {
   390  		if rfs, ok := fs.(*hugofs.RootMappingFs); ok {
   391  			mounts, err := rfs.Mounts(d.Name)
   392  			if err == nil {
   393  				m = append(m, mounts...)
   394  			}
   395  		}
   396  		return false
   397  	})
   398  
   399  	// Filter out any mounts not belonging to this filesystem.
   400  	// TODO(bep) I think this is superflous.
   401  	n := 0
   402  	for _, mm := range m {
   403  		if mm.Meta().Component == d.Name {
   404  			m[n] = mm
   405  			n++
   406  		}
   407  	}
   408  	m = m[:n]
   409  
   410  	return m
   411  }
   412  
   413  func (d *SourceFilesystem) RealFilename(rel string) string {
   414  	fi, err := d.Fs.Stat(rel)
   415  	if err != nil {
   416  		return rel
   417  	}
   418  	if realfi, ok := fi.(hugofs.FileMetaInfo); ok {
   419  		return realfi.Meta().Filename
   420  	}
   421  
   422  	return rel
   423  }
   424  
   425  // Contains returns whether the given filename is a member of the current filesystem.
   426  func (d *SourceFilesystem) Contains(filename string) bool {
   427  	for _, dir := range d.mounts() {
   428  		if !dir.IsDir() {
   429  			continue
   430  		}
   431  		if strings.HasPrefix(filename, dir.Meta().Filename) {
   432  			return true
   433  		}
   434  	}
   435  	return false
   436  }
   437  
   438  // RealDirs gets a list of absolute paths to directories starting from the given
   439  // path.
   440  func (d *SourceFilesystem) RealDirs(from string) []string {
   441  	var dirnames []string
   442  	for _, m := range d.mounts() {
   443  		if !m.IsDir() {
   444  			continue
   445  		}
   446  		dirname := filepath.Join(m.Meta().Filename, from)
   447  		if _, err := d.SourceFs.Stat(dirname); err == nil {
   448  			dirnames = append(dirnames, dirname)
   449  		}
   450  	}
   451  	return dirnames
   452  }
   453  
   454  // WithBaseFs allows reuse of some potentially expensive to create parts that remain
   455  // the same across sites/languages.
   456  func WithBaseFs(b *BaseFs) func(*BaseFs) error {
   457  	return func(bb *BaseFs) error {
   458  		bb.theBigFs = b.theBigFs
   459  		bb.SourceFilesystems = b.SourceFilesystems
   460  		return nil
   461  	}
   462  }
   463  
   464  // NewBase builds the filesystems used by Hugo given the paths and options provided.NewBase
   465  func NewBase(p *paths.Paths, logger loggers.Logger, options ...func(*BaseFs) error) (*BaseFs, error) {
   466  	fs := p.Fs
   467  	if logger == nil {
   468  		logger = loggers.NewDefault()
   469  	}
   470  
   471  	publishFs := hugofs.NewBaseFileDecorator(fs.PublishDir)
   472  	projectSourceFs := hugofs.NewBaseFileDecorator(hugofs.NewBasePathFs(fs.Source, p.Cfg.BaseConfig().WorkingDir))
   473  	sourceFs := hugofs.NewBaseFileDecorator(fs.Source)
   474  	publishFsStatic := fs.PublishDirStatic
   475  
   476  	var buildMu Lockable
   477  	if p.Cfg.NoBuildLock() || htesting.IsTest {
   478  		buildMu = &fakeLockfileMutex{}
   479  	} else {
   480  		buildMu = lockedfile.MutexAt(filepath.Join(p.Cfg.BaseConfig().WorkingDir, lockFileBuild))
   481  	}
   482  
   483  	b := &BaseFs{
   484  		SourceFs:        sourceFs,
   485  		ProjectSourceFs: projectSourceFs,
   486  		WorkDir:         fs.WorkingDirReadOnly,
   487  		PublishFs:       publishFs,
   488  		PublishFsStatic: publishFsStatic,
   489  		workingDir:      p.Cfg.BaseConfig().WorkingDir,
   490  		buildMu:         buildMu,
   491  	}
   492  
   493  	for _, opt := range options {
   494  		if err := opt(b); err != nil {
   495  			return nil, err
   496  		}
   497  	}
   498  
   499  	if b.theBigFs != nil && b.SourceFilesystems != nil {
   500  		return b, nil
   501  	}
   502  
   503  	builder := newSourceFilesystemsBuilder(p, logger, b)
   504  	sourceFilesystems, err := builder.Build()
   505  	if err != nil {
   506  		return nil, fmt.Errorf("build filesystems: %w", err)
   507  	}
   508  
   509  	b.SourceFilesystems = sourceFilesystems
   510  	b.theBigFs = builder.theBigFs
   511  
   512  	return b, nil
   513  }
   514  
   515  type sourceFilesystemsBuilder struct {
   516  	logger   loggers.Logger
   517  	p        *paths.Paths
   518  	sourceFs afero.Fs
   519  	result   *SourceFilesystems
   520  	theBigFs *filesystemsCollector
   521  }
   522  
   523  func newSourceFilesystemsBuilder(p *paths.Paths, logger loggers.Logger, b *BaseFs) *sourceFilesystemsBuilder {
   524  	sourceFs := hugofs.NewBaseFileDecorator(p.Fs.Source)
   525  	return &sourceFilesystemsBuilder{
   526  		p: p, logger: logger, sourceFs: sourceFs, theBigFs: b.theBigFs,
   527  		result: &SourceFilesystems{
   528  			conf: p.Cfg,
   529  		},
   530  	}
   531  }
   532  
   533  func (b *sourceFilesystemsBuilder) newSourceFilesystem(name string, fs afero.Fs) *SourceFilesystem {
   534  	return &SourceFilesystem{
   535  		Name:     name,
   536  		Fs:       fs,
   537  		SourceFs: b.sourceFs,
   538  	}
   539  }
   540  
   541  func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
   542  	if b.theBigFs == nil {
   543  		theBigFs, err := b.createMainOverlayFs(b.p)
   544  		if err != nil {
   545  			return nil, fmt.Errorf("create main fs: %w", err)
   546  		}
   547  
   548  		b.theBigFs = theBigFs
   549  	}
   550  
   551  	createView := func(componentID string, overlayFs *overlayfs.OverlayFs) *SourceFilesystem {
   552  		if b.theBigFs == nil || b.theBigFs.overlayMounts == nil {
   553  			return b.newSourceFilesystem(componentID, hugofs.NoOpFs)
   554  		}
   555  
   556  		fs := hugofs.NewComponentFs(
   557  			hugofs.ComponentFsOptions{
   558  				Fs:                     overlayFs,
   559  				Component:              componentID,
   560  				DefaultContentLanguage: b.p.Cfg.DefaultContentLanguage(),
   561  				PathParser:             b.p.Cfg.PathParser(),
   562  			},
   563  		)
   564  
   565  		return b.newSourceFilesystem(componentID, fs)
   566  	}
   567  
   568  	b.result.Archetypes = createView(files.ComponentFolderArchetypes, b.theBigFs.overlayMounts)
   569  	b.result.Layouts = createView(files.ComponentFolderLayouts, b.theBigFs.overlayMounts)
   570  	b.result.Assets = createView(files.ComponentFolderAssets, b.theBigFs.overlayMounts)
   571  	b.result.ResourcesCache = b.theBigFs.overlayResources
   572  	b.result.RootFss = b.theBigFs.rootFss
   573  
   574  	// data and i18n  needs a different merge strategy.
   575  	overlayMountsPreserveDupes := b.theBigFs.overlayMounts.WithDirsMerger(hugofs.AppendDirsMerger)
   576  	b.result.Data = createView(files.ComponentFolderData, overlayMountsPreserveDupes)
   577  	b.result.I18n = createView(files.ComponentFolderI18n, overlayMountsPreserveDupes)
   578  	b.result.AssetsWithDuplicatesPreserved = createView(files.ComponentFolderAssets, overlayMountsPreserveDupes)
   579  
   580  	contentFs := hugofs.NewComponentFs(
   581  		hugofs.ComponentFsOptions{
   582  			Fs:                     b.theBigFs.overlayMountsContent,
   583  			Component:              files.ComponentFolderContent,
   584  			DefaultContentLanguage: b.p.Cfg.DefaultContentLanguage(),
   585  			PathParser:             b.p.Cfg.PathParser(),
   586  		},
   587  	)
   588  
   589  	b.result.Content = b.newSourceFilesystem(files.ComponentFolderContent, contentFs)
   590  	b.result.Work = hugofs.NewReadOnlyFs(b.theBigFs.overlayFull)
   591  
   592  	// Create static filesystem(s)
   593  	ms := make(map[string]*SourceFilesystem)
   594  	b.result.Static = ms
   595  
   596  	if b.theBigFs.staticPerLanguage != nil {
   597  		// Multihost mode
   598  		for k, v := range b.theBigFs.staticPerLanguage {
   599  			sfs := b.newSourceFilesystem(files.ComponentFolderStatic, v)
   600  			sfs.PublishFolder = k
   601  			ms[k] = sfs
   602  		}
   603  	} else {
   604  		bfs := hugofs.NewBasePathFs(b.theBigFs.overlayMountsStatic, files.ComponentFolderStatic)
   605  		ms[""] = b.newSourceFilesystem(files.ComponentFolderStatic, bfs)
   606  	}
   607  
   608  	return b.result, nil
   609  }
   610  
   611  func (b *sourceFilesystemsBuilder) createMainOverlayFs(p *paths.Paths) (*filesystemsCollector, error) {
   612  	var staticFsMap map[string]*overlayfs.OverlayFs
   613  	if b.p.Cfg.IsMultihost() {
   614  		languages := b.p.Cfg.Languages()
   615  		staticFsMap = make(map[string]*overlayfs.OverlayFs)
   616  		for _, l := range languages {
   617  			staticFsMap[l.Lang] = overlayfs.New(overlayfs.Options{})
   618  		}
   619  	}
   620  
   621  	collector := &filesystemsCollector{
   622  		sourceProject:     b.sourceFs,
   623  		sourceModules:     b.sourceFs,
   624  		staticPerLanguage: staticFsMap,
   625  
   626  		overlayMounts:        overlayfs.New(overlayfs.Options{}),
   627  		overlayMountsContent: overlayfs.New(overlayfs.Options{DirsMerger: hugofs.LanguageDirsMerger}),
   628  		overlayMountsStatic:  overlayfs.New(overlayfs.Options{DirsMerger: hugofs.LanguageDirsMerger}),
   629  		overlayFull:          overlayfs.New(overlayfs.Options{}),
   630  		overlayResources:     overlayfs.New(overlayfs.Options{FirstWritable: true}),
   631  	}
   632  
   633  	mods := p.AllModules()
   634  
   635  	mounts := make([]mountsDescriptor, len(mods))
   636  
   637  	for i := 0; i < len(mods); i++ {
   638  		mod := mods[i]
   639  		dir := mod.Dir()
   640  
   641  		isMainProject := mod.Owner() == nil
   642  		mounts[i] = mountsDescriptor{
   643  			Module:        mod,
   644  			dir:           dir,
   645  			isMainProject: isMainProject,
   646  			ordinal:       i,
   647  		}
   648  
   649  	}
   650  
   651  	err := b.createOverlayFs(collector, mounts)
   652  
   653  	return collector, err
   654  }
   655  
   656  func (b *sourceFilesystemsBuilder) isContentMount(mnt modules.Mount) bool {
   657  	return strings.HasPrefix(mnt.Target, files.ComponentFolderContent)
   658  }
   659  
   660  func (b *sourceFilesystemsBuilder) isStaticMount(mnt modules.Mount) bool {
   661  	return strings.HasPrefix(mnt.Target, files.ComponentFolderStatic)
   662  }
   663  
   664  func (b *sourceFilesystemsBuilder) createOverlayFs(
   665  	collector *filesystemsCollector,
   666  	mounts []mountsDescriptor,
   667  ) error {
   668  	if len(mounts) == 0 {
   669  		appendNopIfEmpty := func(ofs *overlayfs.OverlayFs) *overlayfs.OverlayFs {
   670  			if ofs.NumFilesystems() > 0 {
   671  				return ofs
   672  			}
   673  			return ofs.Append(hugofs.NoOpFs)
   674  		}
   675  		collector.overlayMounts = appendNopIfEmpty(collector.overlayMounts)
   676  		collector.overlayMountsContent = appendNopIfEmpty(collector.overlayMountsContent)
   677  		collector.overlayMountsStatic = appendNopIfEmpty(collector.overlayMountsStatic)
   678  		collector.overlayMountsFull = appendNopIfEmpty(collector.overlayMountsFull)
   679  		collector.overlayFull = appendNopIfEmpty(collector.overlayFull)
   680  		collector.overlayResources = appendNopIfEmpty(collector.overlayResources)
   681  
   682  		return nil
   683  	}
   684  
   685  	for _, md := range mounts {
   686  		var (
   687  			fromTo        []hugofs.RootMapping
   688  			fromToContent []hugofs.RootMapping
   689  			fromToStatic  []hugofs.RootMapping
   690  		)
   691  
   692  		absPathify := func(path string) (string, string) {
   693  			if filepath.IsAbs(path) {
   694  				return "", path
   695  			}
   696  			return md.dir, hpaths.AbsPathify(md.dir, path)
   697  		}
   698  
   699  		for i, mount := range md.Mounts() {
   700  			// Add more weight to early mounts.
   701  			// When two mounts contain the same filename,
   702  			// the first entry wins.
   703  			mountWeight := (10 + md.ordinal) * (len(md.Mounts()) - i)
   704  
   705  			inclusionFilter, err := glob.NewFilenameFilter(
   706  				types.ToStringSlicePreserveString(mount.IncludeFiles),
   707  				types.ToStringSlicePreserveString(mount.ExcludeFiles),
   708  			)
   709  			if err != nil {
   710  				return err
   711  			}
   712  
   713  			base, filename := absPathify(mount.Source)
   714  
   715  			rm := hugofs.RootMapping{
   716  				From:          mount.Target,
   717  				To:            filename,
   718  				ToBase:        base,
   719  				Module:        md.Module.Path(),
   720  				ModuleOrdinal: md.ordinal,
   721  				IsProject:     md.isMainProject,
   722  				Meta: &hugofs.FileMeta{
   723  					Watch:           md.Watch(),
   724  					Weight:          mountWeight,
   725  					InclusionFilter: inclusionFilter,
   726  				},
   727  			}
   728  
   729  			isContentMount := b.isContentMount(mount)
   730  
   731  			lang := mount.Lang
   732  			if lang == "" && isContentMount {
   733  				lang = b.p.Cfg.DefaultContentLanguage()
   734  			}
   735  
   736  			rm.Meta.Lang = lang
   737  
   738  			if isContentMount {
   739  				fromToContent = append(fromToContent, rm)
   740  			} else if b.isStaticMount(mount) {
   741  				fromToStatic = append(fromToStatic, rm)
   742  			} else {
   743  				fromTo = append(fromTo, rm)
   744  			}
   745  		}
   746  
   747  		modBase := collector.sourceProject
   748  		if !md.isMainProject {
   749  			modBase = collector.sourceModules
   750  		}
   751  
   752  		sourceStatic := modBase
   753  
   754  		rmfs, err := hugofs.NewRootMappingFs(modBase, fromTo...)
   755  		if err != nil {
   756  			return err
   757  		}
   758  		rmfsContent, err := hugofs.NewRootMappingFs(modBase, fromToContent...)
   759  		if err != nil {
   760  			return err
   761  		}
   762  		rmfsStatic, err := hugofs.NewRootMappingFs(sourceStatic, fromToStatic...)
   763  		if err != nil {
   764  			return err
   765  		}
   766  
   767  		// We need to keep the list of directories for watching.
   768  		collector.addRootFs(rmfs)
   769  		collector.addRootFs(rmfsContent)
   770  		collector.addRootFs(rmfsStatic)
   771  
   772  		if collector.staticPerLanguage != nil {
   773  			for _, l := range b.p.Cfg.Languages() {
   774  				lang := l.Lang
   775  
   776  				lfs := rmfsStatic.Filter(func(rm hugofs.RootMapping) bool {
   777  					rlang := rm.Meta.Lang
   778  					return rlang == "" || rlang == lang
   779  				})
   780  				bfs := hugofs.NewBasePathFs(lfs, files.ComponentFolderStatic)
   781  				collector.staticPerLanguage[lang] = collector.staticPerLanguage[lang].Append(bfs)
   782  			}
   783  		}
   784  
   785  		getResourcesDir := func() string {
   786  			if md.isMainProject {
   787  				return b.p.AbsResourcesDir
   788  			}
   789  			_, filename := absPathify(files.FolderResources)
   790  			return filename
   791  		}
   792  
   793  		collector.overlayMounts = collector.overlayMounts.Append(rmfs)
   794  		collector.overlayMountsContent = collector.overlayMountsContent.Append(rmfsContent)
   795  		collector.overlayMountsStatic = collector.overlayMountsStatic.Append(rmfsStatic)
   796  		collector.overlayFull = collector.overlayFull.Append(hugofs.NewBasePathFs(modBase, md.dir))
   797  		collector.overlayResources = collector.overlayResources.Append(hugofs.NewBasePathFs(modBase, getResourcesDir()))
   798  
   799  	}
   800  
   801  	return nil
   802  }
   803  
   804  //lint:ignore U1000 useful for debugging
   805  func printFs(fs afero.Fs, path string, w io.Writer) {
   806  	if fs == nil {
   807  		return
   808  	}
   809  	// nolint
   810  	afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error {
   811  		if err != nil {
   812  			return err
   813  		}
   814  		if info.IsDir() {
   815  			return nil
   816  		}
   817  		var filename string
   818  		if fim, ok := info.(hugofs.FileMetaInfo); ok {
   819  			filename = fim.Meta().Filename
   820  		}
   821  		fmt.Fprintf(w, "    %q %q\n", path, filename)
   822  		return nil
   823  	})
   824  }
   825  
   826  type filesystemsCollector struct {
   827  	sourceProject afero.Fs // Source for project folders
   828  	sourceModules afero.Fs // Source for modules/themes
   829  
   830  	overlayMounts        *overlayfs.OverlayFs
   831  	overlayMountsContent *overlayfs.OverlayFs
   832  	overlayMountsStatic  *overlayfs.OverlayFs
   833  	overlayMountsFull    *overlayfs.OverlayFs
   834  	overlayFull          *overlayfs.OverlayFs
   835  	overlayResources     *overlayfs.OverlayFs
   836  
   837  	rootFss []*hugofs.RootMappingFs
   838  
   839  	// Set if in multihost mode
   840  	staticPerLanguage map[string]*overlayfs.OverlayFs
   841  }
   842  
   843  func (c *filesystemsCollector) addRootFs(rfs *hugofs.RootMappingFs) {
   844  	c.rootFss = append(c.rootFss, rfs)
   845  }
   846  
   847  type mountsDescriptor struct {
   848  	modules.Module
   849  	dir           string
   850  	isMainProject bool
   851  	ordinal       int // zero based starting from the project.
   852  }