diff --git a/hugofs/rootmapping_fs.go b/hugofs/rootmapping_fs.go index a5bf9aadf..336c8b4e7 100644 --- a/hugofs/rootmapping_fs.go +++ b/hugofs/rootmapping_fs.go @@ -21,6 +21,7 @@ import ( "path" "path/filepath" "strings" + "sync/atomic" "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/paths" @@ -43,6 +44,7 @@ var _ ReverseLookupProvder = (*RootMappingFs)(nil) func NewRootMappingFs(fs afero.Fs, rms ...RootMapping) (*RootMappingFs, error) { rootMapToReal := radix.New() realMapToRoot := radix.New() + id := fmt.Sprintf("rfs-%d", rootMappingFsCounter.Add(1)) addMapping := func(key string, rm RootMapping, to *radix.Tree) { var mappings []RootMapping @@ -76,6 +78,16 @@ func NewRootMappingFs(fs afero.Fs, rms ...RootMapping) (*RootMappingFs, error) { rm.Meta = NewFileMeta() } + if rm.FromBase == "" { + panic(" rm.FromBase is empty") + } + + rm.Meta.Component = rm.FromBase + rm.Meta.Module = rm.Module + rm.Meta.ModuleOrdinal = rm.ModuleOrdinal + rm.Meta.IsProject = rm.IsProject + rm.Meta.BaseDir = rm.ToBase + if !fi.IsDir() { // We do allow single file mounts. // However, the file system logic will be much simpler with just directories. @@ -122,19 +134,9 @@ func NewRootMappingFs(fs afero.Fs, rms ...RootMapping) (*RootMappingFs, error) { } } - if rm.FromBase == "" { - panic(" rm.FromBase is empty") - } - // Extract "blog" from "content/blog" rm.path = strings.TrimPrefix(strings.TrimPrefix(rm.From, rm.FromBase), filepathSeparator) - rm.Meta.SourceRoot = fi.(MetaProvider).Meta().Filename - rm.Meta.BaseDir = rm.ToBase - rm.Meta.Module = rm.Module - rm.Meta.ModuleOrdinal = rm.ModuleOrdinal - rm.Meta.Component = rm.FromBase - rm.Meta.IsProject = rm.IsProject meta := rm.Meta.Copy() @@ -156,6 +158,7 @@ func NewRootMappingFs(fs afero.Fs, rms ...RootMapping) (*RootMappingFs, error) { } rfs := &RootMappingFs{ + id: id, Fs: fs, rootMapToReal: rootMapToReal, realMapToRoot: realMapToRoot, @@ -227,11 +230,14 @@ var _ FilesystemUnwrapper = (*RootMappingFs)(nil) // is directories only, and they will be returned in Readdir and Readdirnames // in the order given. type RootMappingFs struct { + id string afero.Fs rootMapToReal *radix.Tree realMapToRoot *radix.Tree } +var rootMappingFsCounter atomic.Int32 + func (fs *RootMappingFs) Mounts(base string) ([]FileMetaInfo, error) { base = filepathSeparator + fs.cleanName(base) roots := fs.getRootsWithPrefix(base) @@ -263,6 +269,10 @@ func (fs *RootMappingFs) Mounts(base string) ([]FileMetaInfo, error) { return fss, nil } +func (fs *RootMappingFs) Key() string { + return fs.id +} + func (fs *RootMappingFs) UnwrapFilesystem() afero.Fs { return fs.Fs } @@ -320,16 +330,16 @@ func (c ComponentPath) ComponentPathJoined() string { } type ReverseLookupProvder interface { - ReverseLookup(filename string, checkExists bool) ([]ComponentPath, error) - ReverseLookupComponent(component, filename string, checkExists bool) ([]ComponentPath, error) + ReverseLookup(filename string) ([]ComponentPath, error) + ReverseLookupComponent(component, filename string) ([]ComponentPath, error) } // func (fs *RootMappingFs) ReverseStat(filename string) ([]FileMetaInfo, error) -func (fs *RootMappingFs) ReverseLookup(filename string, checkExists bool) ([]ComponentPath, error) { - return fs.ReverseLookupComponent("", filename, checkExists) +func (fs *RootMappingFs) ReverseLookup(filename string) ([]ComponentPath, error) { + return fs.ReverseLookupComponent("", filename) } -func (fs *RootMappingFs) ReverseLookupComponent(component, filename string, checkExists bool) ([]ComponentPath, error) { +func (fs *RootMappingFs) ReverseLookupComponent(component, filename string) ([]ComponentPath, error) { filename = fs.cleanName(filename) key := filepathSeparator + filename @@ -360,14 +370,6 @@ func (fs *RootMappingFs) ReverseLookupComponent(component, filename string, chec } else { // Now we know that this file _could_ be in this fs. filename = filepathSeparator + filepath.Join(first.path, dir, name) - - if checkExists { - // Confirm that it exists. - _, err := fs.Stat(first.FromBase + filename) - if err != nil { - continue - } - } } cps = append(cps, ComponentPath{ @@ -667,6 +669,7 @@ func (fs *RootMappingFs) doStat(name string) ([]FileMetaInfo, error) { var fis []FileMetaInfo for _, rm := range roots { + var fi FileMetaInfo fi, err = fs.statRoot(rm, name) if err == nil { diff --git a/hugofs/rootmapping_fs_test.go b/hugofs/rootmapping_fs_test.go index b1ef102d3..83a95d648 100644 --- a/hugofs/rootmapping_fs_test.go +++ b/hugofs/rootmapping_fs_test.go @@ -276,20 +276,20 @@ func TestRootMappingFsMount(t *testing.T) { // Test ReverseLookup. // Single file mounts. - cps, err := rfs.ReverseLookup(filepath.FromSlash("singlefiles/no.txt"), true) + cps, err := rfs.ReverseLookup(filepath.FromSlash("singlefiles/no.txt")) c.Assert(err, qt.IsNil) c.Assert(cps, qt.DeepEquals, []ComponentPath{ {Component: "content", Path: "singles/p1.md", Lang: "no"}, }) - cps, err = rfs.ReverseLookup(filepath.FromSlash("singlefiles/sv.txt"), true) + cps, err = rfs.ReverseLookup(filepath.FromSlash("singlefiles/sv.txt")) c.Assert(err, qt.IsNil) c.Assert(cps, qt.DeepEquals, []ComponentPath{ {Component: "content", Path: "singles/p1.md", Lang: "sv"}, }) // File inside directory mount. - cps, err = rfs.ReverseLookup(filepath.FromSlash("mynoblogcontent/test.txt"), true) + cps, err = rfs.ReverseLookup(filepath.FromSlash("mynoblogcontent/test.txt")) c.Assert(err, qt.IsNil) c.Assert(cps, qt.DeepEquals, []ComponentPath{ {Component: "content", Path: "blog/test.txt", Lang: "no"}, diff --git a/hugolib/filesystems/basefs.go b/hugolib/filesystems/basefs.go index aa466e0eb..b3e3284d5 100644 --- a/hugolib/filesystems/basefs.go +++ b/hugolib/filesystems/basefs.go @@ -265,6 +265,9 @@ type SourceFilesystem struct { // This is a virtual composite filesystem. It expects path relative to a context. Fs afero.Fs + // The source filesystem (usually the OS filesystem). + SourceFs afero.Fs + // When syncing a source folder to the target (e.g. /public), this may // be set to publish into a subfolder. This is used for static syncing // in multihost mode. @@ -320,10 +323,10 @@ func (s SourceFilesystems) IsContent(filename string) bool { } // ResolvePaths resolves the given filename to a list of paths in the filesystems. -func (s *SourceFilesystems) ResolvePaths(filename string, checkExists bool) []hugofs.ComponentPath { +func (s *SourceFilesystems) ResolvePaths(filename string) []hugofs.ComponentPath { var cpss []hugofs.ComponentPath for _, rfs := range s.RootFss { - cps, err := rfs.ReverseLookup(filename, checkExists) + cps, err := rfs.ReverseLookup(filename) if err != nil { panic(err) } @@ -362,7 +365,17 @@ func (d *SourceFilesystem) ReverseLookup(filename string, checkExists bool) ([]h var cps []hugofs.ComponentPath hugofs.WalkFilesystems(d.Fs, func(fs afero.Fs) bool { if rfs, ok := fs.(hugofs.ReverseLookupProvder); ok { - if c, err := rfs.ReverseLookupComponent(d.Name, filename, checkExists); err == nil { + if c, err := rfs.ReverseLookupComponent(d.Name, filename); err == nil { + if checkExists { + n := 0 + for _, cp := range c { + if _, err := d.Fs.Stat(filepath.FromSlash(cp.Path)); err == nil { + c[n] = cp + n++ + } + } + c = c[:n] + } cps = append(cps, c...) } } @@ -379,11 +392,12 @@ func (d *SourceFilesystem) mounts() []hugofs.FileMetaInfo { if err == nil { m = append(m, mounts...) } - } return false }) + // Filter out any mounts not belonging to this filesystem. + // TODO(bep) I think this is superflous. n := 0 for _, mm := range m { if mm.Meta().Component == d.Name { @@ -392,6 +406,7 @@ func (d *SourceFilesystem) mounts() []hugofs.FileMetaInfo { } } m = m[:n] + return m } @@ -428,10 +443,8 @@ func (d *SourceFilesystem) RealDirs(from string) []string { if !m.IsDir() { continue } - meta := m.Meta() - _, err := d.Fs.Stat(from) - if err == nil { - dirname := filepath.Join(meta.Filename, from) + dirname := filepath.Join(m.Meta().Filename, from) + if _, err := d.SourceFs.Stat(dirname); err == nil { dirnames = append(dirnames, dirname) } } @@ -519,8 +532,9 @@ func newSourceFilesystemsBuilder(p *paths.Paths, logger loggers.Logger, b *BaseF func (b *sourceFilesystemsBuilder) newSourceFilesystem(name string, fs afero.Fs) *SourceFilesystem { return &SourceFilesystem{ - Name: name, - Fs: fs, + Name: name, + Fs: fs, + SourceFs: b.sourceFs, } } diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index 93acdbf6b..382b1eed0 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -656,7 +656,7 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf isChangedDir := statErr == nil && fi.IsDir() - cpss := h.BaseFs.ResolvePaths(ev.Name, !removed) + cpss := h.BaseFs.ResolvePaths(ev.Name) pss := make([]*paths.Path, len(cpss)) for i, cps := range cpss { p := cps.Path diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go index 642bac7ab..be11c18d6 100644 --- a/hugolib/integrationtest_builder.go +++ b/hugolib/integrationtest_builder.go @@ -183,6 +183,13 @@ type lockingBuffer struct { bytes.Buffer } +func (b *lockingBuffer) ReadFrom(r io.Reader) (n int64, err error) { + b.Lock() + n, err = b.Buffer.ReadFrom(r) + b.Unlock() + return +} + func (b *lockingBuffer) Write(p []byte) (n int, err error) { b.Lock() n, err = b.Buffer.Write(p) diff --git a/resources/resource_transformers/tocss/dartsass/dartsass_integration_test.go b/resources/resource_transformers/tocss/dartsass/dartsass_integration_test.go index dd4c1e5ca..4d48b3b6a 100644 --- a/resources/resource_transformers/tocss/dartsass/dartsass_integration_test.go +++ b/resources/resource_transformers/tocss/dartsass/dartsass_integration_test.go @@ -19,6 +19,7 @@ import ( "github.com/bep/logg" qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/htesting" "github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass" ) @@ -525,3 +526,39 @@ T1: {{ $r.Content }} b.AssertLogMatches(`Dart Sass: .*assets.*main.scss:13:0: number`) b.AssertLogMatches(`Dart Sass: .*assets.*main.scss:14:0: number`) } + +// Note: This test is more or less duplicated in both of the SCSS packages (libsass and dartsass). +func TestBootstrap(t *testing.T) { + t.Parallel() + if !dartsass.Supports() { + t.Skip() + } + if !htesting.IsCI() { + t.Skip("skip (slow) test in non-CI environment") + } + + files := ` +-- hugo.toml -- +disableKinds = ["term", "taxonomy", "section", "page"] +[module] +[[module.imports]] +path="github.com/gohugoio/hugo-mod-bootstrap-scss/v5" +-- go.mod -- +module github.com/gohugoio/tests/testHugoModules +-- assets/scss/main.scss -- +@import "bootstrap/bootstrap"; +-- layouts/index.html -- +{{ $cssOpts := (dict "transpiler" "dartsass" ) }} +{{ $r := resources.Get "scss/main.scss" | toCSS $cssOpts }} +Styles: {{ $r.RelPermalink }} + ` + + b := hugolib.NewIntegrationTestBuilder( + hugolib.IntegrationTestConfig{ + T: t, + TxtarString: files, + NeedsOsFS: true, + }).Build() + + b.AssertFileContent("public/index.html", "Styles: /scss/main.css") +} diff --git a/resources/resource_transformers/tocss/scss/scss_integration_test.go b/resources/resource_transformers/tocss/scss/scss_integration_test.go index 4d7d9d710..c193ca8af 100644 --- a/resources/resource_transformers/tocss/scss/scss_integration_test.go +++ b/resources/resource_transformers/tocss/scss/scss_integration_test.go @@ -20,6 +20,7 @@ import ( qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/htesting" "github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss" ) @@ -290,3 +291,39 @@ T1: {{ $r.Content }} b.AssertFileContent("public/index.html", `T1: body body{background:url(images/hero.jpg) no-repeat center/cover;font-family:Hugo's New Roman}p{color:blue;font-size:var 24px}b{color:green}`) } + +// Note: This test is more or less duplicated in both of the SCSS packages (libsass and dartsass). +func TestBootstrap(t *testing.T) { + t.Parallel() + if !scss.Supports() { + t.Skip() + } + if !htesting.IsCI() { + t.Skip("skip (slow) test in non-CI environment") + } + + files := ` +-- hugo.toml -- +disableKinds = ["term", "taxonomy", "section", "page"] +[module] +[[module.imports]] +path="github.com/gohugoio/hugo-mod-bootstrap-scss/v5" +-- go.mod -- +module github.com/gohugoio/tests/testHugoModules +-- assets/scss/main.scss -- +@import "bootstrap/bootstrap"; +-- layouts/index.html -- +{{ $cssOpts := (dict "transpiler" "libsass" ) }} +{{ $r := resources.Get "scss/main.scss" | toCSS $cssOpts }} +Styles: {{ $r.RelPermalink }} + ` + + b := hugolib.NewIntegrationTestBuilder( + hugolib.IntegrationTestConfig{ + T: t, + TxtarString: files, + NeedsOsFS: true, + }).Build() + + b.AssertFileContent("public/index.html", "Styles: /scss/main.css") +}