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 }