github.com/kerryoscer/gqlgen@v0.17.29/codegen/config/config.go (about)

     1  package config
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"regexp"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/kerryoscer/gqlgen/internal/code"
    13  	"github.com/vektah/gqlparser/v2/ast"
    14  )
    15  
    16  type Config struct {
    17  	SchemaFilename                StringList                 `yaml:"schema,omitempty"`
    18  	Exec                          ExecConfig                 `yaml:"exec"`
    19  	Model                         PackageConfig              `yaml:"model,omitempty"`
    20  	Federation                    PackageConfig              `yaml:"federation,omitempty"`
    21  	Resolver                      ResolverConfig             `yaml:"resolver,omitempty"`
    22  	AutoBind                      []string                   `yaml:"autobind"`
    23  	Models                        TypeMap                    `yaml:"models,omitempty"`
    24  	StructTag                     string                     `yaml:"struct_tag,omitempty"`
    25  	Directives                    map[string]DirectiveConfig `yaml:"directives,omitempty"`
    26  	OmitSliceElementPointers      bool                       `yaml:"omit_slice_element_pointers,omitempty"`
    27  	OmitGetters                   bool                       `yaml:"omit_getters,omitempty"`
    28  	OmitComplexity                bool                       `yaml:"omit_complexity,omitempty"`
    29  	StructFieldsAlwaysPointers    bool                       `yaml:"struct_fields_always_pointers,omitempty"`
    30  	ReturnPointersInUmarshalInput bool                       `yaml:"return_pointers_in_unmarshalinput,omitempty"`
    31  	ResolversAlwaysReturnPointers bool                       `yaml:"resolvers_always_return_pointers,omitempty"`
    32  	SkipValidation                bool                       `yaml:"skip_validation,omitempty"`
    33  	SkipModTidy                   bool                       `yaml:"skip_mod_tidy,omitempty"`
    34  	Sources                       []*ast.Source              `yaml:"-"`
    35  	Packages                      *code.Packages             `yaml:"-"`
    36  	Schema                        *ast.Schema                `yaml:"-"`
    37  
    38  	// Deprecated: use Federation instead. Will be removed next release
    39  	Federated bool `yaml:"federated,omitempty"`
    40  }
    41  
    42  var cfgFilenames = []string{".gqlgen.yml", "gqlgen.yml", "gqlgen.yaml"}
    43  
    44  // DefaultConfig creates a copy of the default config
    45  func DefaultConfig() *Config {
    46  	return &Config{
    47  		SchemaFilename:                StringList{"schema.graphql"},
    48  		Model:                         PackageConfig{Filename: "models_gen.go"},
    49  		Exec:                          ExecConfig{Filename: "generated.go"},
    50  		Directives:                    map[string]DirectiveConfig{},
    51  		Models:                        TypeMap{},
    52  		StructFieldsAlwaysPointers:    true,
    53  		ReturnPointersInUmarshalInput: false,
    54  		ResolversAlwaysReturnPointers: true,
    55  	}
    56  }
    57  
    58  // LoadDefaultConfig loads the default config so that it is ready to be used
    59  func LoadDefaultConfig() (*Config, error) {
    60  	config := DefaultConfig()
    61  
    62  	for _, filename := range config.SchemaFilename {
    63  		filename = filepath.ToSlash(filename)
    64  		var err error
    65  		var schemaRaw []byte
    66  		schemaRaw, err = os.ReadFile(filename)
    67  		if err != nil {
    68  			return nil, fmt.Errorf("unable to open schema: %w", err)
    69  		}
    70  
    71  		config.Sources = append(config.Sources, &ast.Source{Name: filename, Input: string(schemaRaw)})
    72  	}
    73  
    74  	return config, nil
    75  }
    76  
    77  // LoadConfigFromDefaultLocations looks for a config file in the current directory, and all parent directories
    78  // walking up the tree. The closest config file will be returned.
    79  func LoadConfigFromDefaultLocations() (*Config, error) {
    80  	cfgFile, err := findCfg()
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	err = os.Chdir(filepath.Dir(cfgFile))
    86  	if err != nil {
    87  		return nil, fmt.Errorf("unable to enter config dir: %w", err)
    88  	}
    89  	return LoadConfig(cfgFile)
    90  }
    91  
    92  var path2regex = strings.NewReplacer(
    93  	`.`, `\.`,
    94  	`*`, `.+`,
    95  	`\`, `[\\/]`,
    96  	`/`, `[\\/]`,
    97  )
    98  
    99  // LoadConfig reads the gqlgen.yml config file
   100  func LoadConfig(filename string) (*Config, error) {
   101  	config := DefaultConfig()
   102  
   103  	b, err := os.ReadFile(filename)
   104  	if err != nil {
   105  		return nil, fmt.Errorf("unable to read config: %w", err)
   106  	}
   107  
   108  	dec := yaml.NewDecoder(bytes.NewReader(b))
   109  	dec.KnownFields(true)
   110  
   111  	if err := dec.Decode(config); err != nil {
   112  		return nil, fmt.Errorf("unable to parse config: %w", err)
   113  	}
   114  
   115  	if err := CompleteConfig(config); err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	return config, nil
   120  }
   121  
   122  // CompleteConfig fills in the schema and other values to a config loaded from
   123  // YAML.
   124  func CompleteConfig(config *Config) error {
   125  	defaultDirectives := map[string]DirectiveConfig{
   126  		"skip":        {SkipRuntime: true},
   127  		"include":     {SkipRuntime: true},
   128  		"deprecated":  {SkipRuntime: true},
   129  		"specifiedBy": {SkipRuntime: true},
   130  	}
   131  
   132  	for key, value := range defaultDirectives {
   133  		if _, defined := config.Directives[key]; !defined {
   134  			config.Directives[key] = value
   135  		}
   136  	}
   137  
   138  	preGlobbing := config.SchemaFilename
   139  	config.SchemaFilename = StringList{}
   140  	for _, f := range preGlobbing {
   141  		var matches []string
   142  
   143  		// for ** we want to override default globbing patterns and walk all
   144  		// subdirectories to match schema files.
   145  		if strings.Contains(f, "**") {
   146  			pathParts := strings.SplitN(f, "**", 2)
   147  			rest := strings.TrimPrefix(strings.TrimPrefix(pathParts[1], `\`), `/`)
   148  			// turn the rest of the glob into a regex, anchored only at the end because ** allows
   149  			// for any number of dirs in between and walk will let us match against the full path name
   150  			globRe := regexp.MustCompile(path2regex.Replace(rest) + `$`)
   151  
   152  			if err := filepath.Walk(pathParts[0], func(path string, info os.FileInfo, err error) error {
   153  				if err != nil {
   154  					return err
   155  				}
   156  
   157  				if globRe.MatchString(strings.TrimPrefix(path, pathParts[0])) {
   158  					matches = append(matches, path)
   159  				}
   160  
   161  				return nil
   162  			}); err != nil {
   163  				return fmt.Errorf("failed to walk schema at root %s: %w", pathParts[0], err)
   164  			}
   165  		} else {
   166  			var err error
   167  			matches, err = filepath.Glob(f)
   168  			if err != nil {
   169  				return fmt.Errorf("failed to glob schema filename %s: %w", f, err)
   170  			}
   171  		}
   172  
   173  		for _, m := range matches {
   174  			if config.SchemaFilename.Has(m) {
   175  				continue
   176  			}
   177  			config.SchemaFilename = append(config.SchemaFilename, m)
   178  		}
   179  	}
   180  
   181  	for _, filename := range config.SchemaFilename {
   182  		filename = filepath.ToSlash(filename)
   183  		var err error
   184  		var schemaRaw []byte
   185  		schemaRaw, err = os.ReadFile(filename)
   186  		if err != nil {
   187  			return fmt.Errorf("unable to open schema: %w", err)
   188  		}
   189  
   190  		config.Sources = append(config.Sources, &ast.Source{Name: filename, Input: string(schemaRaw)})
   191  	}
   192  	return nil
   193  }
   194  
   195  func (c *Config) Init() error {
   196  	if c.Packages == nil {
   197  		c.Packages = &code.Packages{}
   198  	}
   199  
   200  	if c.Schema == nil {
   201  		if err := c.LoadSchema(); err != nil {
   202  			return err
   203  		}
   204  	}
   205  
   206  	err := c.injectTypesFromSchema()
   207  	if err != nil {
   208  		return err
   209  	}
   210  
   211  	err = c.autobind()
   212  	if err != nil {
   213  		return err
   214  	}
   215  
   216  	c.injectBuiltins()
   217  	// prefetch all packages in one big packages.Load call
   218  	c.Packages.LoadAll(c.packageList()...)
   219  
   220  	//  check everything is valid on the way out
   221  	err = c.check()
   222  	if err != nil {
   223  		return err
   224  	}
   225  
   226  	return nil
   227  }
   228  
   229  func (c *Config) packageList() []string {
   230  	pkgs := []string{
   231  		"github.com/kerryoscer/gqlgen/graphql",
   232  		"github.com/kerryoscer/gqlgen/graphql/introspection",
   233  	}
   234  	pkgs = append(pkgs, c.Models.ReferencedPackages()...)
   235  	pkgs = append(pkgs, c.AutoBind...)
   236  	return pkgs
   237  }
   238  
   239  func (c *Config) ReloadAllPackages() {
   240  	c.Packages.ReloadAll(c.packageList()...)
   241  }
   242  
   243  func (c *Config) injectTypesFromSchema() error {
   244  	c.Directives["goModel"] = DirectiveConfig{
   245  		SkipRuntime: true,
   246  	}
   247  
   248  	c.Directives["goField"] = DirectiveConfig{
   249  		SkipRuntime: true,
   250  	}
   251  
   252  	c.Directives["goTag"] = DirectiveConfig{
   253  		SkipRuntime: true,
   254  	}
   255  
   256  	for _, schemaType := range c.Schema.Types {
   257  		if schemaType == c.Schema.Query || schemaType == c.Schema.Mutation || schemaType == c.Schema.Subscription {
   258  			continue
   259  		}
   260  
   261  		if bd := schemaType.Directives.ForName("goModel"); bd != nil {
   262  			if ma := bd.Arguments.ForName("model"); ma != nil {
   263  				if mv, err := ma.Value.Value(nil); err == nil {
   264  					c.Models.Add(schemaType.Name, mv.(string))
   265  				}
   266  			}
   267  			if ma := bd.Arguments.ForName("models"); ma != nil {
   268  				if mvs, err := ma.Value.Value(nil); err == nil {
   269  					for _, mv := range mvs.([]interface{}) {
   270  						c.Models.Add(schemaType.Name, mv.(string))
   271  					}
   272  				}
   273  			}
   274  		}
   275  
   276  		if schemaType.Kind == ast.Object || schemaType.Kind == ast.InputObject {
   277  			for _, field := range schemaType.Fields {
   278  				if fd := field.Directives.ForName("goField"); fd != nil {
   279  					forceResolver := c.Models[schemaType.Name].Fields[field.Name].Resolver
   280  					fieldName := c.Models[schemaType.Name].Fields[field.Name].FieldName
   281  
   282  					if ra := fd.Arguments.ForName("forceResolver"); ra != nil {
   283  						if fr, err := ra.Value.Value(nil); err == nil {
   284  							forceResolver = fr.(bool)
   285  						}
   286  					}
   287  
   288  					if na := fd.Arguments.ForName("name"); na != nil {
   289  						if fr, err := na.Value.Value(nil); err == nil {
   290  							fieldName = fr.(string)
   291  						}
   292  					}
   293  
   294  					if c.Models[schemaType.Name].Fields == nil {
   295  						c.Models[schemaType.Name] = TypeMapEntry{
   296  							Model:  c.Models[schemaType.Name].Model,
   297  							Fields: map[string]TypeMapField{},
   298  						}
   299  					}
   300  
   301  					c.Models[schemaType.Name].Fields[field.Name] = TypeMapField{
   302  						FieldName: fieldName,
   303  						Resolver:  forceResolver,
   304  					}
   305  				}
   306  			}
   307  		}
   308  	}
   309  
   310  	return nil
   311  }
   312  
   313  type TypeMapEntry struct {
   314  	Model  StringList              `yaml:"model"`
   315  	Fields map[string]TypeMapField `yaml:"fields,omitempty"`
   316  }
   317  
   318  type TypeMapField struct {
   319  	Resolver        bool   `yaml:"resolver"`
   320  	FieldName       string `yaml:"fieldName"`
   321  	GeneratedMethod string `yaml:"-"`
   322  }
   323  
   324  type StringList []string
   325  
   326  func (a *StringList) UnmarshalYAML(unmarshal func(interface{}) error) error {
   327  	var single string
   328  	err := unmarshal(&single)
   329  	if err == nil {
   330  		*a = []string{single}
   331  		return nil
   332  	}
   333  
   334  	var multi []string
   335  	err = unmarshal(&multi)
   336  	if err != nil {
   337  		return err
   338  	}
   339  
   340  	*a = multi
   341  	return nil
   342  }
   343  
   344  func (a StringList) Has(file string) bool {
   345  	for _, existing := range a {
   346  		if existing == file {
   347  			return true
   348  		}
   349  	}
   350  	return false
   351  }
   352  
   353  func (c *Config) check() error {
   354  	if c.Models == nil {
   355  		c.Models = TypeMap{}
   356  	}
   357  
   358  	type FilenamePackage struct {
   359  		Filename string
   360  		Package  string
   361  		Declaree string
   362  	}
   363  
   364  	fileList := map[string][]FilenamePackage{}
   365  
   366  	if err := c.Models.Check(); err != nil {
   367  		return fmt.Errorf("config.models: %w", err)
   368  	}
   369  	if err := c.Exec.Check(); err != nil {
   370  		return fmt.Errorf("config.exec: %w", err)
   371  	}
   372  	fileList[c.Exec.ImportPath()] = append(fileList[c.Exec.ImportPath()], FilenamePackage{
   373  		Filename: c.Exec.Filename,
   374  		Package:  c.Exec.Package,
   375  		Declaree: "exec",
   376  	})
   377  
   378  	if c.Model.IsDefined() {
   379  		if err := c.Model.Check(); err != nil {
   380  			return fmt.Errorf("config.model: %w", err)
   381  		}
   382  		fileList[c.Model.ImportPath()] = append(fileList[c.Model.ImportPath()], FilenamePackage{
   383  			Filename: c.Model.Filename,
   384  			Package:  c.Model.Package,
   385  			Declaree: "model",
   386  		})
   387  	}
   388  	if c.Resolver.IsDefined() {
   389  		if err := c.Resolver.Check(); err != nil {
   390  			return fmt.Errorf("config.resolver: %w", err)
   391  		}
   392  		fileList[c.Resolver.ImportPath()] = append(fileList[c.Resolver.ImportPath()], FilenamePackage{
   393  			Filename: c.Resolver.Filename,
   394  			Package:  c.Resolver.Package,
   395  			Declaree: "resolver",
   396  		})
   397  	}
   398  	if c.Federation.IsDefined() {
   399  		if err := c.Federation.Check(); err != nil {
   400  			return fmt.Errorf("config.federation: %w", err)
   401  		}
   402  		fileList[c.Federation.ImportPath()] = append(fileList[c.Federation.ImportPath()], FilenamePackage{
   403  			Filename: c.Federation.Filename,
   404  			Package:  c.Federation.Package,
   405  			Declaree: "federation",
   406  		})
   407  		if c.Federation.ImportPath() != c.Exec.ImportPath() {
   408  			return fmt.Errorf("federation and exec must be in the same package")
   409  		}
   410  	}
   411  	if c.Federated {
   412  		return fmt.Errorf("federated has been removed, instead use\nfederation:\n    filename: path/to/federated.go")
   413  	}
   414  
   415  	for importPath, pkg := range fileList {
   416  		for _, file1 := range pkg {
   417  			for _, file2 := range pkg {
   418  				if file1.Package != file2.Package {
   419  					return fmt.Errorf("%s and %s define the same import path (%s) with different package names (%s vs %s)",
   420  						file1.Declaree,
   421  						file2.Declaree,
   422  						importPath,
   423  						file1.Package,
   424  						file2.Package,
   425  					)
   426  				}
   427  			}
   428  		}
   429  	}
   430  
   431  	return nil
   432  }
   433  
   434  type TypeMap map[string]TypeMapEntry
   435  
   436  func (tm TypeMap) Exists(typeName string) bool {
   437  	_, ok := tm[typeName]
   438  	return ok
   439  }
   440  
   441  func (tm TypeMap) UserDefined(typeName string) bool {
   442  	m, ok := tm[typeName]
   443  	return ok && len(m.Model) > 0
   444  }
   445  
   446  func (tm TypeMap) Check() error {
   447  	for typeName, entry := range tm {
   448  		for _, model := range entry.Model {
   449  			if strings.LastIndex(model, ".") < strings.LastIndex(model, "/") {
   450  				return fmt.Errorf("model %s: invalid type specifier \"%s\" - you need to specify a struct to map to", typeName, entry.Model)
   451  			}
   452  		}
   453  	}
   454  	return nil
   455  }
   456  
   457  func (tm TypeMap) ReferencedPackages() []string {
   458  	var pkgs []string
   459  
   460  	for _, typ := range tm {
   461  		for _, model := range typ.Model {
   462  			if model == "map[string]interface{}" || model == "interface{}" {
   463  				continue
   464  			}
   465  			pkg, _ := code.PkgAndType(model)
   466  			if pkg == "" || inStrSlice(pkgs, pkg) {
   467  				continue
   468  			}
   469  			pkgs = append(pkgs, code.QualifyPackagePath(pkg))
   470  		}
   471  	}
   472  
   473  	sort.Slice(pkgs, func(i, j int) bool {
   474  		return pkgs[i] > pkgs[j]
   475  	})
   476  	return pkgs
   477  }
   478  
   479  func (tm TypeMap) Add(name string, goType string) {
   480  	modelCfg := tm[name]
   481  	modelCfg.Model = append(modelCfg.Model, goType)
   482  	tm[name] = modelCfg
   483  }
   484  
   485  type DirectiveConfig struct {
   486  	SkipRuntime bool `yaml:"skip_runtime"`
   487  }
   488  
   489  func inStrSlice(haystack []string, needle string) bool {
   490  	for _, v := range haystack {
   491  		if needle == v {
   492  			return true
   493  		}
   494  	}
   495  
   496  	return false
   497  }
   498  
   499  // findCfg searches for the config file in this directory and all parents up the tree
   500  // looking for the closest match
   501  func findCfg() (string, error) {
   502  	dir, err := os.Getwd()
   503  	if err != nil {
   504  		return "", fmt.Errorf("unable to get working dir to findCfg: %w", err)
   505  	}
   506  
   507  	cfg := findCfgInDir(dir)
   508  
   509  	for cfg == "" && dir != filepath.Dir(dir) {
   510  		dir = filepath.Dir(dir)
   511  		cfg = findCfgInDir(dir)
   512  	}
   513  
   514  	if cfg == "" {
   515  		return "", os.ErrNotExist
   516  	}
   517  
   518  	return cfg, nil
   519  }
   520  
   521  func findCfgInDir(dir string) string {
   522  	for _, cfgName := range cfgFilenames {
   523  		path := filepath.Join(dir, cfgName)
   524  		if _, err := os.Stat(path); err == nil {
   525  			return path
   526  		}
   527  	}
   528  	return ""
   529  }
   530  
   531  func (c *Config) autobind() error {
   532  	if len(c.AutoBind) == 0 {
   533  		return nil
   534  	}
   535  
   536  	ps := c.Packages.LoadAll(c.AutoBind...)
   537  
   538  	for _, t := range c.Schema.Types {
   539  		if c.Models.UserDefined(t.Name) {
   540  			continue
   541  		}
   542  
   543  		for i, p := range ps {
   544  			if p == nil || p.Module == nil {
   545  				return fmt.Errorf("unable to load %s - make sure you're using an import path to a package that exists", c.AutoBind[i])
   546  			}
   547  			if t := p.Types.Scope().Lookup(t.Name); t != nil {
   548  				c.Models.Add(t.Name(), t.Pkg().Path()+"."+t.Name())
   549  				break
   550  			}
   551  		}
   552  	}
   553  
   554  	for i, t := range c.Models {
   555  		for j, m := range t.Model {
   556  			pkg, typename := code.PkgAndType(m)
   557  
   558  			// skip anything that looks like an import path
   559  			if strings.Contains(pkg, "/") {
   560  				continue
   561  			}
   562  
   563  			for _, p := range ps {
   564  				if p.Name != pkg {
   565  					continue
   566  				}
   567  				if t := p.Types.Scope().Lookup(typename); t != nil {
   568  					c.Models[i].Model[j] = t.Pkg().Path() + "." + t.Name()
   569  					break
   570  				}
   571  			}
   572  		}
   573  	}
   574  
   575  	return nil
   576  }
   577  
   578  func (c *Config) injectBuiltins() {
   579  	builtins := TypeMap{
   580  		"__Directive":         {Model: StringList{"github.com/kerryoscer/gqlgen/graphql/introspection.Directive"}},
   581  		"__DirectiveLocation": {Model: StringList{"github.com/kerryoscer/gqlgen/graphql.String"}},
   582  		"__Type":              {Model: StringList{"github.com/kerryoscer/gqlgen/graphql/introspection.Type"}},
   583  		"__TypeKind":          {Model: StringList{"github.com/kerryoscer/gqlgen/graphql.String"}},
   584  		"__Field":             {Model: StringList{"github.com/kerryoscer/gqlgen/graphql/introspection.Field"}},
   585  		"__EnumValue":         {Model: StringList{"github.com/kerryoscer/gqlgen/graphql/introspection.EnumValue"}},
   586  		"__InputValue":        {Model: StringList{"github.com/kerryoscer/gqlgen/graphql/introspection.InputValue"}},
   587  		"__Schema":            {Model: StringList{"github.com/kerryoscer/gqlgen/graphql/introspection.Schema"}},
   588  		"Float":               {Model: StringList{"github.com/kerryoscer/gqlgen/graphql.FloatContext"}},
   589  		"String":              {Model: StringList{"github.com/kerryoscer/gqlgen/graphql.String"}},
   590  		"Boolean":             {Model: StringList{"github.com/kerryoscer/gqlgen/graphql.Boolean"}},
   591  		"Int": {Model: StringList{
   592  			"github.com/kerryoscer/gqlgen/graphql.Int",
   593  			"github.com/kerryoscer/gqlgen/graphql.Int32",
   594  			"github.com/kerryoscer/gqlgen/graphql.Int64",
   595  		}},
   596  		"ID": {
   597  			Model: StringList{
   598  				"github.com/kerryoscer/gqlgen/graphql.ID",
   599  				"github.com/kerryoscer/gqlgen/graphql.IntID",
   600  			},
   601  		},
   602  	}
   603  
   604  	for typeName, entry := range builtins {
   605  		if !c.Models.Exists(typeName) {
   606  			c.Models[typeName] = entry
   607  		}
   608  	}
   609  
   610  	// These are additional types that are injected if defined in the schema as scalars.
   611  	extraBuiltins := TypeMap{
   612  		"Time":   {Model: StringList{"github.com/kerryoscer/gqlgen/graphql.Time"}},
   613  		"Map":    {Model: StringList{"github.com/kerryoscer/gqlgen/graphql.Map"}},
   614  		"Upload": {Model: StringList{"github.com/kerryoscer/gqlgen/graphql.Upload"}},
   615  		"Any":    {Model: StringList{"github.com/kerryoscer/gqlgen/graphql.Any"}},
   616  	}
   617  
   618  	for typeName, entry := range extraBuiltins {
   619  		if t, ok := c.Schema.Types[typeName]; !c.Models.Exists(typeName) && ok && t.Kind == ast.Scalar {
   620  			c.Models[typeName] = entry
   621  		}
   622  	}
   623  }
   624  
   625  func (c *Config) LoadSchema() error {
   626  	if c.Packages != nil {
   627  		c.Packages = &code.Packages{}
   628  	}
   629  
   630  	if err := c.check(); err != nil {
   631  		return err
   632  	}
   633  
   634  	schema, err := gqlparser.LoadSchema(c.Sources...)
   635  	if err != nil {
   636  		return err
   637  	}
   638  
   639  	if schema.Query == nil {
   640  		schema.Query = &ast.Definition{
   641  			Kind: ast.Object,
   642  			Name: "Query",
   643  		}
   644  		schema.Types["Query"] = schema.Query
   645  	}
   646  
   647  	c.Schema = schema
   648  	return nil
   649  }
   650  
   651  func abs(path string) string {
   652  	absPath, err := filepath.Abs(path)
   653  	if err != nil {
   654  		panic(err)
   655  	}
   656  	return filepath.ToSlash(absPath)
   657  }