gohugoio / hugo

The world’s fastest framework for building websites.
https://gohugo.io
Apache License 2.0
74.5k stars 7.44k forks source link

Windows: mount path with leading slash is found in the project directory #12470

Open jmooring opened 3 months ago

jmooring commented 3 months ago

Reference: https://discourse.gohugo.io/t/linux-hugo-for-ubuntu-renders-one-page-less-than-windows-version-from-mounted-folder/49664/7?u=jmooring

On Windows, if a mount path has a leading slash, and that path exists relative to the root of the project directory, Hugo finds the path in the project directory and mounts the directory when it shouldn't. Paths with leading slashes are supposed to be relative to the system root.

func TestFoo(t *testing.T) {
    t.Parallel()

    files := `
-- hugo.toml --
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
[[module.mounts]]
source = "content"
target = "content"
[[module.mounts]]
source = "/content-other" # leading slash
target = "content"
-- content/p1.md --
---
title: p1
---
-- content-other/p2.md --
---
title: p2
---
-- layouts/_default/single.html --
{{ .Title }}
`
    b := hugolib.NewIntegrationTestBuilder(
        hugolib.IntegrationTestConfig{
            T:           t,
            TxtarString: files,
            NeedsOsFS:   true,
        }).Build()

    b.AssertFileExists("public/p1/index.html", true)
    b.AssertFileExists("public/p2/index.html", false) // fails on Windows
}
irkode commented 3 months ago

one point regarding handling from (my) users perspective. mounting a non-existing folder should cause at least a "no such file" warning or even an error. not just a normal rendering stuff.

jmooring commented 3 months ago

mounting a non-existing folder...

See https://github.com/gohugoio/hugo/issues/6967

irkode commented 3 months ago

consider the strange windows behavior with having drive letters and cwd for each drive (multiple roots)

No idea about

so your second test case should fail on windows

willing to assist , if there's something (not GO) that would help

and more special stuff

irkode commented 3 months ago

source="c:" or source ="d:" may be problematic cause they refer to the current folder of that drive. So if you are somewhere in c:... and address d: it may point to some unexpected unwanted folder.

btw. docs state it must me forward slashes, but "c:\content" works fine

bep commented 3 months ago

@irkode you have some valid points. My take on this would be that if

os.Stat("/foo/bar.txt")  // Using the correct OS path separator

... throws an error (typically os.ErrNotExist), then that file does not exist.

I'm not sure exactly happens in this particular case, but I'm pretty sure it can be explained re. the above.

irkode commented 3 months ago

I played around a little with Windows style pathes and have a finding

short result

Go's Stat and Abs method seem not to handle Drive Relative Paths like x:some-path-without-starting-slash

I'm new to Go, so there might be methods that handle that

Dunno if this will affect Hugo code

I could not identify issues if absolute paths are used X:\

(p.s. backslash and slash are handled properly)

TL;TR;

wrote a small Go program that

run that for several filenames and compared with the output of Windows Powershell tests

The Good answer for all tested combinations where

have an absolute path with the same drive the results are identical Even for Drive relative paths.

if the drive letter differs

Go's method seem not to handle Drive Relative paths respect the current working of the tested Drive and uses absolute paths

drive C:

C:\
├───bar.md
└───stat
      ├───bar.md
      └───baz.md

failing testcases

this reads as:

PS E:\> C:\mydir\winosstat\mystat.ps1| ?{-Not $_.status }| ft -AutoSize

status cwd driveCwd  path          winStat goStat winAbs         goAbs
------ --- --------  ----          ------- ------ ------         -----
 False E:\ c:\stat   c:bar.md         True   True C:\stat\bar.md C:\bar.md
 False E:\ c:\stat   c:baz.md         True  False C:\stat\baz.md
 False E:\ c:\stat   c:stat/bar.md   False   True                C:\stat\bar.md
 False E:\ c:\stat   c:stat/baz.md   False   True                C:\stat\baz.md
 False E:\ c:\mydir  c:bar.md        False   True                C:\bar.md
 False E:\ c:\mydir  c:stat/bar.md   False   True                C:\stat\bar.md
 False E:\ c:\mydir  c:stat/baz.md   False   True                C:\stat\baz.md
 False E:\ c:\_repos c:bar.md        False   True                C:\bar.md
 False E:\ c:\_repos c:stat/bar.md   False   True                C:\stat\bar.md
 False E:\ c:\_repos c:stat/baz.md   False   True                C:\stat\baz.md

CODE

quite hacky powershell and my first Go program - please be generous.

Go (mystat.go)

package main

import (
   "fmt"
   "os"
   "path/filepath"
)

func main() {

   if len(os.Args) != 2 {
      var arg0 = os.Args[0]
      prg, _ := os.Stat(arg0)
      fmt.Printf("Usage: %s <PATH>", prg.Name())
      os.Exit(1)
   }
   var filename = os.Args[1]
   _, err := os.Stat(filename)
   if err != nil {
      os.Exit(2)
   } else {
      absPath, err := filepath.Abs(filename)
      if err != nil {
         os.Exit(3)
      } else {
         fmt.Print(absPath)
         os.Exit(0)
      }
   }
   os.Exit(1)
}

Powershell

$mystat = "C:\mydir\winosstat\mystat.exe"

# This will be the current workin directories for finding files from
$driveCwds = @(
   "c:\"       # bar.txt
   "c:\stat"    # bar.txt & baz.txt
   "c:\mydir"  # neither
   "c:\_repos"  # neither
)

# Matrix test prefixes + pathes
$prefixes = @(
   "",
   "/",
   "c:",
   "c:/"
)
$pathes = @(
   "bar.md"
   "baz.md"

   "stat/bar.md"
   "stat/baz.md"
)
# Save Current Working directory to be able to go back after changing work director on other drive
$cwd = Get-Location

$driveCwds | % {
   $driveCwd = $_
   # Switch the tested drives working folder and come back
   $newLoc = Set-location $driveCwd -PassThru
   if ($cwd.Drive -ne $newLoc.Drive) {
      Set-Location $cwd
   }
   $prefixes | % {
      $pre = $_; $pathes | % {
         $checkPath = $pre + $_
         $winAbs = $Null
         $goAbs = $Null

         # check Windows style
         $winStat = Test-Path $checkPath -ErrorAction SilentlyContinue
         If ($winStat) {
            $winAbs = (Resolve-Path $checkPath).Path
         }
         # Use Go program
         $goAbs = & $mystat $checkPath
         $goStat = $LastExitCode -eq 0
         [PSCustomObject]@{
            # compare results
            status   = (($winStat -eq $goStat) -and ($winAbs -eq $goAbs))
            cwd      = $cwd
            driveCwd = $driveCwd
            path     = $checkPath
            winStat  = $winStat
            goStat   = $goStat
            winAbs   = $winAbs
            goAbs    = $goAbs
         }
      }
   }
}