blob: fb29d3741bfb38a944df4ffb2dd5aa67708c9e5d [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package project
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
log "github.com/Sirupsen/logrus"
"mynewt.apache.org/newt/newt/compat"
"mynewt.apache.org/newt/newt/deprepo"
"mynewt.apache.org/newt/newt/downloader"
"mynewt.apache.org/newt/newt/install"
"mynewt.apache.org/newt/newt/interfaces"
"mynewt.apache.org/newt/newt/newtutil"
"mynewt.apache.org/newt/newt/pkg"
"mynewt.apache.org/newt/newt/repo"
"mynewt.apache.org/newt/newt/ycfg"
"mynewt.apache.org/newt/util"
)
var globalProject *Project = nil
const PROJECT_FILE_NAME = "project.yml"
var ignoreSearchDirs []string = []string{
"bin",
"repos",
}
type Project struct {
// Name of this project
name string
// Base path of the project
BasePath string
packages interfaces.PackageList
// Contains all the repos that form this project. Each repo is in one of
// two states:
// * description: Only the repo's basic description fields have been
// read from `project.yml` or from a dependent repo's
// `repository.yml` file. This repo's `repository.yml`
// file still needs to be read.
// * complete: The repo's `repository.yml` file exists and has been
// read.
repos deprepo.RepoMap
// The local repository at the top-level of the project. This repo is
// excluded from most repo operations.
localRepo *repo.Repo
// Required versions of installed repos, as read from `project.yml`.
rootRepoReqs deprepo.RequirementMap
warnings []string
// Indicates the repos whose version we couldn't detect. Prevents
// duplicate warnings.
unknownRepoVers map[string]struct{}
yc ycfg.YCfg
}
func initProject(dir string) error {
var err error
globalProject, err = LoadProject(dir)
if err != nil {
return err
}
if err := globalProject.loadPackageList(); err != nil {
return err
}
return nil
}
func initialize() error {
if globalProject == nil {
wd, err := os.Getwd()
wd = filepath.ToSlash(wd)
if err != nil {
return util.NewNewtError(err.Error())
}
if err := initProject(wd); err != nil {
return err
}
}
return nil
}
func TryGetProject() (*Project, error) {
if err := initialize(); err != nil {
return nil, err
}
return globalProject, nil
}
func GetProject() *Project {
if _, err := TryGetProject(); err != nil {
panic(err.Error())
}
return globalProject
}
func ResetProject() {
globalProject = nil
}
func ResetDeps(newList interfaces.PackageList) interfaces.PackageList {
return nil
if globalProject == nil {
return nil
}
oldList := globalProject.packages
globalProject.packages = newList
if newList == nil {
globalProject.loadPackageList()
}
return oldList
}
func NewProject(dir string) (*Project, error) {
proj := &Project{}
if err := proj.Init(dir); err != nil {
return nil, err
}
return proj, nil
}
func (proj *Project) Path() string {
return proj.BasePath
}
func (proj *Project) Name() string {
return proj.name
}
func (proj *Project) Repos() map[string]*repo.Repo {
return proj.repos
}
func (proj *Project) FindRepo(rname string) *repo.Repo {
if rname == repo.REPO_NAME_LOCAL {
return proj.LocalRepo()
} else {
r, _ := proj.repos[rname]
return r
}
}
func (proj *Project) FindRepoPath(rname string) string {
r := proj.FindRepo(rname)
if r == nil {
return ""
}
return r.Path()
}
func (proj *Project) GetRepoVersion(
rname string) (*newtutil.RepoVersion, error) {
// First, try to read the repo's `version.yml` file.
r := proj.repos[rname]
if r == nil {
return nil, nil
}
ver, err := r.InstalledVersion()
if err != nil {
return nil, err
}
if ver == nil {
commit, err := r.CurrentHash()
if err != nil {
return nil, err
}
if proj.unknownRepoVers == nil {
proj.unknownRepoVers = map[string]struct{}{}
}
if _, ok := proj.unknownRepoVers[rname]; !ok {
proj.unknownRepoVers[rname] = struct{}{}
util.StatusMessage(util.VERBOSITY_QUIET,
"WARNING: Could not detect version of installed repo \"%s\"; "+
"assuming 0.0.0/%s\n", r.Name(), commit)
}
ver = &newtutil.RepoVersion{
Commit: commit,
}
}
return ver, nil
}
// XXX: Incorrect comment.
// Indicates whether the specified repo is present in the `project.state` file.
func (proj *Project) RepoIsInstalled(rname string) bool {
ver, err := proj.GetRepoVersion(rname)
return err == nil && ver != nil
}
func (proj *Project) RepoIsRoot(rname string) bool {
return proj.rootRepoReqs[rname] != nil
}
func (proj *Project) LocalRepo() *repo.Repo {
return proj.localRepo
}
func (proj *Project) Warnings() []string {
return proj.warnings
}
// Selects repositories from the global state that satisfy the specified
// predicate.
func (proj *Project) SelectRepos(pred func(r *repo.Repo) bool) []*repo.Repo {
all := proj.repos.Sorted()
var filtered []*repo.Repo
for _, r := range all {
if pred(r) {
filtered = append(filtered, r)
}
}
return filtered
}
// Installs or upgrades repos matching the specified predicate.
func (proj *Project) InstallIf(
upgrade bool, force bool, ask bool,
predicate func(r *repo.Repo) bool) error {
// Make sure we have an up to date copy of all `repository.yml` files.
if err := proj.downloadRepositoryYmlFiles(); err != nil {
return err
}
// Now that all repos have been successfully fetched, we can finish the
// install procedure locally.
// Determine which repos the user wants to install or upgrade.
specifiedRepoList := proj.SelectRepos(predicate)
inst, err := install.NewInstaller(proj.repos, proj.rootRepoReqs)
if err != nil {
return err
}
if upgrade {
return inst.Upgrade(specifiedRepoList, force, ask)
} else {
return inst.Install(specifiedRepoList, force, ask)
}
}
// Syncs (i.e., applies `git pull` to) repos matching the specified predicate.
func (proj *Project) SyncIf(
force bool, ask bool, predicate func(r *repo.Repo) bool) error {
// Make sure we have an up to date copy of all `repository.yml` files.
if err := proj.downloadRepositoryYmlFiles(); err != nil {
return err
}
// Determine which repos the user wants to sync.
repoList := proj.SelectRepos(predicate)
inst, err := install.NewInstaller(proj.repos, proj.rootRepoReqs)
if err != nil {
return err
}
return inst.Sync(repoList, force, ask)
}
func (proj *Project) InfoIf(predicate func(r *repo.Repo) bool,
remote bool) error {
if remote {
// Make sure we have an up to date copy of all `repository.yml` files.
if err := proj.downloadRepositoryYmlFiles(); err != nil {
return err
}
}
// Determine which repos the user wants info about.
repoList := proj.SelectRepos(predicate)
// Ignore errors. We will deal with bad repos individually when we display
// info about them.
inst, _ := install.NewInstaller(proj.repos, proj.rootRepoReqs)
if err := inst.Info(repoList, remote); err != nil {
return err
}
return nil
}
// Loads a complete repo definition from the appropriate `repository.yml` file.
// The supplied fields form a basic repo description as read from `project.yml`
// or from another repo's dependency list.
//
// @param name The name of the repo to read.
// @param fields Fields containing the basic repo description.
//
// @return *Repo The fully-read repo on success; nil on failure.
// @return error Error on failure.
func (proj *Project) loadRepo(name string, fields map[string]string) (
*repo.Repo, error) {
// First, read the repo description from the supplied fields.
if fields["type"] == "" {
return nil,
util.FmtNewtError("Missing type for repository %s", name)
}
dl, err := downloader.LoadDownloader(name, fields)
if err != nil {
return nil, err
}
// Construct a new repo object from the basic description information.
r, err := repo.NewRepo(name, dl)
if err != nil {
return nil, err
}
for _, ignDir := range ignoreSearchDirs {
r.AddIgnoreDir(ignDir)
}
// Read the full repo definition from its `repository.yml` file.
if err := r.Read(); err != nil {
return r, err
}
// Warn the user about incompatibilities with this version of newt.
ver, err := proj.GetRepoVersion(name)
if err != nil {
return nil, err
}
if ver != nil {
code, msg := r.CheckNewtCompatibility(*ver, newtutil.NewtVersion)
switch code {
case compat.NEWT_COMPAT_GOOD:
case compat.NEWT_COMPAT_WARN:
util.StatusMessage(util.VERBOSITY_QUIET, "WARNING: %s.\n", msg)
case compat.NEWT_COMPAT_ERROR:
return nil, util.NewNewtError(msg)
}
}
// XXX: This log message assumes a "github" type repo.
log.Debugf("Loaded repository %s (type: %s, user: %s, repo: %s)", name,
fields["type"], fields["user"], fields["repo"])
return r, nil
}
func (proj *Project) checkNewtVer() error {
compatSms := proj.yc.GetValStringMapString(
"project.newt_compatibility", nil)
// If this project doesn't have a newt compatibility map, just assume there
// is no incompatibility.
if len(compatSms) == 0 {
return nil
}
tbl, err := compat.ParseNcTable(compatSms)
if err != nil {
return util.FmtNewtError("Error reading project.yml: %s", err.Error())
}
code, msg := tbl.CheckNewtVer(newtutil.NewtVersion)
msg = fmt.Sprintf("This version of newt (%s) is incompatible with "+
"your project; %s", newtutil.NewtVersion.String(), msg)
switch code {
case compat.NEWT_COMPAT_GOOD:
return nil
case compat.NEWT_COMPAT_WARN:
util.StatusMessage(util.VERBOSITY_QUIET, "WARNING: %s.\n", msg)
return nil
case compat.NEWT_COMPAT_ERROR:
return util.NewNewtError(msg)
default:
return nil
}
}
// Loads the `repository.yml` file for each depended-on repo. This
func (proj *Project) loadRepoDeps(download bool) error {
seen := map[string]struct{}{}
loadDeps := func(r *repo.Repo) ([]*repo.Repo, error) {
var newRepos []*repo.Repo
depMap := r.CommitDepMap()
for _, depSlice := range depMap {
for _, dep := range depSlice {
if _, ok := seen[dep.Name]; !ok {
seen[r.Name()] = struct{}{}
depRepo := proj.repos[dep.Name]
if depRepo == nil {
depRepo, _ = proj.loadRepo(dep.Name, dep.Fields)
proj.repos[dep.Name] = depRepo
}
newRepos = append(newRepos, depRepo)
if download {
if _, err := depRepo.UpdateDesc(); err != nil {
return nil, err
}
}
}
}
}
return newRepos, nil
}
curRepos := proj.repos.Sorted()
for len(curRepos) > 0 {
var nextRepos []*repo.Repo
for _, r := range curRepos {
depRepos, err := loadDeps(r)
if err != nil {
return err
}
nextRepos = append(nextRepos, depRepos...)
}
curRepos = nextRepos
}
return nil
}
func (proj *Project) downloadRepositoryYmlFiles() error {
// Download the `repository.yml` file for each root-level repo (those
// specified in the `project.yml` file).
for _, r := range proj.repos.Sorted() {
if !r.IsLocal() {
if _, err := r.UpdateDesc(); err != nil {
return err
}
}
}
// Download the `repository.yml` file for each depended-on repo.
if err := proj.loadRepoDeps(true); err != nil {
return err
}
return nil
}
func (proj *Project) loadConfig() error {
yc, err := newtutil.ReadConfig(proj.BasePath,
strings.TrimSuffix(PROJECT_FILE_NAME, ".yml"))
if err != nil {
return util.NewNewtError(err.Error())
}
// Store configuration object for access to future values,
// this avoids keeping every string around as a project variable when
// we need to process it later.
proj.yc = yc
proj.name = yc.GetValString("project.name", nil)
// Local repository always included in initialization
r, err := repo.NewLocalRepo(proj.name)
if err != nil {
return err
}
proj.repos[proj.name] = r
proj.localRepo = r
for _, ignDir := range ignoreSearchDirs {
r.AddIgnoreDir(ignDir)
}
// Assume every item starting with "repository." is a repository descriptor
// and try to load it.
for k, _ := range yc.AllSettings() {
repoName := strings.TrimPrefix(k, "repository.")
if repoName != k {
fields := yc.GetValStringMapString(k, nil)
r, _ := proj.loadRepo(repoName, fields)
verReqs, err := newtutil.ParseRepoVersionReqs(fields["vers"])
if err != nil {
return util.FmtNewtError(
"Repo \"%s\" contains invalid version requirement: %s (%s)",
repoName, fields["vers"], err.Error())
}
proj.repos[repoName] = r
proj.rootRepoReqs[repoName] = verReqs
}
}
// Read `repository.yml` files belonging to dependee repos from disk.
// These repos might not be specified in the `project.yml` file, but they
// are still part of the project.
if err := proj.loadRepoDeps(false); err != nil {
return err
}
ignoreDirs := yc.GetValStringSlice("project.ignore_dirs", nil)
for _, ignDir := range ignoreDirs {
repoName, dirName, err := newtutil.ParsePackageString(ignDir)
if err != nil {
return err
}
if repoName == "" {
r = proj.LocalRepo()
} else {
r = proj.FindRepo(repoName)
}
if r == nil {
return util.NewNewtError(
fmt.Sprintf("ignore_dirs: unknown repo %s", repoName))
}
r.AddIgnoreDir(dirName)
}
if err := proj.checkNewtVer(); err != nil {
return err
}
return nil
}
func (proj *Project) Init(dir string) error {
proj.BasePath = filepath.ToSlash(filepath.Clean(dir))
// Only one project per system, when created, set it as the global project
interfaces.SetProject(proj)
proj.repos = map[string]*repo.Repo{}
proj.rootRepoReqs = map[string][]newtutil.RepoVersionReq{}
// Load Project configuration
if err := proj.loadConfig(); err != nil {
return err
}
return nil
}
func (proj *Project) ResolveDependency(dep interfaces.DependencyInterface) interfaces.PackageInterface {
type NamePath struct {
name string
path string
}
for _, pkgList := range proj.packages {
for _, pkg := range *pkgList {
if dep.SatisfiesDependency(pkg) {
return pkg
}
}
}
return nil
}
func (proj *Project) ResolvePackage(
dfltRepo interfaces.RepoInterface, name string) (*pkg.LocalPackage, error) {
// Trim trailing slash from name. This is necessary when tab
// completion is used to specify the name.
name = strings.TrimSuffix(name, "/")
repoName, pkgName, err := newtutil.ParsePackageString(name)
if err != nil {
return nil, util.FmtNewtError("invalid package name: %s (%s)", name,
err.Error())
}
var repo interfaces.RepoInterface
if repoName == "" {
repo = dfltRepo
} else if proj.repos[repoName] != nil {
repo = proj.repos[repoName]
} else {
return nil, util.FmtNewtError("invalid package name: %s (unkwn repo %s)",
name, repoName)
}
dep, err := pkg.NewDependency(repo, pkgName)
if err != nil {
return nil, util.FmtNewtError("invalid package name: %s (%s)", name,
err.Error())
}
if dep == nil {
return nil, util.NewNewtError("invalid package name: " + name)
}
pack := proj.ResolveDependency(dep)
if pack == nil {
return nil, util.NewNewtError("unknown package: " + name)
}
return pack.(*pkg.LocalPackage), nil
}
// Resolves a path with an optional repo prefix (e.g., "@apache-mynewt-core").
func (proj *Project) ResolvePath(
basePath string, name string) (string, error) {
repoName, subPath, err := newtutil.ParsePackageString(name)
if err != nil {
return "", util.FmtNewtError("invalid path: %s (%s)", name,
err.Error())
}
if repoName == "" {
return basePath + "/" + subPath, nil
} else {
repo := proj.repos[repoName]
if repo == nil {
return "", util.FmtNewtError("Unknown repository: %s", repoName)
}
return repo.Path() + "/" + subPath, nil
}
}
func findProjectDir(dir string) (string, error) {
for {
projFile := path.Clean(dir) + "/" + PROJECT_FILE_NAME
log.Debugf("Searching for project file %s", projFile)
if util.NodeExist(projFile) {
break
}
// Move back one directory and continue searching
dir = path.Clean(dir + "../../")
// path.Clean returns . if processing results in empty string.
// Need to check for . on Windows.
if dir == "/" || dir == "." {
return "", util.NewNewtError("No project file found!")
}
}
return dir, nil
}
func (proj *Project) loadPackageList() error {
proj.packages = interfaces.PackageList{}
// Go through a list of repositories, starting with local, and search for
// packages / store them in the project package list.
repos := proj.Repos()
for name, repo := range repos {
list, warnings, err := pkg.ReadLocalPackages(repo, repo.Path())
if err == nil {
proj.packages[name] = list
}
proj.warnings = append(proj.warnings, warnings...)
}
return nil
}
func (proj *Project) PackageList() interfaces.PackageList {
return proj.packages
}
func (proj *Project) PackagesOfType(pkgType interfaces.PackageType) []interfaces.PackageInterface {
matches := []interfaces.PackageInterface{}
packs := proj.PackageList()
for _, packHash := range packs {
for _, pack := range *packHash {
if pkgType == -1 || pack.Type() == pkgType {
matches = append(matches, pack)
}
}
}
return matches
}
func LoadProject(dir string) (*Project, error) {
projDir, err := findProjectDir(dir)
if err != nil {
return nil, err
}
proj, err := NewProject(projDir)
return proj, err
}