github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/modules/collect.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 modules
    15  
    16  import (
    17  	"bufio"
    18  	"fmt"
    19  	"os"
    20  	"path/filepath"
    21  	"regexp"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/bep/debounce"
    26  	"github.com/gohugoio/hugo/common/herrors"
    27  	"github.com/gohugoio/hugo/common/loggers"
    28  
    29  	"github.com/spf13/cast"
    30  
    31  	"github.com/gohugoio/hugo/common/maps"
    32  
    33  	"github.com/gohugoio/hugo/common/hugo"
    34  	"github.com/gohugoio/hugo/parser/metadecoders"
    35  
    36  	"github.com/gohugoio/hugo/hugofs/files"
    37  
    38  	"github.com/rogpeppe/go-internal/module"
    39  
    40  	"errors"
    41  
    42  	"github.com/gohugoio/hugo/config"
    43  	"github.com/spf13/afero"
    44  )
    45  
    46  var ErrNotExist = errors.New("module does not exist")
    47  
    48  const vendorModulesFilename = "modules.txt"
    49  
    50  // IsNotExist returns whether an error means that a module could not be found.
    51  func IsNotExist(err error) bool {
    52  	return errors.Is(err, os.ErrNotExist)
    53  }
    54  
    55  // CreateProjectModule creates modules from the given config.
    56  // This is used in tests only.
    57  func CreateProjectModule(cfg config.Provider) (Module, error) {
    58  	workingDir := cfg.GetString("workingDir")
    59  	var modConfig Config
    60  
    61  	mod := createProjectModule(nil, workingDir, modConfig)
    62  	if err := ApplyProjectConfigDefaults(cfg, mod); err != nil {
    63  		return nil, err
    64  	}
    65  
    66  	return mod, nil
    67  }
    68  
    69  func (h *Client) Collect() (ModulesConfig, error) {
    70  	mc, coll := h.collect(true)
    71  	if coll.err != nil {
    72  		return mc, coll.err
    73  	}
    74  
    75  	if err := (&mc).setActiveMods(h.logger); err != nil {
    76  		return mc, err
    77  	}
    78  
    79  	if h.ccfg.HookBeforeFinalize != nil {
    80  		if err := h.ccfg.HookBeforeFinalize(&mc); err != nil {
    81  			return mc, err
    82  		}
    83  	}
    84  
    85  	if err := (&mc).finalize(h.logger); err != nil {
    86  		return mc, err
    87  	}
    88  
    89  	return mc, nil
    90  }
    91  
    92  func (h *Client) collect(tidy bool) (ModulesConfig, *collector) {
    93  	c := &collector{
    94  		Client: h,
    95  	}
    96  
    97  	c.collect()
    98  	if c.err != nil {
    99  		return ModulesConfig{}, c
   100  	}
   101  
   102  	// https://github.com/gohugoio/hugo/issues/6115
   103  	/*if !c.skipTidy && tidy {
   104  		if err := h.tidy(c.modules, true); err != nil {
   105  			c.err = err
   106  			return ModulesConfig{}, c
   107  		}
   108  	}*/
   109  
   110  	var workspaceFilename string
   111  	if h.ccfg.ModuleConfig.Workspace != WorkspaceDisabled {
   112  		workspaceFilename = h.ccfg.ModuleConfig.Workspace
   113  	}
   114  
   115  	return ModulesConfig{
   116  		AllModules:          c.modules,
   117  		GoModulesFilename:   c.GoModulesFilename,
   118  		GoWorkspaceFilename: workspaceFilename,
   119  	}, c
   120  }
   121  
   122  type ModulesConfig struct {
   123  	// All modules, including any disabled.
   124  	AllModules Modules
   125  
   126  	// All active modules.
   127  	ActiveModules Modules
   128  
   129  	// Set if this is a Go modules enabled project.
   130  	GoModulesFilename string
   131  
   132  	// Set if a Go workspace file is configured.
   133  	GoWorkspaceFilename string
   134  }
   135  
   136  func (m *ModulesConfig) setActiveMods(logger loggers.Logger) error {
   137  	var activeMods Modules
   138  	for _, mod := range m.AllModules {
   139  		if !mod.Config().HugoVersion.IsValid() {
   140  			logger.Warnf(`Module %q is not compatible with this Hugo version; run "hugo mod graph" for more information.`, mod.Path())
   141  		}
   142  		if !mod.Disabled() {
   143  			activeMods = append(activeMods, mod)
   144  		}
   145  	}
   146  
   147  	m.ActiveModules = activeMods
   148  
   149  	return nil
   150  }
   151  
   152  func (m *ModulesConfig) finalize(logger loggers.Logger) error {
   153  	for _, mod := range m.AllModules {
   154  		m := mod.(*moduleAdapter)
   155  		m.mounts = filterUnwantedMounts(m.mounts)
   156  	}
   157  	return nil
   158  }
   159  
   160  func filterUnwantedMounts(mounts []Mount) []Mount {
   161  	// Remove duplicates
   162  	seen := make(map[string]bool)
   163  	tmp := mounts[:0]
   164  	for _, m := range mounts {
   165  		if !seen[m.key()] {
   166  			tmp = append(tmp, m)
   167  		}
   168  		seen[m.key()] = true
   169  	}
   170  	return tmp
   171  }
   172  
   173  type collected struct {
   174  	// Pick the first and prevent circular loops.
   175  	seen map[string]bool
   176  
   177  	// Maps module path to a _vendor dir. These values are fetched from
   178  	// _vendor/modules.txt, and the first (top-most) will win.
   179  	vendored map[string]vendoredModule
   180  
   181  	// Set if a Go modules enabled project.
   182  	gomods goModules
   183  
   184  	// Ordered list of collected modules, including Go Modules and theme
   185  	// components stored below /themes.
   186  	modules Modules
   187  }
   188  
   189  // Collects and creates a module tree.
   190  type collector struct {
   191  	*Client
   192  
   193  	// Store away any non-fatal error and return at the end.
   194  	err error
   195  
   196  	// Set to disable any Tidy operation in the end.
   197  	skipTidy bool
   198  
   199  	*collected
   200  }
   201  
   202  func (c *collector) initModules() error {
   203  	c.collected = &collected{
   204  		seen:     make(map[string]bool),
   205  		vendored: make(map[string]vendoredModule),
   206  		gomods:   goModules{},
   207  	}
   208  
   209  	// If both these are true, we don't even need Go installed to build.
   210  	if c.ccfg.IgnoreVendor == nil && c.isVendored(c.ccfg.WorkingDir) {
   211  		return nil
   212  	}
   213  
   214  	// We may fail later if we don't find the mods.
   215  	return c.loadModules()
   216  }
   217  
   218  func (c *collector) isSeen(path string) bool {
   219  	key := pathKey(path)
   220  	if c.seen[key] {
   221  		return true
   222  	}
   223  	c.seen[key] = true
   224  	return false
   225  }
   226  
   227  func (c *collector) getVendoredDir(path string) (vendoredModule, bool) {
   228  	v, found := c.vendored[path]
   229  	return v, found
   230  }
   231  
   232  func (c *collector) add(owner *moduleAdapter, moduleImport Import, disabled bool) (*moduleAdapter, error) {
   233  	var (
   234  		mod       *goModule
   235  		moduleDir string
   236  		version   string
   237  		vendored  bool
   238  	)
   239  
   240  	modulePath := moduleImport.Path
   241  	var realOwner Module = owner
   242  
   243  	if !c.ccfg.shouldIgnoreVendor(modulePath) {
   244  		if err := c.collectModulesTXT(owner); err != nil {
   245  			return nil, err
   246  		}
   247  
   248  		// Try _vendor first.
   249  		var vm vendoredModule
   250  		vm, vendored = c.getVendoredDir(modulePath)
   251  		if vendored {
   252  			moduleDir = vm.Dir
   253  			realOwner = vm.Owner
   254  			version = vm.Version
   255  
   256  			if owner.projectMod {
   257  				// We want to keep the go.mod intact with the versions and all.
   258  				c.skipTidy = true
   259  			}
   260  
   261  		}
   262  	}
   263  
   264  	if moduleDir == "" {
   265  		var versionQuery string
   266  		mod = c.gomods.GetByPath(modulePath)
   267  		if mod != nil {
   268  			moduleDir = mod.Dir
   269  			versionQuery = mod.Version
   270  		}
   271  
   272  		if moduleDir == "" {
   273  			if c.GoModulesFilename != "" && isProbablyModule(modulePath) {
   274  				// Try to "go get" it and reload the module configuration.
   275  				if versionQuery == "" {
   276  					// See https://golang.org/ref/mod#version-queries
   277  					// This will select the latest release-version (not beta etc.).
   278  					versionQuery = "upgrade"
   279  				}
   280  				if err := c.Get(fmt.Sprintf("%s@%s", modulePath, versionQuery)); err != nil {
   281  					return nil, err
   282  				}
   283  				if err := c.loadModules(); err != nil {
   284  					return nil, err
   285  				}
   286  
   287  				mod = c.gomods.GetByPath(modulePath)
   288  				if mod != nil {
   289  					moduleDir = mod.Dir
   290  				}
   291  			}
   292  
   293  			// Fall back to project/themes/<mymodule>
   294  			if moduleDir == "" {
   295  				var err error
   296  				moduleDir, err = c.createThemeDirname(modulePath, owner.projectMod || moduleImport.pathProjectReplaced)
   297  				if err != nil {
   298  					c.err = err
   299  					return nil, nil
   300  				}
   301  				if found, _ := afero.Exists(c.fs, moduleDir); !found {
   302  					c.err = c.wrapModuleNotFound(fmt.Errorf(`module %q not found; either add it as a Hugo Module or store it in %q.`, modulePath, c.ccfg.ThemesDir))
   303  					return nil, nil
   304  				}
   305  			}
   306  		}
   307  	}
   308  
   309  	if found, _ := afero.Exists(c.fs, moduleDir); !found {
   310  		c.err = c.wrapModuleNotFound(fmt.Errorf("%q not found", moduleDir))
   311  		return nil, nil
   312  	}
   313  
   314  	if !strings.HasSuffix(moduleDir, fileSeparator) {
   315  		moduleDir += fileSeparator
   316  	}
   317  
   318  	ma := &moduleAdapter{
   319  		dir:      moduleDir,
   320  		vendor:   vendored,
   321  		disabled: disabled,
   322  		gomod:    mod,
   323  		version:  version,
   324  		// This may be the owner of the _vendor dir
   325  		owner: realOwner,
   326  	}
   327  
   328  	if mod == nil {
   329  		ma.path = modulePath
   330  	}
   331  
   332  	if !moduleImport.IgnoreConfig {
   333  		if err := c.applyThemeConfig(ma); err != nil {
   334  			return nil, err
   335  		}
   336  	}
   337  
   338  	if err := c.applyMounts(moduleImport, ma); err != nil {
   339  		return nil, err
   340  	}
   341  
   342  	c.modules = append(c.modules, ma)
   343  	return ma, nil
   344  }
   345  
   346  func (c *collector) addAndRecurse(owner *moduleAdapter, disabled bool) error {
   347  	moduleConfig := owner.Config()
   348  	if owner.projectMod {
   349  		if err := c.applyMounts(Import{}, owner); err != nil {
   350  			return err
   351  		}
   352  	}
   353  
   354  	for _, moduleImport := range moduleConfig.Imports {
   355  		disabled := disabled || moduleImport.Disable
   356  
   357  		if !c.isSeen(moduleImport.Path) {
   358  			tc, err := c.add(owner, moduleImport, disabled)
   359  			if err != nil {
   360  				return err
   361  			}
   362  			if tc == nil || moduleImport.IgnoreImports {
   363  				continue
   364  			}
   365  			if err := c.addAndRecurse(tc, disabled); err != nil {
   366  				return err
   367  			}
   368  		}
   369  	}
   370  	return nil
   371  }
   372  
   373  func (c *collector) applyMounts(moduleImport Import, mod *moduleAdapter) error {
   374  	if moduleImport.NoMounts {
   375  		mod.mounts = nil
   376  		return nil
   377  	}
   378  
   379  	mounts := moduleImport.Mounts
   380  
   381  	modConfig := mod.Config()
   382  
   383  	if len(mounts) == 0 {
   384  		// Mounts not defined by the import.
   385  		mounts = modConfig.Mounts
   386  	}
   387  
   388  	if !mod.projectMod && len(mounts) == 0 {
   389  		// Create default mount points for every component folder that
   390  		// exists in the module.
   391  		for _, componentFolder := range files.ComponentFolders {
   392  			sourceDir := filepath.Join(mod.Dir(), componentFolder)
   393  			_, err := c.fs.Stat(sourceDir)
   394  			if err == nil {
   395  				mounts = append(mounts, Mount{
   396  					Source: componentFolder,
   397  					Target: componentFolder,
   398  				})
   399  			}
   400  		}
   401  	}
   402  
   403  	var err error
   404  	mounts, err = c.normalizeMounts(mod, mounts)
   405  	if err != nil {
   406  		return err
   407  	}
   408  
   409  	mounts, err = c.mountCommonJSConfig(mod, mounts)
   410  	if err != nil {
   411  		return err
   412  	}
   413  
   414  	mod.mounts = mounts
   415  	return nil
   416  }
   417  
   418  func (c *collector) applyThemeConfig(tc *moduleAdapter) error {
   419  	var (
   420  		configFilename string
   421  		themeCfg       map[string]any
   422  		hasConfigFile  bool
   423  		err            error
   424  	)
   425  
   426  LOOP:
   427  	for _, configBaseName := range config.DefaultConfigNames {
   428  		for _, configFormats := range config.ValidConfigFileExtensions {
   429  			configFilename = filepath.Join(tc.Dir(), configBaseName+"."+configFormats)
   430  			hasConfigFile, _ = afero.Exists(c.fs, configFilename)
   431  			if hasConfigFile {
   432  				break LOOP
   433  			}
   434  		}
   435  	}
   436  
   437  	// The old theme information file.
   438  	themeTOML := filepath.Join(tc.Dir(), "theme.toml")
   439  
   440  	hasThemeTOML, _ := afero.Exists(c.fs, themeTOML)
   441  	if hasThemeTOML {
   442  		data, err := afero.ReadFile(c.fs, themeTOML)
   443  		if err != nil {
   444  			return err
   445  		}
   446  		themeCfg, err = metadecoders.Default.UnmarshalToMap(data, metadecoders.TOML)
   447  		if err != nil {
   448  			c.logger.Warnf("Failed to read module config for %q in %q: %s", tc.Path(), themeTOML, err)
   449  		} else {
   450  			maps.PrepareParams(themeCfg)
   451  		}
   452  	}
   453  
   454  	if hasConfigFile {
   455  		if configFilename != "" {
   456  			var err error
   457  			tc.cfg, err = config.FromFile(c.fs, configFilename)
   458  			if err != nil {
   459  				return err
   460  			}
   461  		}
   462  
   463  		tc.configFilenames = append(tc.configFilenames, configFilename)
   464  
   465  	}
   466  
   467  	// Also check for a config dir, which we overlay on top of the file configuration.
   468  	configDir := filepath.Join(tc.Dir(), "config")
   469  	dcfg, dirnames, err := config.LoadConfigFromDir(c.fs, configDir, c.ccfg.Environment)
   470  	if err != nil {
   471  		return err
   472  	}
   473  
   474  	if len(dirnames) > 0 {
   475  		tc.configFilenames = append(tc.configFilenames, dirnames...)
   476  
   477  		if hasConfigFile {
   478  			// Set will overwrite existing keys.
   479  			tc.cfg.Set("", dcfg.Get(""))
   480  		} else {
   481  			tc.cfg = dcfg
   482  		}
   483  	}
   484  
   485  	config, err := decodeConfig(tc.cfg, c.moduleConfig.replacementsMap)
   486  	if err != nil {
   487  		return err
   488  	}
   489  
   490  	const oldVersionKey = "min_version"
   491  
   492  	if hasThemeTOML {
   493  
   494  		// Merge old with new
   495  		if minVersion, found := themeCfg[oldVersionKey]; found {
   496  			if config.HugoVersion.Min == "" {
   497  				config.HugoVersion.Min = hugo.VersionString(cast.ToString(minVersion))
   498  			}
   499  		}
   500  
   501  		if config.Params == nil {
   502  			config.Params = make(map[string]any)
   503  		}
   504  
   505  		for k, v := range themeCfg {
   506  			if k == oldVersionKey {
   507  				continue
   508  			}
   509  			config.Params[k] = v
   510  		}
   511  
   512  	}
   513  
   514  	tc.config = config
   515  
   516  	return nil
   517  }
   518  
   519  func (c *collector) collect() {
   520  	defer c.logger.PrintTimerIfDelayed(time.Now(), "hugo: collected modules")
   521  	d := debounce.New(2 * time.Second)
   522  	d(func() {
   523  		c.logger.Println("hugo: downloading modules …")
   524  	})
   525  	defer d(func() {})
   526  
   527  	if err := c.initModules(); err != nil {
   528  		c.err = err
   529  		return
   530  	}
   531  
   532  	projectMod := createProjectModule(c.gomods.GetMain(), c.ccfg.WorkingDir, c.moduleConfig)
   533  
   534  	if err := c.addAndRecurse(projectMod, false); err != nil {
   535  		c.err = err
   536  		return
   537  	}
   538  
   539  	// Add the project mod on top.
   540  	c.modules = append(Modules{projectMod}, c.modules...)
   541  }
   542  
   543  func (c *collector) isVendored(dir string) bool {
   544  	_, err := c.fs.Stat(filepath.Join(dir, vendord, vendorModulesFilename))
   545  	return err == nil
   546  }
   547  
   548  func (c *collector) collectModulesTXT(owner Module) error {
   549  	vendorDir := filepath.Join(owner.Dir(), vendord)
   550  	filename := filepath.Join(vendorDir, vendorModulesFilename)
   551  
   552  	f, err := c.fs.Open(filename)
   553  	if err != nil {
   554  		if herrors.IsNotExist(err) {
   555  			return nil
   556  		}
   557  
   558  		return err
   559  	}
   560  
   561  	defer f.Close()
   562  
   563  	scanner := bufio.NewScanner(f)
   564  
   565  	for scanner.Scan() {
   566  		// # github.com/alecthomas/chroma v0.6.3
   567  		line := scanner.Text()
   568  		line = strings.Trim(line, "# ")
   569  		line = strings.TrimSpace(line)
   570  		parts := strings.Fields(line)
   571  		if len(parts) != 2 {
   572  			return fmt.Errorf("invalid modules list: %q", filename)
   573  		}
   574  		path := parts[0]
   575  
   576  		shouldAdd := c.Client.moduleConfig.VendorClosest
   577  
   578  		if !shouldAdd {
   579  			if _, found := c.vendored[path]; !found {
   580  				shouldAdd = true
   581  			}
   582  		}
   583  
   584  		if shouldAdd {
   585  			c.vendored[path] = vendoredModule{
   586  				Owner:   owner,
   587  				Dir:     filepath.Join(vendorDir, path),
   588  				Version: parts[1],
   589  			}
   590  		}
   591  
   592  	}
   593  	return nil
   594  }
   595  
   596  func (c *collector) loadModules() error {
   597  	modules, err := c.listGoMods()
   598  	if err != nil {
   599  		return err
   600  	}
   601  	c.gomods = modules
   602  	return nil
   603  }
   604  
   605  // Matches postcss.config.js etc.
   606  var commonJSConfigs = regexp.MustCompile(`(babel|postcss|tailwind)\.config\.js`)
   607  
   608  func (c *collector) mountCommonJSConfig(owner *moduleAdapter, mounts []Mount) ([]Mount, error) {
   609  	for _, m := range mounts {
   610  		if strings.HasPrefix(m.Target, files.JsConfigFolderMountPrefix) {
   611  			// This follows the convention of the other component types (assets, content, etc.),
   612  			// if one or more is specified by the user, we skip the defaults.
   613  			// These mounts were added to Hugo in 0.75.
   614  			return mounts, nil
   615  		}
   616  	}
   617  
   618  	// Mount the common JS config files.
   619  	fis, err := afero.ReadDir(c.fs, owner.Dir())
   620  	if err != nil {
   621  		return mounts, err
   622  	}
   623  
   624  	for _, fi := range fis {
   625  		n := fi.Name()
   626  
   627  		should := n == files.FilenamePackageHugoJSON || n == files.FilenamePackageJSON
   628  		should = should || commonJSConfigs.MatchString(n)
   629  
   630  		if should {
   631  			mounts = append(mounts, Mount{
   632  				Source: n,
   633  				Target: filepath.Join(files.ComponentFolderAssets, files.FolderJSConfig, n),
   634  			})
   635  		}
   636  
   637  	}
   638  
   639  	return mounts, nil
   640  }
   641  
   642  func (c *collector) normalizeMounts(owner *moduleAdapter, mounts []Mount) ([]Mount, error) {
   643  	var out []Mount
   644  	dir := owner.Dir()
   645  
   646  	for _, mnt := range mounts {
   647  		errMsg := fmt.Sprintf("invalid module config for %q", owner.Path())
   648  
   649  		if mnt.Source == "" || mnt.Target == "" {
   650  			return nil, errors.New(errMsg + ": both source and target must be set")
   651  		}
   652  
   653  		mnt.Source = filepath.Clean(mnt.Source)
   654  		mnt.Target = filepath.Clean(mnt.Target)
   655  		var sourceDir string
   656  
   657  		if owner.projectMod && filepath.IsAbs(mnt.Source) {
   658  			// Abs paths in the main project is allowed.
   659  			sourceDir = mnt.Source
   660  		} else {
   661  			sourceDir = filepath.Join(dir, mnt.Source)
   662  		}
   663  
   664  		// Verify that Source exists
   665  		_, err := c.fs.Stat(sourceDir)
   666  		if err != nil {
   667  			continue
   668  		}
   669  
   670  		// Verify that target points to one of the predefined component dirs
   671  		targetBase := mnt.Target
   672  		idxPathSep := strings.Index(mnt.Target, string(os.PathSeparator))
   673  		if idxPathSep != -1 {
   674  			targetBase = mnt.Target[0:idxPathSep]
   675  		}
   676  		if !files.IsComponentFolder(targetBase) {
   677  			return nil, fmt.Errorf("%s: mount target must be one of: %v", errMsg, files.ComponentFolders)
   678  		}
   679  
   680  		out = append(out, mnt)
   681  	}
   682  
   683  	return out, nil
   684  }
   685  
   686  func (c *collector) wrapModuleNotFound(err error) error {
   687  	err = fmt.Errorf(err.Error()+": %w", ErrNotExist)
   688  	if c.GoModulesFilename == "" {
   689  		return err
   690  	}
   691  
   692  	baseMsg := "we found a go.mod file in your project, but"
   693  
   694  	switch c.goBinaryStatus {
   695  	case goBinaryStatusNotFound:
   696  		return fmt.Errorf(baseMsg+" you need to install Go to use it. See https://golang.org/dl/ : %q", err)
   697  	case goBinaryStatusTooOld:
   698  		return fmt.Errorf(baseMsg+" you need to a newer version of Go to use it. See https://golang.org/dl/ : %w", err)
   699  	}
   700  
   701  	return err
   702  }
   703  
   704  type vendoredModule struct {
   705  	Owner   Module
   706  	Dir     string
   707  	Version string
   708  }
   709  
   710  func createProjectModule(gomod *goModule, workingDir string, conf Config) *moduleAdapter {
   711  	// Create a pseudo module for the main project.
   712  	var path string
   713  	if gomod == nil {
   714  		path = "project"
   715  	}
   716  
   717  	return &moduleAdapter{
   718  		path:       path,
   719  		dir:        workingDir,
   720  		gomod:      gomod,
   721  		projectMod: true,
   722  		config:     conf,
   723  	}
   724  }
   725  
   726  // In the first iteration of Hugo Modules, we do not support multiple
   727  // major versions running at the same time, so we pick the first (upper most).
   728  // We will investigate namespaces in future versions.
   729  // TODO(bep) add a warning when the above happens.
   730  func pathKey(p string) string {
   731  	prefix, _, _ := module.SplitPathVersion(p)
   732  
   733  	return strings.ToLower(prefix)
   734  }