github.com/gohugoio/hugo@v0.88.1/hugolib/hugo_sites_build.go (about)

     1  // Copyright 2019 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 hugolib
    15  
    16  import (
    17  	"bytes"
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"os"
    22  	"path/filepath"
    23  	"runtime/trace"
    24  	"strings"
    25  
    26  	"github.com/gohugoio/hugo/publisher"
    27  
    28  	"github.com/gohugoio/hugo/hugofs"
    29  
    30  	"github.com/gohugoio/hugo/common/para"
    31  	"github.com/gohugoio/hugo/config"
    32  	"github.com/gohugoio/hugo/resources/postpub"
    33  
    34  	"github.com/spf13/afero"
    35  
    36  	"github.com/gohugoio/hugo/output"
    37  
    38  	"github.com/pkg/errors"
    39  
    40  	"github.com/fsnotify/fsnotify"
    41  	"github.com/gohugoio/hugo/helpers"
    42  )
    43  
    44  // Build builds all sites. If filesystem events are provided,
    45  // this is considered to be a potential partial rebuild.
    46  func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
    47  	if h.running {
    48  		// Make sure we don't trigger rebuilds in parallel.
    49  		h.runningMu.Lock()
    50  		defer h.runningMu.Unlock()
    51  	} else {
    52  		defer func() {
    53  			h.Close()
    54  		}()
    55  	}
    56  
    57  	ctx, task := trace.NewTask(context.Background(), "Build")
    58  	defer task.End()
    59  
    60  	errCollector := h.StartErrorCollector()
    61  	errs := make(chan error)
    62  
    63  	go func(from, to chan error) {
    64  		var errors []error
    65  		i := 0
    66  		for e := range from {
    67  			i++
    68  			if i > 50 {
    69  				break
    70  			}
    71  			errors = append(errors, e)
    72  		}
    73  		to <- h.pickOneAndLogTheRest(errors)
    74  
    75  		close(to)
    76  	}(errCollector, errs)
    77  
    78  	if h.Metrics != nil {
    79  		h.Metrics.Reset()
    80  	}
    81  
    82  	h.testCounters = config.testCounters
    83  
    84  	// Need a pointer as this may be modified.
    85  	conf := &config
    86  
    87  	if conf.whatChanged == nil {
    88  		// Assume everything has changed
    89  		conf.whatChanged = &whatChanged{source: true}
    90  	}
    91  
    92  	var prepareErr error
    93  
    94  	if !config.PartialReRender {
    95  		prepare := func() error {
    96  			init := func(conf *BuildCfg) error {
    97  				for _, s := range h.Sites {
    98  					s.Deps.BuildStartListeners.Notify()
    99  				}
   100  
   101  				if len(events) > 0 {
   102  					// Rebuild
   103  					if err := h.initRebuild(conf); err != nil {
   104  						return errors.Wrap(err, "initRebuild")
   105  					}
   106  				} else {
   107  					if err := h.initSites(conf); err != nil {
   108  						return errors.Wrap(err, "initSites")
   109  					}
   110  				}
   111  
   112  				return nil
   113  			}
   114  
   115  			var err error
   116  
   117  			f := func() {
   118  				err = h.process(conf, init, events...)
   119  			}
   120  			trace.WithRegion(ctx, "process", f)
   121  			if err != nil {
   122  				return errors.Wrap(err, "process")
   123  			}
   124  
   125  			f = func() {
   126  				err = h.assemble(conf)
   127  			}
   128  			trace.WithRegion(ctx, "assemble", f)
   129  			if err != nil {
   130  				return err
   131  			}
   132  
   133  			return nil
   134  		}
   135  
   136  		f := func() {
   137  			prepareErr = prepare()
   138  		}
   139  		trace.WithRegion(ctx, "prepare", f)
   140  		if prepareErr != nil {
   141  			h.SendError(prepareErr)
   142  		}
   143  
   144  	}
   145  
   146  	if prepareErr == nil {
   147  		var err error
   148  		f := func() {
   149  			err = h.render(conf)
   150  		}
   151  		trace.WithRegion(ctx, "render", f)
   152  		if err != nil {
   153  			h.SendError(err)
   154  		}
   155  
   156  		if err = h.postProcess(); err != nil {
   157  			h.SendError(err)
   158  		}
   159  	}
   160  
   161  	if h.Metrics != nil {
   162  		var b bytes.Buffer
   163  		h.Metrics.WriteMetrics(&b)
   164  
   165  		h.Log.Printf("\nTemplate Metrics:\n\n")
   166  		h.Log.Println(b.String())
   167  	}
   168  
   169  	select {
   170  	// Make sure the channel always gets something.
   171  	case errCollector <- nil:
   172  	default:
   173  	}
   174  	close(errCollector)
   175  
   176  	err := <-errs
   177  	if err != nil {
   178  		return err
   179  	}
   180  
   181  	if err := h.fatalErrorHandler.getErr(); err != nil {
   182  		return err
   183  	}
   184  
   185  	errorCount := h.Log.LogCounters().ErrorCounter.Count()
   186  	if errorCount > 0 {
   187  		return fmt.Errorf("logged %d error(s)", errorCount)
   188  	}
   189  
   190  	return nil
   191  }
   192  
   193  // Build lifecycle methods below.
   194  // The order listed matches the order of execution.
   195  
   196  func (h *HugoSites) initSites(config *BuildCfg) error {
   197  	h.reset(config)
   198  
   199  	if config.NewConfig != nil {
   200  		if err := h.createSitesFromConfig(config.NewConfig); err != nil {
   201  			return err
   202  		}
   203  	}
   204  
   205  	return nil
   206  }
   207  
   208  func (h *HugoSites) initRebuild(config *BuildCfg) error {
   209  	if config.NewConfig != nil {
   210  		return errors.New("rebuild does not support 'NewConfig'")
   211  	}
   212  
   213  	if config.ResetState {
   214  		return errors.New("rebuild does not support 'ResetState'")
   215  	}
   216  
   217  	if !h.running {
   218  		return errors.New("rebuild called when not in watch mode")
   219  	}
   220  
   221  	for _, s := range h.Sites {
   222  		s.resetBuildState(config.whatChanged.source)
   223  	}
   224  
   225  	h.reset(config)
   226  	h.resetLogs()
   227  	helpers.InitLoggers()
   228  
   229  	return nil
   230  }
   231  
   232  func (h *HugoSites) process(config *BuildCfg, init func(config *BuildCfg) error, events ...fsnotify.Event) error {
   233  	// We should probably refactor the Site and pull up most of the logic from there to here,
   234  	// but that seems like a daunting task.
   235  	// So for now, if there are more than one site (language),
   236  	// we pre-process the first one, then configure all the sites based on that.
   237  
   238  	firstSite := h.Sites[0]
   239  
   240  	if len(events) > 0 {
   241  		// This is a rebuild
   242  		return firstSite.processPartial(config, init, events)
   243  	}
   244  
   245  	return firstSite.process(*config)
   246  }
   247  
   248  func (h *HugoSites) assemble(bcfg *BuildCfg) error {
   249  	if len(h.Sites) > 1 {
   250  		// The first is initialized during process; initialize the rest
   251  		for _, site := range h.Sites[1:] {
   252  			if err := site.initializeSiteInfo(); err != nil {
   253  				return err
   254  			}
   255  		}
   256  	}
   257  
   258  	if !bcfg.whatChanged.source {
   259  		return nil
   260  	}
   261  
   262  	if err := h.getContentMaps().AssemblePages(); err != nil {
   263  		return err
   264  	}
   265  
   266  	if err := h.createPageCollections(); err != nil {
   267  		return err
   268  	}
   269  
   270  	return nil
   271  }
   272  
   273  func (h *HugoSites) render(config *BuildCfg) error {
   274  	if _, err := h.init.layouts.Do(); err != nil {
   275  		return err
   276  	}
   277  
   278  	siteRenderContext := &siteRenderContext{cfg: config, multihost: h.multihost}
   279  
   280  	if !config.PartialReRender {
   281  		h.renderFormats = output.Formats{}
   282  		h.withSite(func(s *Site) error {
   283  			s.initRenderFormats()
   284  			return nil
   285  		})
   286  
   287  		for _, s := range h.Sites {
   288  			h.renderFormats = append(h.renderFormats, s.renderFormats...)
   289  		}
   290  	}
   291  
   292  	i := 0
   293  	for _, s := range h.Sites {
   294  		for siteOutIdx, renderFormat := range s.renderFormats {
   295  			siteRenderContext.outIdx = siteOutIdx
   296  			siteRenderContext.sitesOutIdx = i
   297  			i++
   298  
   299  			select {
   300  			case <-h.Done():
   301  				return nil
   302  			default:
   303  				for _, s2 := range h.Sites {
   304  					// We render site by site, but since the content is lazily rendered
   305  					// and a site can "borrow" content from other sites, every site
   306  					// needs this set.
   307  					s2.rc = &siteRenderingContext{Format: renderFormat}
   308  
   309  					if err := s2.preparePagesForRender(s == s2, siteRenderContext.sitesOutIdx); err != nil {
   310  						return err
   311  					}
   312  				}
   313  
   314  				if !config.SkipRender {
   315  					if config.PartialReRender {
   316  						if err := s.renderPages(siteRenderContext); err != nil {
   317  							return err
   318  						}
   319  					} else {
   320  						if err := s.render(siteRenderContext); err != nil {
   321  							return err
   322  						}
   323  					}
   324  				}
   325  			}
   326  
   327  		}
   328  	}
   329  
   330  	if !config.SkipRender {
   331  		if err := h.renderCrossSitesSitemap(); err != nil {
   332  			return err
   333  		}
   334  		if err := h.renderCrossSitesRobotsTXT(); err != nil {
   335  			return err
   336  		}
   337  	}
   338  
   339  	return nil
   340  }
   341  
   342  func (h *HugoSites) postProcess() error {
   343  	// Make sure to write any build stats to disk first so it's available
   344  	// to the post processors.
   345  	if err := h.writeBuildStats(); err != nil {
   346  		return err
   347  	}
   348  
   349  	// This will only be set when js.Build have been triggered with
   350  	// imports that resolves to the project or a module.
   351  	// Write a jsconfig.json file to the project's /asset directory
   352  	// to help JS intellisense in VS Code etc.
   353  	if !h.ResourceSpec.BuildConfig.NoJSConfigInAssets && h.BaseFs.Assets.Dirs != nil {
   354  		fi, err := h.BaseFs.Assets.Fs.Stat("")
   355  		if err != nil {
   356  			h.Log.Warnf("Failed to resolve jsconfig.json dir: %s", err)
   357  		} else {
   358  			m := fi.(hugofs.FileMetaInfo).Meta()
   359  			assetsDir := m.SourceRoot
   360  			if strings.HasPrefix(assetsDir, h.ResourceSpec.WorkingDir) {
   361  				if jsConfig := h.ResourceSpec.JSConfigBuilder.Build(assetsDir); jsConfig != nil {
   362  
   363  					b, err := json.MarshalIndent(jsConfig, "", " ")
   364  					if err != nil {
   365  						h.Log.Warnf("Failed to create jsconfig.json: %s", err)
   366  					} else {
   367  						filename := filepath.Join(assetsDir, "jsconfig.json")
   368  						if h.running {
   369  							h.skipRebuildForFilenamesMu.Lock()
   370  							h.skipRebuildForFilenames[filename] = true
   371  							h.skipRebuildForFilenamesMu.Unlock()
   372  						}
   373  						// Make sure it's  written to the OS fs as this is used by
   374  						// editors.
   375  						if err := afero.WriteFile(hugofs.Os, filename, b, 0666); err != nil {
   376  							h.Log.Warnf("Failed to write jsconfig.json: %s", err)
   377  						}
   378  					}
   379  				}
   380  			}
   381  
   382  		}
   383  	}
   384  
   385  	var toPostProcess []postpub.PostPublishedResource
   386  	for _, r := range h.ResourceSpec.PostProcessResources {
   387  		toPostProcess = append(toPostProcess, r)
   388  	}
   389  
   390  	if len(toPostProcess) == 0 {
   391  		// Nothing more to do.
   392  		return nil
   393  	}
   394  
   395  	workers := para.New(config.GetNumWorkerMultiplier())
   396  	g, _ := workers.Start(context.Background())
   397  
   398  	handleFile := func(filename string) error {
   399  		content, err := afero.ReadFile(h.BaseFs.PublishFs, filename)
   400  		if err != nil {
   401  			return err
   402  		}
   403  
   404  		k := 0
   405  		changed := false
   406  
   407  		for {
   408  			l := bytes.Index(content[k:], []byte(postpub.PostProcessPrefix))
   409  			if l == -1 {
   410  				break
   411  			}
   412  			m := bytes.Index(content[k+l:], []byte(postpub.PostProcessSuffix)) + len(postpub.PostProcessSuffix)
   413  
   414  			low, high := k+l, k+l+m
   415  
   416  			field := content[low:high]
   417  
   418  			forward := l + m
   419  
   420  			for i, r := range toPostProcess {
   421  				if r == nil {
   422  					panic(fmt.Sprintf("resource %d to post process is nil", i+1))
   423  				}
   424  				v, ok := r.GetFieldString(string(field))
   425  				if ok {
   426  					content = append(content[:low], append([]byte(v), content[high:]...)...)
   427  					changed = true
   428  					forward = len(v)
   429  					break
   430  				}
   431  			}
   432  
   433  			k += forward
   434  		}
   435  
   436  		if changed {
   437  			return afero.WriteFile(h.BaseFs.PublishFs, filename, content, 0666)
   438  		}
   439  
   440  		return nil
   441  	}
   442  
   443  	_ = afero.Walk(h.BaseFs.PublishFs, "", func(path string, info os.FileInfo, err error) error {
   444  		if info == nil || info.IsDir() {
   445  			return nil
   446  		}
   447  
   448  		if !strings.HasSuffix(path, "html") {
   449  			return nil
   450  		}
   451  
   452  		g.Run(func() error {
   453  			return handleFile(path)
   454  		})
   455  
   456  		return nil
   457  	})
   458  
   459  	// Prepare for a new build.
   460  	for _, s := range h.Sites {
   461  		s.ResourceSpec.PostProcessResources = make(map[string]postpub.PostPublishedResource)
   462  	}
   463  
   464  	return g.Wait()
   465  }
   466  
   467  type publishStats struct {
   468  	CSSClasses string `json:"cssClasses"`
   469  }
   470  
   471  func (h *HugoSites) writeBuildStats() error {
   472  	if !h.ResourceSpec.BuildConfig.WriteStats {
   473  		return nil
   474  	}
   475  
   476  	htmlElements := &publisher.HTMLElements{}
   477  	for _, s := range h.Sites {
   478  		stats := s.publisher.PublishStats()
   479  		htmlElements.Merge(stats.HTMLElements)
   480  	}
   481  
   482  	htmlElements.Sort()
   483  
   484  	stats := publisher.PublishStats{
   485  		HTMLElements: *htmlElements,
   486  	}
   487  
   488  	js, err := json.MarshalIndent(stats, "", "  ")
   489  	if err != nil {
   490  		return err
   491  	}
   492  
   493  	filename := filepath.Join(h.WorkingDir, "hugo_stats.json")
   494  
   495  	// Make sure it's always written to the OS fs.
   496  	if err := afero.WriteFile(hugofs.Os, filename, js, 0666); err != nil {
   497  		return err
   498  	}
   499  
   500  	// Write to the destination, too, if a mem fs is in play.
   501  	if h.Fs.Source != hugofs.Os {
   502  		if err := afero.WriteFile(h.Fs.Destination, filename, js, 0666); err != nil {
   503  			return err
   504  		}
   505  	}
   506  
   507  	return nil
   508  }