@@ -25,6 +25,7 @@ import (
2525 "os/exec"
2626 "path/filepath"
2727 "regexp"
28+ "sort"
2829 "strings"
2930 "time"
3031
@@ -63,6 +64,10 @@ const vendord = "_vendor"
6364const (
6465 goModFilename = "go.mod"
6566 goSumFilename = "go.sum"
67+
68+ // Checksum file for direct dependencies only,
69+ // that is, module imports with version set.
70+ hugoDirectSumFilename = "hugo.direct.sum"
6671)
6772
6873// NewClient creates a new Client that can be used to manage the Hugo Components
@@ -208,7 +213,7 @@ func (c *Client) Vendor() error {
208213 continue
209214 }
210215
211- if ! c . shouldVendor (t .Path ( )) {
216+ if c . shouldNotVendor (t .PathVersionQuery ( false )) || c . shouldNotVendor ( t . PathVersionQuery ( true )) {
212217 continue
213218 }
214219
@@ -221,16 +226,16 @@ func (c *Client) Vendor() error {
221226 // See https://github.com/gohugoio/hugo/issues/8239
222227 // This is an error situation. We need something to vendor.
223228 if t .Mounts () == nil {
224- return fmt .Errorf ("cannot vendor module %q, need at least one mount" , t .Path ( ))
229+ return fmt .Errorf ("cannot vendor module %q, need at least one mount" , t .PathVersionQuery ( false ))
225230 }
226231
227- fmt .Fprintln (& modulesContent , "# " + t .Path ( )+ " " + t .Version ())
232+ fmt .Fprintln (& modulesContent , "# " + t .PathVersionQuery ( true )+ " " + t .Version ())
228233
229234 dir := t .Dir ()
230235
231236 for _ , mount := range t .Mounts () {
232237 sourceFilename := filepath .Join (dir , mount .Source )
233- targetFilename := filepath .Join (vendorDir , t .Path ( ), mount .Source )
238+ targetFilename := filepath .Join (vendorDir , t .PathVersionQuery ( true ), mount .Source )
234239 fi , err := c .fs .Stat (sourceFilename )
235240 if err != nil {
236241 return fmt .Errorf ("failed to vendor module: %w" , err )
@@ -257,7 +262,7 @@ func (c *Client) Vendor() error {
257262 resourcesDir := filepath .Join (dir , files .FolderResources )
258263 _ , err := c .fs .Stat (resourcesDir )
259264 if err == nil {
260- if err := hugio .CopyDir (c .fs , resourcesDir , filepath .Join (vendorDir , t .Path ( ), files .FolderResources ), nil ); err != nil {
265+ if err := hugio .CopyDir (c .fs , resourcesDir , filepath .Join (vendorDir , t .PathVersionQuery ( true ), files .FolderResources ), nil ); err != nil {
261266 return fmt .Errorf ("failed to copy resources to vendor dir: %w" , err )
262267 }
263268 }
@@ -266,7 +271,7 @@ func (c *Client) Vendor() error {
266271 configDir := filepath .Join (dir , "config" )
267272 _ , err = c .fs .Stat (configDir )
268273 if err == nil {
269- if err := hugio .CopyDir (c .fs , configDir , filepath .Join (vendorDir , t .Path ( ), "config" ), nil ); err != nil {
274+ if err := hugio .CopyDir (c .fs , configDir , filepath .Join (vendorDir , t .PathVersionQuery ( true ), "config" ), nil ); err != nil {
270275 return fmt .Errorf ("failed to copy config dir to vendor dir: %w" , err )
271276 }
272277 }
@@ -277,7 +282,7 @@ func (c *Client) Vendor() error {
277282 configFiles = append (configFiles , configFiles2 ... )
278283 configFiles = append (configFiles , filepath .Join (dir , "theme.toml" ))
279284 for _ , configFile := range configFiles {
280- if err := hugio .CopyFile (c .fs , configFile , filepath .Join (vendorDir , t .Path ( ), filepath .Base (configFile ))); err != nil {
285+ if err := hugio .CopyFile (c .fs , configFile , filepath .Join (vendorDir , t .PathVersionQuery ( true ), filepath .Base (configFile ))); err != nil {
281286 if ! herrors .IsNotExist (err ) {
282287 return err
283288 }
@@ -434,13 +439,34 @@ func isProbablyModule(path string) bool {
434439 return module .CheckPath (path ) == nil
435440}
436441
442+ func (c * Client ) downloadModuleVersion (path , version string ) (* goModule , error ) {
443+ args := []string {"mod" , "download" , "-json" , fmt .Sprintf ("%s@%s" , path , version )}
444+ b := & bytes.Buffer {}
445+
446+ err := c .runGo (context .Background (), b , args ... )
447+ if err != nil {
448+ return nil , fmt .Errorf ("failed to download module %s@%s: %w" , path , version , err )
449+ }
450+
451+ m := & goModule {}
452+ if err := json .NewDecoder (b ).Decode (m ); err != nil {
453+ return nil , fmt .Errorf ("failed to decode module download result: %w" , err )
454+ }
455+
456+ if m .Error != nil {
457+ return nil , errors .New (m .Error .Err )
458+ }
459+
460+ return m , nil
461+ }
462+
437463func (c * Client ) listGoMods () (goModules , error ) {
438464 if c .GoModulesFilename == "" || ! c .moduleConfig .hasModuleImport () {
439465 return nil , nil
440466 }
441467
442468 downloadModules := func (modules ... string ) error {
443- args := []string {"mod" , "download" , "-modcacherw" }
469+ args := []string {"mod" , "download" }
444470 args = append (args , modules ... )
445471 out := io .Discard
446472 err := c .runGo (context .Background (), out , args ... )
@@ -521,6 +547,86 @@ func (c *Client) listGoMods() (goModules, error) {
521547 return modules , err
522548}
523549
550+ func (c * Client ) writeHugoDirectSum (mods Modules ) error {
551+ if c .GoModulesFilename == "" {
552+ return nil
553+ }
554+ var sums []modSum
555+ for _ , m := range mods {
556+ if m .Owner () == nil {
557+ // This is the project.
558+ continue
559+ }
560+ if m .IsGoMod () && m .VersionQuery () != "" {
561+ sums = append (sums , modSum {pathVersionKey : pathVersionKey {path : m .Path (), version : m .Version ()}, sum : m .Sum ()})
562+ }
563+ }
564+
565+ // Read the existing sums.
566+ existingSums , err := c .readModSumFile (hugoDirectSumFilename )
567+ if err != nil {
568+ return err
569+ }
570+ if len (sums ) == 0 && len (existingSums ) == 0 {
571+ // Nothing to do.
572+ return nil
573+ }
574+
575+ dirty := len (sums ) != len (existingSums )
576+ for _ , s1 := range sums {
577+ if s2 , ok := existingSums [s1 .pathVersionKey ]; ok {
578+ if s1 .sum != s2 {
579+ return fmt .Errorf ("verifying %s@%s: checksum mismatch: %s != %s" , s1 .path , s1 .version , s1 .sum , s2 )
580+ }
581+ } else if ! dirty {
582+ dirty = true
583+ }
584+ }
585+ if ! dirty {
586+ // Nothing changed.
587+ return nil
588+ }
589+
590+ // Write the sums file.
591+ // First sort the sums for reproducible output.
592+ sort .Slice (sums , func (i , j int ) bool {
593+ pvi , pvj := sums [i ].pathVersionKey , sums [j ].pathVersionKey
594+ return pvi .path < pvj .path || (pvi .path == pvj .path && pvi .version < pvj .version )
595+ })
596+
597+ f , err := c .fs .OpenFile (filepath .Join (c .ccfg .WorkingDir , hugoDirectSumFilename ), os .O_CREATE | os .O_TRUNC | os .O_WRONLY , 0o666 )
598+ if err != nil {
599+ return err
600+ }
601+ defer f .Close ()
602+
603+ for _ , s := range sums {
604+ fmt .Fprintf (f , "%s %s %s\n " , s .pathVersionKey .path , s .pathVersionKey .version , s .sum )
605+ }
606+
607+ return nil
608+ }
609+
610+ func (c * Client ) readModSumFile (filename string ) (map [pathVersionKey ]string , error ) {
611+ b , err := afero .ReadFile (c .fs , filepath .Join (c .ccfg .WorkingDir , filename ))
612+ if err != nil {
613+ if herrors .IsNotExist (err ) {
614+ return make (map [pathVersionKey ]string ), nil
615+ }
616+ return nil , err
617+ }
618+ lines := bytes .Split (b , []byte {'\n' })
619+ sums := make (map [pathVersionKey ]string )
620+ for _ , line := range lines {
621+ parts := bytes .Fields (line )
622+ if len (parts ) == 3 {
623+ sums [pathVersionKey {path : string (parts [0 ]), version : string (parts [1 ])}] = string (parts [2 ])
624+ }
625+ }
626+
627+ return sums , nil
628+ }
629+
524630func (c * Client ) rewriteGoMod (name string , isGoMod map [string ]bool ) error {
525631 data , err := c .rewriteGoModRewrite (name , isGoMod )
526632 if err != nil {
@@ -707,8 +813,8 @@ func (c *Client) tidy(mods Modules, goModOnly bool) error {
707813 return nil
708814}
709815
710- func (c * Client ) shouldVendor (path string ) bool {
711- return c .noVendor == nil || ! c .noVendor .Match (path )
816+ func (c * Client ) shouldNotVendor (path string ) bool {
817+ return c .noVendor != nil && c .noVendor .Match (path )
712818}
713819
714820func (c * Client ) createThemeDirname (modulePath string , isProjectMod bool ) (string , error ) {
@@ -773,6 +879,7 @@ func (cfg ClientConfig) toEnv() []string {
773879 keyVals := []string {
774880 "PWD" , cfg .WorkingDir ,
775881 "GO111MODULE" , "on" ,
882+ "GOFLAGS" , "-modcacherw" ,
776883 "GOPATH" , cfg .CacheDir ,
777884 "GOWORK" , mcfg .Workspace , // Requires Go 1.18, see https://tip.golang.org/doc/go1.18
778885 // GOCACHE was introduced in Go 1.15. This matches the location derived from GOPATH above.
@@ -807,6 +914,7 @@ type goModule struct {
807914 Replace * goModule // replaced by this module
808915 Time * time.Time // time version was created
809916 Update * goModule // available update, if any (with -u)
917+ Sum string // checksum
810918 Main bool // is this the main module?
811919 Indirect bool // is this module only an indirect dependency of main module?
812920 Dir string // directory holding files for this module, if any
@@ -820,6 +928,11 @@ type goModuleError struct {
820928
821929type goModules []* goModule
822930
931+ type modSum struct {
932+ pathVersionKey
933+ sum string
934+ }
935+
823936func (modules goModules ) GetByPath (p string ) * goModule {
824937 if modules == nil {
825938 return nil
0 commit comments