kcl-lang.io/kpm@v0.8.7-0.20240520061008-9fc4c5efc8c7/pkg/package/modfile.go (about)

     1  // Copyright 2022 The KCL Authors. All rights reserved.
     2  package pkg
     3  
     4  import (
     5  	"errors"
     6  	"fmt"
     7  	"net/url"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/BurntSushi/toml"
    13  
    14  	"kcl-lang.io/kcl-go/pkg/kcl"
    15  	"oras.land/oras-go/v2/registry"
    16  
    17  	"kcl-lang.io/kpm/pkg/constants"
    18  	"kcl-lang.io/kpm/pkg/opt"
    19  	"kcl-lang.io/kpm/pkg/reporter"
    20  	"kcl-lang.io/kpm/pkg/runner"
    21  	"kcl-lang.io/kpm/pkg/settings"
    22  	"kcl-lang.io/kpm/pkg/utils"
    23  )
    24  
    25  const (
    26  	MOD_FILE      = "kcl.mod"
    27  	MOD_LOCK_FILE = "kcl.mod.lock"
    28  	GIT           = "git"
    29  	OCI           = "oci"
    30  	LOCAL         = "local"
    31  )
    32  
    33  // 'Package' is the kcl package section of 'kcl.mod'.
    34  type Package struct {
    35  	// The name of the package.
    36  	Name string `toml:"name,omitempty"`
    37  	// The kcl compiler version
    38  	Edition string `toml:"edition,omitempty"`
    39  	// The version of the package.
    40  	Version string `toml:"version,omitempty"`
    41  	// Description denotes the description of the package.
    42  	Description string `toml:"description,omitempty"` // kcl package description
    43  	// Exclude denote the files to include when publishing.
    44  	Include []string `toml:"include,omitempty"`
    45  	// Exclude denote the files to exclude when publishing.
    46  	Exclude []string `toml:"exclude,omitempty"`
    47  }
    48  
    49  // 'ModFile' is kcl package file 'kcl.mod'.
    50  type ModFile struct {
    51  	HomePath string  `toml:"-"`
    52  	Pkg      Package `toml:"package,omitempty"`
    53  	// Whether the current package uses the vendor mode
    54  	// In the vendor mode, kpm will look for the package in the vendor subdirectory
    55  	// in the current package directory.
    56  	VendorMode bool     `toml:"-"`
    57  	Profiles   *Profile `toml:"profile"`
    58  	Dependencies
    59  }
    60  
    61  // Profile is the profile section of 'kcl.mod'.
    62  // It is used to specify the compilation options of the current package.
    63  type Profile struct {
    64  	Entries     *[]string `toml:"entries"`
    65  	DisableNone *bool     `toml:"disable_none"`
    66  	SortKeys    *bool     `toml:"sort_keys"`
    67  	Selectors   *[]string `toml:"selectors"`
    68  	Overrides   *[]string `toml:"overrides"`
    69  	Options     *[]string `toml:"arguments"`
    70  }
    71  
    72  // NewProfile will create a new profile.
    73  func NewProfile() Profile {
    74  	return Profile{}
    75  }
    76  
    77  // IntoKclOptions will transform the profile into kcl options.
    78  func (profile *Profile) IntoKclOptions() *kcl.Option {
    79  
    80  	opts := kcl.NewOption()
    81  
    82  	if profile.Entries != nil {
    83  		for _, entry := range *profile.Entries {
    84  			ext := filepath.Ext(entry)
    85  			if ext == ".yaml" {
    86  				opts.Merge(kcl.WithSettings(entry))
    87  			} else {
    88  				opts.Merge(kcl.WithKFilenames(entry))
    89  			}
    90  		}
    91  	}
    92  
    93  	if profile.DisableNone != nil {
    94  		opts.Merge(kcl.WithDisableNone(*profile.DisableNone))
    95  	}
    96  
    97  	if profile.SortKeys != nil {
    98  		opts.Merge(kcl.WithSortKeys(*profile.SortKeys))
    99  	}
   100  
   101  	if profile.Selectors != nil {
   102  		opts.Merge(kcl.WithSelectors(*profile.Selectors...))
   103  	}
   104  
   105  	if profile.Overrides != nil {
   106  		opts.Merge(kcl.WithOverrides(*profile.Overrides...))
   107  	}
   108  
   109  	if profile.Options != nil {
   110  		opts.Merge(kcl.WithOptions(*profile.Options...))
   111  	}
   112  
   113  	return opts
   114  }
   115  
   116  // GetEntries will get the entry kcl files from profile.
   117  func (profile *Profile) GetEntries() []string {
   118  	if profile == nil || profile.Entries == nil {
   119  		return []string{}
   120  	}
   121  	return *profile.Entries
   122  }
   123  
   124  // FillDependenciesInfo will fill registry information for all dependencies in a kcl.mod.
   125  func (modFile *ModFile) FillDependenciesInfo() error {
   126  	for k, v := range modFile.Deps {
   127  		err := v.FillDepInfo(modFile.HomePath)
   128  		if err != nil {
   129  			return err
   130  		}
   131  		modFile.Deps[k] = v
   132  	}
   133  	return nil
   134  }
   135  
   136  // GetEntries will get the entry kcl files from kcl.mod.
   137  func (modFile *ModFile) GetEntries() []string {
   138  	if modFile.Profiles == nil {
   139  		return []string{}
   140  	}
   141  	return modFile.Profiles.GetEntries()
   142  }
   143  
   144  // 'Dependencies' is dependencies section of 'kcl.mod'.
   145  type Dependencies struct {
   146  	Deps map[string]Dependency `json:"packages" toml:"dependencies,omitempty"`
   147  }
   148  
   149  // ToDepMetadata will transform the dependencies into metadata.
   150  // And check whether the dependency name conflicts.
   151  func (deps *Dependencies) ToDepMetadata() (*Dependencies, error) {
   152  	depMetadata := Dependencies{
   153  		Deps: make(map[string]Dependency),
   154  	}
   155  	for _, d := range deps.Deps {
   156  		if _, ok := depMetadata.Deps[d.GetAliasName()]; ok {
   157  			return nil, reporter.NewErrorEvent(
   158  				reporter.PathIsEmpty,
   159  				fmt.Errorf("dependency name conflict, '%s' already exists", d.GetAliasName()),
   160  				"because '-' in the original dependency names is replaced with '_'\n",
   161  				"please check your dependencies with '-' or '_' in dependency name",
   162  			)
   163  		}
   164  		d.Name = d.GetAliasName()
   165  		depMetadata.Deps[d.GetAliasName()] = d
   166  	}
   167  
   168  	return &depMetadata, nil
   169  }
   170  
   171  type Dependency struct {
   172  	Name     string `json:"name" toml:"name,omitempty"`
   173  	FullName string `json:"-" toml:"full_name,omitempty"`
   174  	Version  string `json:"-" toml:"version,omitempty"`
   175  	Sum      string `json:"-" toml:"sum,omitempty"`
   176  	// The actual local path of the package.
   177  	// In vendor mode is "current_kcl_package/vendor"
   178  	// In non-vendor mode is "$KCL_PKG_PATH"
   179  	LocalFullPath string `json:"manifest_path" toml:"-"`
   180  	Source        `json:"-"`
   181  }
   182  
   183  func (d *Dependency) FromKclPkg(pkg *KclPkg) {
   184  	d.FullName = pkg.GetPkgFullName()
   185  	d.Version = pkg.GetPkgVersion()
   186  	d.LocalFullPath = pkg.HomePath
   187  }
   188  
   189  // SetName will set the name and alias name of a dependency.
   190  func (d *Dependency) GetAliasName() string {
   191  	return strings.ReplaceAll(d.Name, "-", "_")
   192  }
   193  
   194  // WithTheSameVersion will check whether two dependencies have the same version.
   195  func (d Dependency) WithTheSameVersion(other Dependency) bool {
   196  
   197  	var sameVersion = true
   198  	if len(d.Version) != 0 && len(other.Version) != 0 {
   199  		sameVersion = d.Version == other.Version
   200  
   201  	}
   202  	sameNameAndVersion := d.Name == other.Name && sameVersion
   203  	sameGitSrc := true
   204  	if d.Source.Git != nil && other.Source.Git != nil {
   205  		sameGitSrc = d.Source.Git.Url == other.Source.Git.Url &&
   206  			(d.Source.Git.Branch == other.Source.Git.Branch ||
   207  				d.Source.Git.Commit == other.Source.Git.Commit ||
   208  				d.Source.Git.Tag == other.Source.Git.Tag)
   209  	}
   210  
   211  	return sameNameAndVersion && sameGitSrc
   212  }
   213  
   214  // GetLocalFullPath will get the local path of a dependency.
   215  func (dep *Dependency) GetLocalFullPath(rootpath string) string {
   216  	if !filepath.IsAbs(dep.LocalFullPath) && dep.IsFromLocal() {
   217  		if filepath.IsAbs(dep.Source.Local.Path) {
   218  			return dep.Source.Local.Path
   219  		}
   220  		return filepath.Join(rootpath, dep.Source.Local.Path)
   221  	}
   222  	return dep.LocalFullPath
   223  }
   224  
   225  func (dep *Dependency) IsFromLocal() bool {
   226  	return dep.Source.Oci == nil && dep.Source.Git == nil && dep.Source.Local != nil
   227  }
   228  
   229  // FillDepInfo will fill registry information for a dependency.
   230  func (dep *Dependency) FillDepInfo(homepath string) error {
   231  	if dep.Source.Oci != nil {
   232  		settings := settings.GetSettings()
   233  		if settings.ErrorEvent != nil {
   234  			return settings.ErrorEvent
   235  		}
   236  		if dep.Source.Oci.Reg == "" {
   237  			dep.Source.Oci.Reg = settings.DefaultOciRegistry()
   238  		}
   239  
   240  		if dep.Source.Oci.Repo == "" {
   241  			urlpath := utils.JoinPath(settings.DefaultOciRepo(), dep.Name)
   242  			dep.Source.Oci.Repo = urlpath
   243  		}
   244  	}
   245  	if dep.Source.Local != nil {
   246  		dep.LocalFullPath = dep.Source.Local.Path
   247  	}
   248  	return nil
   249  }
   250  
   251  // GenDepFullName will generate the full name of a dependency by its name and version
   252  // based on the '<package_name>_<package_tag>' format.
   253  func (dep *Dependency) GenDepFullName() string {
   254  	dep.FullName = fmt.Sprintf(PKG_NAME_PATTERN, dep.Name, dep.Version)
   255  	return dep.FullName
   256  }
   257  
   258  // GetDownloadPath will get the download path of a dependency.
   259  func (dep *Dependency) GetDownloadPath() string {
   260  	if dep.Source.Git != nil {
   261  		return dep.Source.Git.Url
   262  	}
   263  	if dep.Source.Oci != nil {
   264  		return dep.Source.Oci.IntoOciUrl()
   265  	}
   266  	return ""
   267  }
   268  
   269  func GenSource(sourceType string, uri string, tagName string) (Source, error) {
   270  	source := Source{}
   271  	if sourceType == GIT {
   272  		source.Git = &Git{
   273  			Url: uri,
   274  			Tag: tagName,
   275  		}
   276  		return source, nil
   277  	}
   278  	if sourceType == OCI {
   279  		oci := Oci{}
   280  		_, err := oci.FromString(uri + ":" + tagName)
   281  		if err != nil {
   282  			return Source{}, err
   283  		}
   284  		source.Oci = &oci
   285  	}
   286  	if sourceType == LOCAL {
   287  		source.Local = &Local{
   288  			Path: uri,
   289  		}
   290  	}
   291  	return source, nil
   292  }
   293  
   294  // GetSourceType will get the source type of a dependency.
   295  func (dep *Dependency) GetSourceType() string {
   296  	if dep.Source.Git != nil {
   297  		return GIT
   298  	}
   299  	if dep.Source.Oci != nil {
   300  		return OCI
   301  	}
   302  	if dep.Source.Local != nil {
   303  		return LOCAL
   304  	}
   305  	return ""
   306  }
   307  
   308  // Source is the package source from registry.
   309  type Source struct {
   310  	*Git
   311  	*Oci
   312  	*Local `toml:"-"`
   313  }
   314  
   315  type Local struct {
   316  	Path string `toml:"path,omitempty"`
   317  }
   318  
   319  type Oci struct {
   320  	Reg  string `toml:"reg,omitempty"`
   321  	Repo string `toml:"repo,omitempty"`
   322  	Tag  string `toml:"oci_tag,omitempty"`
   323  }
   324  
   325  func (oci *Oci) IntoOciUrl() string {
   326  	if oci != nil {
   327  		u := &url.URL{
   328  			Scheme: constants.OciScheme,
   329  			Host:   oci.Reg,
   330  			Path:   oci.Repo,
   331  		}
   332  
   333  		return u.String()
   334  	}
   335  	return ""
   336  }
   337  
   338  func (oci *Oci) FromString(ociUrl string) (*Oci, error) {
   339  	u, err := url.Parse(ociUrl)
   340  	if err != nil {
   341  		return nil, err
   342  	}
   343  
   344  	if u.Scheme != constants.OciScheme {
   345  		return nil, fmt.Errorf("invalid oci url with schema: %s", u.Scheme)
   346  	}
   347  
   348  	ref, err := registry.ParseReference(u.Host + u.Path)
   349  	if err != nil {
   350  		return nil, fmt.Errorf("'%s' invalid URL format: %w", ociUrl, err)
   351  	}
   352  
   353  	oci.Reg = ref.Registry
   354  	oci.Repo = ref.Repository
   355  	oci.Tag = ref.ReferenceOrDefault()
   356  
   357  	return oci, nil
   358  }
   359  
   360  // Git is the package source from git registry.
   361  type Git struct {
   362  	Url     string `toml:"url,omitempty"`
   363  	Branch  string `toml:"branch,omitempty"`
   364  	Commit  string `toml:"commit,omitempty"`
   365  	Tag     string `toml:"git_tag,omitempty"`
   366  	Version string `toml:"version,omitempty"`
   367  }
   368  
   369  // GetValidGitReference will get the valid git reference from git source.
   370  // Only one of branch, tag or commit is allowed.
   371  func (git *Git) GetValidGitReference() (string, error) {
   372  	nonEmptyFields := 0
   373  	var nonEmptyRef string
   374  
   375  	if git.Tag != "" {
   376  		nonEmptyFields++
   377  		nonEmptyRef = git.Tag
   378  	}
   379  	if git.Commit != "" {
   380  		nonEmptyFields++
   381  		nonEmptyRef = git.Commit
   382  	}
   383  	if git.Branch != "" {
   384  		nonEmptyFields++
   385  		nonEmptyRef = git.Branch
   386  	}
   387  
   388  	if nonEmptyFields != 1 {
   389  		return "", errors.New("only one of branch, tag or commit is allowed")
   390  	}
   391  
   392  	return nonEmptyRef, nil
   393  }
   394  
   395  // ModFileExists returns whether a 'kcl.mod' file exists in the path.
   396  func ModFileExists(path string) (bool, error) {
   397  	return utils.Exists(filepath.Join(path, MOD_FILE))
   398  }
   399  
   400  // ModLockFileExists returns whether a 'kcl.mod.lock' file exists in the path.
   401  func ModLockFileExists(path string) (bool, error) {
   402  	return utils.Exists(filepath.Join(path, MOD_LOCK_FILE))
   403  }
   404  
   405  // LoadLockDeps will load all dependencies from 'kcl.mod.lock'.
   406  func LoadLockDeps(homePath string) (*Dependencies, error) {
   407  	deps := new(Dependencies)
   408  	deps.Deps = make(map[string]Dependency)
   409  	err := deps.loadLockFile(filepath.Join(homePath, MOD_LOCK_FILE))
   410  
   411  	if os.IsNotExist(err) {
   412  		return deps, nil
   413  	}
   414  
   415  	if err != nil {
   416  		return nil, err
   417  	}
   418  
   419  	return deps, nil
   420  }
   421  
   422  // Write the contents of 'ModFile' to 'kcl.mod' file
   423  func (mfile *ModFile) StoreModFile() error {
   424  	fullPath := filepath.Join(mfile.HomePath, MOD_FILE)
   425  	return utils.StoreToFile(fullPath, mfile.MarshalTOML())
   426  }
   427  
   428  // Returns the path to the kcl.mod file
   429  func (mfile *ModFile) GetModFilePath() string {
   430  	return filepath.Join(mfile.HomePath, MOD_FILE)
   431  }
   432  
   433  // Returns the path to the kcl.mod.lock file
   434  func (mfile *ModFile) GetModLockFilePath() string {
   435  	return filepath.Join(mfile.HomePath, MOD_LOCK_FILE)
   436  }
   437  
   438  const defaultVerion = "0.0.1"
   439  
   440  var defaultEdition = runner.GetKclVersion()
   441  
   442  func NewModFile(opts *opt.InitOptions) *ModFile {
   443  	if opts.Version == "" {
   444  		opts.Version = defaultVerion
   445  	}
   446  	return &ModFile{
   447  		HomePath: opts.InitPath,
   448  		Pkg: Package{
   449  			Name:    opts.Name,
   450  			Version: opts.Version,
   451  			Edition: defaultEdition,
   452  		},
   453  		Dependencies: Dependencies{
   454  			Deps: make(map[string]Dependency),
   455  		},
   456  	}
   457  }
   458  
   459  // Load the kcl.mod file.
   460  func (mod *ModFile) LoadModFile(filepath string) error {
   461  
   462  	modData, err := os.ReadFile(filepath)
   463  	if err != nil {
   464  		return err
   465  	}
   466  
   467  	err = toml.Unmarshal(modData, &mod)
   468  
   469  	if err != nil {
   470  		return err
   471  	}
   472  
   473  	return nil
   474  }
   475  
   476  // LoadModFile load the contents of the 'kcl.mod' file in the path.
   477  func LoadModFile(homePath string) (*ModFile, error) {
   478  	modFile := new(ModFile)
   479  	err := modFile.LoadModFile(filepath.Join(homePath, MOD_FILE))
   480  	if err != nil {
   481  		return nil, err
   482  	}
   483  
   484  	modFile.HomePath = homePath
   485  
   486  	if modFile.Dependencies.Deps == nil {
   487  		modFile.Dependencies.Deps = make(map[string]Dependency)
   488  	}
   489  	err = modFile.FillDependenciesInfo()
   490  	if err != nil {
   491  		return nil, err
   492  	}
   493  
   494  	return modFile, nil
   495  }
   496  
   497  // Load the kcl.mod.lock file.
   498  func (deps *Dependencies) loadLockFile(filepath string) error {
   499  	data, err := os.ReadFile(filepath)
   500  	if os.IsNotExist(err) {
   501  		return err
   502  	}
   503  
   504  	if err != nil {
   505  		return reporter.NewErrorEvent(reporter.FailedLoadKclModLock, err, fmt.Sprintf("failed to load '%s'", filepath))
   506  	}
   507  
   508  	err = deps.UnmarshalLockTOML(string(data))
   509  
   510  	if err != nil {
   511  		return reporter.NewErrorEvent(reporter.FailedLoadKclModLock, err, fmt.Sprintf("failed to load '%s'", filepath))
   512  	}
   513  
   514  	return nil
   515  }
   516  
   517  // Parse out some information for a Dependency from registry url.
   518  func ParseOpt(opt *opt.RegistryOptions) (*Dependency, error) {
   519  	if opt.Git != nil {
   520  		gitSource := Git{
   521  			Url:    opt.Git.Url,
   522  			Branch: opt.Git.Branch,
   523  			Commit: opt.Git.Commit,
   524  			Tag:    opt.Git.Tag,
   525  		}
   526  
   527  		gitRef, err := gitSource.GetValidGitReference()
   528  		if err != nil {
   529  			return nil, err
   530  		}
   531  
   532  		fullName, err := ParseRepoFullNameFromGitSource(gitSource)
   533  		if err != nil {
   534  			return nil, err
   535  		}
   536  
   537  		return &Dependency{
   538  			Name:     ParseRepoNameFromGitSource(gitSource),
   539  			FullName: fullName,
   540  			Source: Source{
   541  				Git: &gitSource,
   542  			},
   543  			Version: gitRef,
   544  		}, nil
   545  	}
   546  	if opt.Oci != nil {
   547  		repoPath := utils.JoinPath(opt.Oci.Repo, opt.Oci.PkgName)
   548  		ociSource := Oci{
   549  			Reg:  opt.Oci.Reg,
   550  			Repo: repoPath,
   551  			Tag:  opt.Oci.Tag,
   552  		}
   553  
   554  		return &Dependency{
   555  			Name:     opt.Oci.PkgName,
   556  			FullName: opt.Oci.PkgName + "_" + opt.Oci.Tag,
   557  			Source: Source{
   558  				Oci: &ociSource,
   559  			},
   560  			Version: opt.Oci.Tag,
   561  		}, nil
   562  	}
   563  	if opt.Local != nil {
   564  		depPkg, err := LoadKclPkg(opt.Local.Path)
   565  		if err != nil {
   566  			return nil, err
   567  		}
   568  
   569  		return &Dependency{
   570  			Name:          depPkg.ModFile.Pkg.Name,
   571  			FullName:      depPkg.ModFile.Pkg.Name + "_" + depPkg.ModFile.Pkg.Version,
   572  			LocalFullPath: opt.Local.Path,
   573  			Source: Source{
   574  				Local: &Local{
   575  					Path: opt.Local.Path,
   576  				},
   577  			},
   578  			Version: depPkg.ModFile.Pkg.Version,
   579  		}, nil
   580  
   581  	}
   582  	return nil, nil
   583  }
   584  
   585  const PKG_NAME_PATTERN = "%s_%s"
   586  
   587  // ParseRepoFullNameFromGitSource will extract the kcl package name from the git url.
   588  func ParseRepoFullNameFromGitSource(gitSrc Git) (string, error) {
   589  	ref, err := gitSrc.GetValidGitReference()
   590  	if err != nil {
   591  		return "", err
   592  	}
   593  	if len(ref) != 0 {
   594  		return fmt.Sprintf(PKG_NAME_PATTERN, utils.ParseRepoNameFromGitUrl(gitSrc.Url), ref), nil
   595  	}
   596  	return utils.ParseRepoNameFromGitUrl(gitSrc.Url), nil
   597  }
   598  
   599  // ParseRepoNameFromGitSource will extract the kcl package name from the git url.
   600  func ParseRepoNameFromGitSource(gitSrc Git) string {
   601  	return utils.ParseRepoNameFromGitUrl(gitSrc.Url)
   602  }