diff options
author | Dan Willemsen <dwillemsen@google.com> | 2015-09-23 15:26:20 -0700 |
---|---|---|
committer | Dan Willemsen <dwillemsen@google.com> | 2015-12-09 14:29:12 -0800 |
commit | 34cc69e4bf36cb65bb181b42ccb0f8c792a32cfb (patch) | |
tree | ecaff919037e29033fa9b3bbfb33baa8e93196cf /common | |
parent | fafa3dc7e267675610a34b0c335be350eb30936d (diff) | |
download | build_soong-34cc69e4bf36cb65bb181b42ccb0f8c792a32cfb.tar.gz build_soong-34cc69e4bf36cb65bb181b42ccb0f8c792a32cfb.tar.bz2 build_soong-34cc69e4bf36cb65bb181b42ccb0f8c792a32cfb.zip |
Use `Path` instead of string for file paths
This centralizes verification and common operations, like converting the
path to a source file to the path for a built object.
It also embeds the configuration knowledge into the path, so that we can
remove "${SrcDir}/path" from the ninja file. When SrcDir is '.', that
leads to paths like './path' instead of just 'path' like make is doing,
causing differences in compiled binaries.
Change-Id: Ib4e8910a6e867ce1b7b420d927c04f1142a7589e
Diffstat (limited to 'common')
-rw-r--r-- | common/androidmk.go | 48 | ||||
-rw-r--r-- | common/config.go | 73 | ||||
-rw-r--r-- | common/defs.go | 4 | ||||
-rw-r--r-- | common/env.go | 11 | ||||
-rw-r--r-- | common/module.go | 135 | ||||
-rw-r--r-- | common/package_ctx.go | 127 | ||||
-rw-r--r-- | common/paths.go | 600 | ||||
-rw-r--r-- | common/paths_test.go | 167 |
8 files changed, 956 insertions, 209 deletions
diff --git a/common/androidmk.go b/common/androidmk.go index 5dd422d1..06aa30c5 100644 --- a/common/androidmk.go +++ b/common/androidmk.go @@ -37,11 +37,11 @@ type AndroidMkDataProvider interface { type AndroidMkData struct { Class string - OutputFile string + OutputFile OptionalPath Custom func(w io.Writer, name, prefix string) - Extra func(name, prefix, outputFile string, arch Arch) []string + Extra func(name, prefix string, outputFile Path, arch Arch) []string } func AndroidMkSingleton() blueprint.Singleton { @@ -55,7 +55,7 @@ func (c *androidMkSingleton) GenerateBuildActions(ctx blueprint.SingletonContext hasBPDir := make(map[string]bool) bpDirs := []string{} - ctx.SetNinjaBuildDir(pctx, filepath.Join(ctx.Config().(Config).BuildDir(), "..")) + ctx.SetNinjaBuildDir(pctx, filepath.Join(ctx.Config().(Config).buildDir, "..")) ctx.VisitAllModules(func(module blueprint.Module) { if _, ok := module.(AndroidModule); ok { @@ -72,28 +72,13 @@ func (c *androidMkSingleton) GenerateBuildActions(ctx blueprint.SingletonContext // Gather list of eligible Android modules for translation androidMkModules := make(map[blueprint.Module]bool) - srcDir := ctx.Config().(Config).SrcDir() - intermediatesDir := filepath.Join(ctx.Config().(Config).IntermediatesDir(), "androidmk") sort.Strings(bpDirs) for _, bpDir := range bpDirs { - mkFile := filepath.Join(srcDir, bpDir, "Android.mk") - - files, err := Glob(ctx, intermediatesDir, mkFile, nil) - if err != nil { - ctx.Errorf("glob: %s", err.Error()) - continue - } - - // Existing Android.mk file, use that instead - if len(files) > 0 { - for _, file := range files { - ctx.AddNinjaFileDeps(file) + mkFile := OptionalPathForSource(ctx, "androidmk", bpDir, "Android.mk") + if !mkFile.Valid() { + for _, mod := range dirModules[bpDir] { + androidMkModules[mod] = true } - continue - } - - for _, mod := range dirModules[bpDir] { - androidMkModules[mod] = true } } @@ -110,16 +95,19 @@ func (c *androidMkSingleton) GenerateBuildActions(ctx blueprint.SingletonContext } } - transMk := filepath.Join(ctx.Config().(Config).BuildDir(), "Android.mk") + transMk := PathForOutput(ctx, "Android.mk") + if ctx.Failed() { + return + } - err := translateAndroidMk(ctx, transMk, androidMkModulesList) + err := translateAndroidMk(ctx, transMk.String(), androidMkModulesList) if err != nil { ctx.Errorf(err.Error()) } ctx.Build(pctx, blueprint.BuildParams{ Rule: blueprint.Phony, - Outputs: []string{transMk}, + Outputs: []string{transMk.String()}, Optional: true, }) } @@ -177,7 +165,7 @@ func translateAndroidMkModule(ctx blueprint.SingletonContext, w io.Writer, mod b type archSrc struct { arch Arch - src string + src Path extra []string } @@ -211,6 +199,10 @@ func translateAndroidMkModule(ctx blueprint.SingletonContext, w io.Writer, mod b return } + if !data.OutputFile.Valid() { + return + } + hC := hostClass{ host: amod.HostOrDevice() == Host, class: data.Class, @@ -219,7 +211,7 @@ func translateAndroidMkModule(ctx blueprint.SingletonContext, w io.Writer, mod b src := archSrc{ arch: arch, - src: data.OutputFile, + src: data.OutputFile.Path(), } if data.Extra != nil { @@ -242,7 +234,7 @@ func translateAndroidMkModule(ctx blueprint.SingletonContext, w io.Writer, mod b printed := make(map[string]bool) for _, src := range archSrcs { - io.WriteString(w, "LOCAL_SRC_FILES_"+src.arch.ArchType.String()+" := "+src.src+"\n") + io.WriteString(w, "LOCAL_SRC_FILES_"+src.arch.ArchType.String()+" := "+src.src.String()+"\n") for _, extra := range src.extra { if !printed[extra] { diff --git a/common/config.go b/common/config.go index c67023e3..7f6ee65d 100644 --- a/common/config.go +++ b/common/config.go @@ -20,6 +20,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "sync" ) @@ -38,8 +39,6 @@ func (f *FileConfigurableOptions) SetDefaultConfig() { type Config struct { *config - - dontCreateNinjaFile bool } // A config object represents the entire build configuration for Android. @@ -142,8 +141,24 @@ func NewConfig(srcDir, buildDir string) (Config, error) { }, } + // Sanity check the build and source directories. This won't catch strange + // configurations with symlinks, but at least checks the obvious cases. + absBuildDir, err := filepath.Abs(buildDir) + if err != nil { + return Config{}, err + } + + absSrcDir, err := filepath.Abs(srcDir) + if err != nil { + return Config{}, err + } + + if strings.HasPrefix(absSrcDir, absBuildDir) { + return Config{}, fmt.Errorf("Build dir must not contain source directory") + } + // Load any configurable options from the configuration file - err := loadConfig(config.config) + err = loadConfig(config.config) if err != nil { return Config{}, err } @@ -159,18 +174,6 @@ func NewConfig(srcDir, buildDir string) (Config, error) { return config, nil } -func (c *config) SrcDir() string { - return c.srcDir -} - -func (c *config) BuildDir() string { - return c.buildDir -} - -func (c *config) IntermediatesDir() string { - return filepath.Join(c.BuildDir(), ".intermediates") -} - func (c *config) RemoveAbandonedFiles() bool { return false } @@ -238,37 +241,7 @@ func (c *config) DeviceUsesClang() bool { return false } -// DeviceOut returns the path to out directory for device targets -func (c *config) DeviceOut() string { - return filepath.Join(c.BuildDir(), "target/product", c.DeviceName()) -} - -// HostOut returns the path to out directory for host targets -func (c *config) HostOut() string { - return filepath.Join(c.BuildDir(), "host", c.PrebuiltOS()) -} - -// HostBin returns the path to bin directory for host targets -func (c *config) HostBin() string { - return filepath.Join(c.HostOut(), "bin") -} - -// HostBinTool returns the path to a host tool in the bin directory for host targets -func (c *config) HostBinTool(tool string) (string, error) { - return filepath.Join(c.HostBin(), tool), nil -} - -// HostJavaDir returns the path to framework directory for host targets -func (c *config) HostJavaDir() string { - return filepath.Join(c.HostOut(), "framework") -} - -// HostJavaTool returns the path to a host tool in the frameworks directory for host targets -func (c *config) HostJavaTool(tool string) (string, error) { - return filepath.Join(c.HostJavaDir(), tool), nil -} - -func (c *config) ResourceOverlays() []string { +func (c *config) ResourceOverlays() []SourcePath { return nil } @@ -296,10 +269,10 @@ func (c *config) ProductAaptCharacteristics() string { return "nosdcard" } -func (c *config) DefaultAppCertificateDir() string { - return filepath.Join(c.SrcDir(), "build/target/product/security") +func (c *config) DefaultAppCertificateDir(ctx PathContext) SourcePath { + return PathForSource(ctx, "build/target/product/security") } -func (c *config) DefaultAppCertificate() string { - return filepath.Join(c.DefaultAppCertificateDir(), "testkey") +func (c *config) DefaultAppCertificate(ctx PathContext) SourcePath { + return c.DefaultAppCertificateDir(ctx).Join(ctx, "testkey") } diff --git a/common/defs.go b/common/defs.go index 7b2a7067..9e185e44 100644 --- a/common/defs.go +++ b/common/defs.go @@ -20,13 +20,11 @@ import ( ) var ( - pctx = blueprint.NewPackageContext("android/soong/common") + pctx = NewPackageContext("android/soong/common") cpPreserveSymlinks = pctx.VariableConfigMethod("cpPreserveSymlinks", Config.CpPreserveSymlinksFlags) - srcDir = pctx.VariableConfigMethod("srcDir", Config.SrcDir) - // A phony rule that is not the built-in Ninja phony rule. The built-in // phony rule has special behavior that is sometimes not desired. See the // Ninja docs for more details. diff --git a/common/env.go b/common/env.go index 8694c28a..478fffce 100644 --- a/common/env.go +++ b/common/env.go @@ -15,8 +15,6 @@ package common import ( - "path/filepath" - "android/soong" "android/soong/env" @@ -43,12 +41,15 @@ type envSingleton struct{} func (c *envSingleton) GenerateBuildActions(ctx blueprint.SingletonContext) { envDeps := ctx.Config().(Config).EnvDeps() - envFile := filepath.Join(ctx.Config().(Config).BuildDir(), ".soong.environment") + envFile := PathForOutput(ctx, ".soong.environment") + if ctx.Failed() { + return + } - err := env.WriteEnvFile(envFile, envDeps) + err := env.WriteEnvFile(envFile.String(), envDeps) if err != nil { ctx.Errorf(err.Error()) } - ctx.AddNinjaFileDeps(envFile) + ctx.AddNinjaFileDeps(envFile.String()) } diff --git a/common/module.go b/common/module.go index 113768a7..36710c5a 100644 --- a/common/module.go +++ b/common/module.go @@ -32,6 +32,19 @@ var ( HostExecutable = "host_executable" ) +type ModuleBuildParams struct { + Rule blueprint.Rule + Output WritablePath + Outputs WritablePaths + Input Path + Inputs Paths + Implicit Path + Implicits Paths + OrderOnly Paths + Default bool + Args map[string]string +} + type androidBaseContext interface { Arch() Arch HostOrDevice() HostOrDevice @@ -52,12 +65,16 @@ type AndroidModuleContext interface { blueprint.ModuleContext androidBaseContext - ExpandSources(srcFiles, excludes []string) []string - Glob(outDir, globPattern string, excludes []string) []string + // Similar to Build, but takes Paths instead of []string, + // and performs more verification. + ModuleBuild(pctx blueprint.PackageContext, params ModuleBuildParams) + + ExpandSources(srcFiles, excludes []string) Paths + Glob(outDir, globPattern string, excludes []string) Paths - InstallFile(installPath, srcPath string, deps ...string) string - InstallFileName(installPath, name, srcPath string, deps ...string) string - CheckbuildFile(srcPath string) + InstallFile(installPath string, srcPath Path, deps ...Path) Path + InstallFileName(installPath, name string, srcPath Path, deps ...Path) Path + CheckbuildFile(srcPath Path) } type AndroidModule interface { @@ -196,8 +213,8 @@ type AndroidModuleBase struct { archProperties []*archProperties noAddressSanitizer bool - installFiles []string - checkbuildFiles []string + installFiles Paths + checkbuildFiles Paths // Used by buildTargetSingleton to create checkbuild and per-directory build targets // Only set on the final variant of each module @@ -254,9 +271,9 @@ func (a *AndroidModuleBase) Enabled() bool { } func (a *AndroidModuleBase) computeInstallDeps( - ctx blueprint.ModuleContext) []string { + ctx blueprint.ModuleContext) Paths { - result := []string{} + result := Paths{} ctx.VisitDepsDepthFirstIf(isFileInstaller, func(m blueprint.Module) { fileInstaller := m.(fileInstaller) @@ -267,7 +284,7 @@ func (a *AndroidModuleBase) computeInstallDeps( return result } -func (a *AndroidModuleBase) filesToInstall() []string { +func (a *AndroidModuleBase) filesToInstall() Paths { return a.installFiles } @@ -280,8 +297,8 @@ func (a *AndroidModuleBase) generateModuleTarget(ctx blueprint.ModuleContext) { return } - allInstalledFiles := []string{} - allCheckbuildFiles := []string{} + allInstalledFiles := Paths{} + allCheckbuildFiles := Paths{} ctx.VisitAllModuleVariants(func(module blueprint.Module) { a := module.(AndroidModule).base() allInstalledFiles = append(allInstalledFiles, a.installFiles...) @@ -295,7 +312,7 @@ func (a *AndroidModuleBase) generateModuleTarget(ctx blueprint.ModuleContext) { ctx.Build(pctx, blueprint.BuildParams{ Rule: blueprint.Phony, Outputs: []string{name}, - Implicits: allInstalledFiles, + Implicits: allInstalledFiles.Strings(), }) deps = append(deps, name) a.installTarget = name @@ -306,7 +323,7 @@ func (a *AndroidModuleBase) generateModuleTarget(ctx blueprint.ModuleContext) { ctx.Build(pctx, blueprint.BuildParams{ Rule: blueprint.Phony, Outputs: []string{name}, - Implicits: allCheckbuildFiles, + Implicits: allCheckbuildFiles.Strings(), Optional: true, }) deps = append(deps, name) @@ -371,9 +388,9 @@ type androidBaseContextImpl struct { type androidModuleContext struct { blueprint.ModuleContext androidBaseContextImpl - installDeps []string - installFiles []string - checkbuildFiles []string + installDeps Paths + installFiles Paths + checkbuildFiles Paths } func (a *androidModuleContext) Build(pctx blueprint.PackageContext, params blueprint.BuildParams) { @@ -381,6 +398,30 @@ func (a *androidModuleContext) Build(pctx blueprint.PackageContext, params bluep a.ModuleContext.Build(pctx, params) } +func (a *androidModuleContext) ModuleBuild(pctx blueprint.PackageContext, params ModuleBuildParams) { + bparams := blueprint.BuildParams{ + Rule: params.Rule, + Outputs: params.Outputs.Strings(), + Inputs: params.Inputs.Strings(), + Implicits: params.Implicits.Strings(), + OrderOnly: params.OrderOnly.Strings(), + Args: params.Args, + Optional: !params.Default, + } + + if params.Output != nil { + bparams.Outputs = append(bparams.Outputs, params.Output.String()) + } + if params.Input != nil { + bparams.Inputs = append(bparams.Inputs, params.Input.String()) + } + if params.Implicit != nil { + bparams.Implicits = append(bparams.Implicits, params.Implicit.String()) + } + + a.ModuleContext.Build(pctx, bparams) +} + func (a *androidBaseContextImpl) Arch() Arch { return a.arch } @@ -413,31 +454,19 @@ func (a *androidBaseContextImpl) AConfig() Config { return a.config } -func (a *androidModuleContext) InstallFileName(installPath, name, srcPath string, - deps ...string) string { +func (a *androidModuleContext) InstallFileName(installPath, name string, srcPath Path, + deps ...Path) Path { - config := a.AConfig() - var fullInstallPath string - if a.hod.Device() { - // TODO: replace unset with a device name once we have device targeting - fullInstallPath = filepath.Join(config.DeviceOut(), "system", - installPath, name) - } else { - // TODO - if a.ht == Windows { - fullInstallPath = filepath.Join(config.BuildDir(), "host", "windows-x86", installPath, name) - } else { - fullInstallPath = filepath.Join(config.HostOut(), installPath, name) - } - } + fullInstallPath := PathForModuleInstall(a, installPath, name) deps = append(deps, a.installDeps...) - a.ModuleContext.Build(pctx, blueprint.BuildParams{ + a.ModuleBuild(pctx, ModuleBuildParams{ Rule: Cp, - Outputs: []string{fullInstallPath}, - Inputs: []string{srcPath}, - OrderOnly: deps, + Output: fullInstallPath, + Input: srcPath, + OrderOnly: Paths(deps), + Default: true, }) a.installFiles = append(a.installFiles, fullInstallPath) @@ -445,16 +474,16 @@ func (a *androidModuleContext) InstallFileName(installPath, name, srcPath string return fullInstallPath } -func (a *androidModuleContext) InstallFile(installPath, srcPath string, deps ...string) string { - return a.InstallFileName(installPath, filepath.Base(srcPath), srcPath, deps...) +func (a *androidModuleContext) InstallFile(installPath string, srcPath Path, deps ...Path) Path { + return a.InstallFileName(installPath, filepath.Base(srcPath.String()), srcPath, deps...) } -func (a *androidModuleContext) CheckbuildFile(srcPath string) { +func (a *androidModuleContext) CheckbuildFile(srcPath Path) { a.checkbuildFiles = append(a.checkbuildFiles, srcPath) } type fileInstaller interface { - filesToInstall() []string + filesToInstall() Paths } func isFileInstaller(m blueprint.Module) bool { @@ -476,8 +505,8 @@ func findStringInSlice(str string, slice []string) int { return -1 } -func (ctx *androidModuleContext) ExpandSources(srcFiles, excludes []string) []string { - prefix := ModuleSrcDir(ctx) +func (ctx *androidModuleContext) ExpandSources(srcFiles, excludes []string) Paths { + prefix := PathForModuleSrc(ctx).String() for i, e := range excludes { j := findStringInSlice(e, srcFiles) if j != -1 { @@ -487,32 +516,24 @@ func (ctx *androidModuleContext) ExpandSources(srcFiles, excludes []string) []st excludes[i] = filepath.Join(prefix, e) } - for i, srcFile := range srcFiles { - srcFiles[i] = filepath.Join(prefix, srcFile) - } - - if !hasGlob(srcFiles) { - return srcFiles - } - - globbedSrcFiles := make([]string, 0, len(srcFiles)) + globbedSrcFiles := make(Paths, 0, len(srcFiles)) for _, s := range srcFiles { if glob.IsGlob(s) { - globbedSrcFiles = append(globbedSrcFiles, ctx.Glob("src_glob", s, excludes)...) + globbedSrcFiles = append(globbedSrcFiles, ctx.Glob("src_glob", filepath.Join(prefix, s), excludes)...) } else { - globbedSrcFiles = append(globbedSrcFiles, s) + globbedSrcFiles = append(globbedSrcFiles, PathForModuleSrc(ctx, s)) } } return globbedSrcFiles } -func (ctx *androidModuleContext) Glob(outDir, globPattern string, excludes []string) []string { - ret, err := Glob(ctx, filepath.Join(ModuleOutDir(ctx), outDir), globPattern, excludes) +func (ctx *androidModuleContext) Glob(outDir, globPattern string, excludes []string) Paths { + ret, err := Glob(ctx, PathForModuleOut(ctx, outDir).String(), globPattern, excludes) if err != nil { ctx.ModuleErrorf("glob: %s", err.Error()) } - return ret + return pathsForModuleSrcFromFullPath(ctx, ret) } func init() { diff --git a/common/package_ctx.go b/common/package_ctx.go new file mode 100644 index 00000000..cd18b658 --- /dev/null +++ b/common/package_ctx.go @@ -0,0 +1,127 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Licensed 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 common + +import ( + "fmt" + + "github.com/google/blueprint" +) + +// AndroidPackageContext is a wrapper for blueprint.PackageContext that adds +// some android-specific helper functions. +type AndroidPackageContext struct { + blueprint.PackageContext +} + +func NewPackageContext(pkgPath string) AndroidPackageContext { + return AndroidPackageContext{blueprint.NewPackageContext(pkgPath)} +} + +// configErrorWrapper can be used with Path functions when a Context is not +// available. A Config can be provided, and errors are stored as a list for +// later retrieval. +// +// The most common use here will be with VariableFunc, where only a config is +// provided, and an error should be returned. +type configErrorWrapper struct { + config Config + errors []error +} + +var _ PathContext = &configErrorWrapper{} +var _ errorfContext = &configErrorWrapper{} + +func (e *configErrorWrapper) Config() interface{} { + return e.config +} +func (e *configErrorWrapper) Errorf(format string, args ...interface{}) { + e.errors = append(e.errors, fmt.Errorf(format, args...)) +} + +// SourcePathVariable returns a Variable whose value is the source directory +// appended with the supplied path. It may only be called during a Go package's +// initialization - either from the init() function or as part of a +// package-scoped variable's initialization. +func (p AndroidPackageContext) SourcePathVariable(name, path string) blueprint.Variable { + return p.VariableFunc(name, func(config interface{}) (string, error) { + ctx := &configErrorWrapper{config.(Config), []error{}} + p := safePathForSource(ctx, path) + if len(ctx.errors) > 0 { + return "", ctx.errors[0] + } + return p.String(), nil + }) +} + +// HostBinVariable returns a Variable whose value is the path to a host tool +// in the bin directory for host targets. It may only be called during a Go +// package's initialization - either from the init() function or as part of a +// package-scoped variable's initialization. +func (p AndroidPackageContext) HostBinToolVariable(name, path string) blueprint.Variable { + return p.VariableFunc(name, func(config interface{}) (string, error) { + ctx := &configErrorWrapper{config.(Config), []error{}} + p := PathForOutput(ctx, "host", ctx.config.PrebuiltOS(), "bin", path) + if len(ctx.errors) > 0 { + return "", ctx.errors[0] + } + return p.String(), nil + }) +} + +// HostJavaToolVariable returns a Variable whose value is the path to a host +// tool in the frameworks directory for host targets. It may only be called +// during a Go package's initialization - either from the init() function or as +// part of a package-scoped variable's initialization. +func (p AndroidPackageContext) HostJavaToolVariable(name, path string) blueprint.Variable { + return p.VariableFunc(name, func(config interface{}) (string, error) { + ctx := &configErrorWrapper{config.(Config), []error{}} + p := PathForOutput(ctx, "host", ctx.config.PrebuiltOS(), "framework", path) + if len(ctx.errors) > 0 { + return "", ctx.errors[0] + } + return p.String(), nil + }) +} + +// IntermediatesPathVariable returns a Variable whose value is the intermediate +// directory appended with the supplied path. It may only be called during a Go +// package's initialization - either from the init() function or as part of a +// package-scoped variable's initialization. +func (p AndroidPackageContext) IntermediatesPathVariable(name, path string) blueprint.Variable { + return p.VariableFunc(name, func(config interface{}) (string, error) { + ctx := &configErrorWrapper{config.(Config), []error{}} + p := PathForIntermediates(ctx, path) + if len(ctx.errors) > 0 { + return "", ctx.errors[0] + } + return p.String(), nil + }) +} + +// PrefixedPathsForSourceVariable returns a Variable whose value is the +// list of source paths prefixed with the supplied prefix. It may only be +// called during a Go package's initialization - either from the init() +// function or as part of a package-scoped variable's initialization. +func (p AndroidPackageContext) PrefixedPathsForSourceVariable(name, prefix string, paths []string) blueprint.Variable { + return p.VariableFunc(name, func(config interface{}) (string, error) { + ctx := &configErrorWrapper{config.(Config), []error{}} + paths := PathsForSource(ctx, paths) + if len(ctx.errors) > 0 { + return "", ctx.errors[0] + } + return JoinWithPrefix(paths.Strings(), prefix), nil + }) +} diff --git a/common/paths.go b/common/paths.go index d92dcf91..8a085ea1 100644 --- a/common/paths.go +++ b/common/paths.go @@ -18,103 +18,571 @@ import ( "fmt" "os" "path/filepath" + "reflect" + "strings" + + "github.com/google/blueprint" + "github.com/google/blueprint/pathtools" ) -// ModuleOutDir returns the path to the module-specific output directory. -func ModuleOutDir(ctx AndroidModuleContext) string { - return filepath.Join(ctx.AConfig().IntermediatesDir(), - ctx.ModuleDir(), ctx.ModuleName(), ctx.ModuleSubDir()) +// PathContext is the subset of a (Module|Singleton)Context required by the +// Path methods. +type PathContext interface { + Config() interface{} } -// ModuleSrcDir returns the path of the directory that all source file paths are -// specified relative to. -func ModuleSrcDir(ctx AndroidModuleContext) string { - return filepath.Join(ctx.AConfig().SrcDir(), ctx.ModuleDir()) +var _ PathContext = blueprint.SingletonContext(nil) +var _ PathContext = blueprint.ModuleContext(nil) + +// errorfContext is the interface containing the Errorf method matching the +// Errorf method in blueprint.SingletonContext. +type errorfContext interface { + Errorf(format string, args ...interface{}) } -// ModuleBinDir returns the path to the module- and architecture-specific binary -// output directory. -func ModuleBinDir(ctx AndroidModuleContext) string { - return filepath.Join(ModuleOutDir(ctx), "bin") +var _ errorfContext = blueprint.SingletonContext(nil) + +// moduleErrorf is the interface containing the ModuleErrorf method matching +// the ModuleErrorf method in blueprint.ModuleContext. +type moduleErrorf interface { + ModuleErrorf(format string, args ...interface{}) +} + +var _ moduleErrorf = blueprint.ModuleContext(nil) + +// pathConfig returns the android Config interface associated to the context. +// Panics if the context isn't affiliated with an android build. +func pathConfig(ctx PathContext) Config { + if ret, ok := ctx.Config().(Config); ok { + return ret + } + panic("Paths may only be used on Soong builds") +} + +// reportPathError will register an error with the attached context. It +// attempts ctx.ModuleErrorf for a better error message first, then falls +// back to ctx.Errorf. +func reportPathError(ctx PathContext, format string, args ...interface{}) { + if mctx, ok := ctx.(moduleErrorf); ok { + mctx.ModuleErrorf(format, args...) + } else if ectx, ok := ctx.(errorfContext); ok { + ectx.Errorf(format, args...) + } else { + panic(fmt.Sprintf(format, args...)) + } } -// ModuleLibDir returns the path to the module- and architecture-specific -// library output directory. -func ModuleLibDir(ctx AndroidModuleContext) string { - return filepath.Join(ModuleOutDir(ctx), "lib") +type Path interface { + // Returns the path in string form + String() string + + // Returns the current file extension of the path + Ext() string } -// ModuleGenDir returns the module directory for generated files -// path. -func ModuleGenDir(ctx AndroidModuleContext) string { - return filepath.Join(ModuleOutDir(ctx), "gen") +// WritablePath is a type of path that can be used as an output for build rules. +type WritablePath interface { + Path + + writablePath() } -// ModuleObjDir returns the module- and architecture-specific object directory -// path. -func ModuleObjDir(ctx AndroidModuleContext) string { - return filepath.Join(ModuleOutDir(ctx), "obj") +type genPathProvider interface { + genPathWithExt(ctx AndroidModuleContext, ext string) ModuleGenPath +} +type objPathProvider interface { + objPathWithExt(ctx AndroidModuleContext, subdir, ext string) ModuleObjPath +} +type resPathProvider interface { + resPathWithName(ctx AndroidModuleContext, name string) ModuleResPath } -// ModuleGoPackageDir returns the module-specific package root directory path. -// This directory is where the final package .a files are output and where -// dependent modules search for this package via -I arguments. -func ModuleGoPackageDir(ctx AndroidModuleContext) string { - return filepath.Join(ModuleOutDir(ctx), "pkg") +// GenPathWithExt derives a new file path in ctx's generated sources directory +// from the current path, but with the new extension. +func GenPathWithExt(ctx AndroidModuleContext, p Path, ext string) ModuleGenPath { + if path, ok := p.(genPathProvider); ok { + return path.genPathWithExt(ctx, ext) + } + reportPathError(ctx, "Tried to create generated file from unsupported path: %s(%s)", reflect.TypeOf(p).Name(), p) + return PathForModuleGen(ctx) } -// ModuleIncludeDir returns the module-specific public include directory path. -func ModuleIncludeDir(ctx AndroidModuleContext) string { - return filepath.Join(ModuleOutDir(ctx), "include") +// ObjPathWithExt derives a new file path in ctx's object directory from the +// current path, but with the new extension. +func ObjPathWithExt(ctx AndroidModuleContext, p Path, subdir, ext string) ModuleObjPath { + if path, ok := p.(objPathProvider); ok { + return path.objPathWithExt(ctx, subdir, ext) + } + reportPathError(ctx, "Tried to create object file from unsupported path: %s (%s)", reflect.TypeOf(p).Name(), p) + return PathForModuleObj(ctx) } -// ModuleProtoDir returns the module-specific public proto include directory path. -func ModuleProtoDir(ctx AndroidModuleContext) string { - return filepath.Join(ModuleOutDir(ctx), "proto") +// ResPathWithName derives a new path in ctx's output resource directory, using +// the current path to create the directory name, and the `name` argument for +// the filename. +func ResPathWithName(ctx AndroidModuleContext, p Path, name string) ModuleResPath { + if path, ok := p.(resPathProvider); ok { + return path.resPathWithName(ctx, name) + } + reportPathError(ctx, "Tried to create object file from unsupported path: %s (%s)", reflect.TypeOf(p).Name(), p) + return PathForModuleRes(ctx) } -func ModuleJSCompiledDir(ctx AndroidModuleContext) string { - return filepath.Join(ModuleOutDir(ctx), "js") +// OptionalPath is a container that may or may not contain a valid Path. +type OptionalPath struct { + valid bool + path Path } -// CheckModuleSrcDirsExist logs an error on a property if any of the directories relative to the -// Blueprints file don't exist. -func CheckModuleSrcDirsExist(ctx AndroidModuleContext, dirs []string, prop string) { - for _, dir := range dirs { - fullDir := filepath.Join(ModuleSrcDir(ctx), dir) - if _, err := os.Stat(fullDir); err != nil { - if os.IsNotExist(err) { - ctx.PropertyErrorf(prop, "module source directory %q does not exist", dir) - } else { - ctx.PropertyErrorf(prop, "%s", err.Error()) - } +// OptionalPathForPath returns an OptionalPath containing the path. +func OptionalPathForPath(path Path) OptionalPath { + if path == nil { + return OptionalPath{} + } + return OptionalPath{valid: true, path: path} +} + +// Valid returns whether there is a valid path +func (p OptionalPath) Valid() bool { + return p.valid +} + +// Path returns the Path embedded in this OptionalPath. You must be sure that +// there is a valid path, since this method will panic if there is not. +func (p OptionalPath) Path() Path { + if !p.valid { + panic("Requesting an invalid path") + } + return p.path +} + +// String returns the string version of the Path, or "" if it isn't valid. +func (p OptionalPath) String() string { + if p.valid { + return p.path.String() + } else { + return "" + } +} + +// Paths is a slice of Path objects, with helpers to operate on the collection. +type Paths []Path + +// PathsForSource returns Paths rooted from SrcDir +func PathsForSource(ctx PathContext, paths []string) Paths { + ret := make(Paths, len(paths)) + for i, path := range paths { + ret[i] = PathForSource(ctx, path) + } + return ret +} + +// PathsForModuleSrc returns Paths rooted from the module's local source +// directory +func PathsForModuleSrc(ctx AndroidModuleContext, paths []string) Paths { + ret := make(Paths, len(paths)) + for i, path := range paths { + ret[i] = PathForModuleSrc(ctx, path) + } + return ret +} + +// pathsForModuleSrcFromFullPath returns Paths rooted from the module's local +// source directory, but strip the local source directory from the beginning of +// each string. +func pathsForModuleSrcFromFullPath(ctx AndroidModuleContext, paths []string) Paths { + prefix := filepath.Join(ctx.AConfig().srcDir, ctx.ModuleDir()) + "/" + ret := make(Paths, 0, len(paths)) + for _, p := range paths { + path := filepath.Clean(p) + if !strings.HasPrefix(path, prefix) { + reportPathError(ctx, "Path '%s' is not in module source directory '%s'", p, prefix) + continue } + ret = append(ret, PathForModuleSrc(ctx, path[len(prefix):])) + } + return ret +} + +// PathsWithOptionalDefaultForModuleSrc returns Paths rooted from the module's +// local source directory. If none are provided, use the default if it exists. +func PathsWithOptionalDefaultForModuleSrc(ctx AndroidModuleContext, input []string, def string) Paths { + if len(input) > 0 { + return PathsForModuleSrc(ctx, input) } + // Use Glob so that if the default doesn't exist, a dependency is added so that when it + // is created, we're run again. + path := filepath.Join(ctx.AConfig().srcDir, ctx.ModuleDir(), def) + return ctx.Glob("default", path, []string{}) } -// CheckModuleSrcDirsExist logs an error on a property if any of the directories relative to the -// top of the source tree don't exist. -func CheckSrcDirsExist(ctx AndroidModuleContext, dirs []string, prop string) { - for _, dir := range dirs { - fullDir := filepath.Join(ctx.AConfig().SrcDir(), dir) - if _, err := os.Stat(fullDir); err != nil { - if os.IsNotExist(err) { - ctx.PropertyErrorf(prop, "top-level source directory %q does not exist", dir) - } else { - ctx.PropertyErrorf(prop, "%s", err.Error()) - } +// Strings returns the Paths in string form +func (p Paths) Strings() []string { + if p == nil { + return nil + } + ret := make([]string, len(p)) + for i, path := range p { + ret[i] = path.String() + } + return ret +} + +// WritablePaths is a slice of WritablePaths, used for multiple outputs. +type WritablePaths []WritablePath + +// Strings returns the string forms of the writable paths. +func (p WritablePaths) Strings() []string { + if p == nil { + return nil + } + ret := make([]string, len(p)) + for i, path := range p { + ret[i] = path.String() + } + return ret +} + +type basePath struct { + path string + config Config +} + +func (p basePath) Ext() string { + return filepath.Ext(p.path) +} + +// SourcePath is a Path representing a file path rooted from SrcDir +type SourcePath struct { + basePath +} + +var _ Path = SourcePath{} + +// safePathForSource is for paths that we expect are safe -- only for use by go +// code that is embedding ninja variables in paths +func safePathForSource(ctx PathContext, path string) SourcePath { + p := validateSafePath(ctx, path) + ret := SourcePath{basePath{p, pathConfig(ctx)}} + + abs, err := filepath.Abs(ret.String()) + if err != nil { + reportPathError(ctx, "%s", err.Error()) + return ret + } + buildroot, err := filepath.Abs(pathConfig(ctx).buildDir) + if err != nil { + reportPathError(ctx, "%s", err.Error()) + return ret + } + if strings.HasPrefix(abs, buildroot) { + reportPathError(ctx, "source path %s is in output", abs) + return ret + } + + return ret +} + +// PathForSource returns a SourcePath for the provided paths... (which are +// joined together with filepath.Join). This also validates that the path +// doesn't escape the source dir, or is contained in the build dir. On error, it +// will return a usable, but invalid SourcePath, and report a ModuleError. +func PathForSource(ctx PathContext, paths ...string) SourcePath { + p := validatePath(ctx, paths...) + ret := SourcePath{basePath{p, pathConfig(ctx)}} + + abs, err := filepath.Abs(ret.String()) + if err != nil { + reportPathError(ctx, "%s", err.Error()) + return ret + } + buildroot, err := filepath.Abs(pathConfig(ctx).buildDir) + if err != nil { + reportPathError(ctx, "%s", err.Error()) + return ret + } + if strings.HasPrefix(abs, buildroot) { + reportPathError(ctx, "source path %s is in output", abs) + return ret + } + + if _, err = os.Stat(ret.String()); err != nil { + if os.IsNotExist(err) { + reportPathError(ctx, "source path %s does not exist", ret) + } else { + reportPathError(ctx, "%s: %s", ret, err.Error()) } } + return ret +} + +// OptionalPathForSource returns an OptionalPath with the SourcePath if the +// path exists, or an empty OptionalPath if it doesn't exist. Dependencies are added +// so that the ninja file will be regenerated if the state of the path changes. +func OptionalPathForSource(ctx blueprint.SingletonContext, intermediates string, paths ...string) OptionalPath { + p := validatePath(ctx, paths...) + path := SourcePath{basePath{p, pathConfig(ctx)}} + + abs, err := filepath.Abs(path.String()) + if err != nil { + reportPathError(ctx, "%s", err.Error()) + return OptionalPath{} + } + buildroot, err := filepath.Abs(pathConfig(ctx).buildDir) + if err != nil { + reportPathError(ctx, "%s", err.Error()) + return OptionalPath{} + } + if strings.HasPrefix(abs, buildroot) { + reportPathError(ctx, "source path %s is in output", abs) + return OptionalPath{} + } + + // Use glob to produce proper dependencies, even though we only want + // a single file. + files, err := Glob(ctx, PathForIntermediates(ctx, intermediates).String(), path.String(), nil) + if err != nil { + reportPathError(ctx, "glob: %s", err.Error()) + return OptionalPath{} + } + + if len(files) == 0 { + return OptionalPath{} + } + return OptionalPathForPath(path) +} + +func (p SourcePath) String() string { + return filepath.Join(p.config.srcDir, p.path) +} + +// Join creates a new SourcePath with paths... joined with the current path. The +// provided paths... may not use '..' to escape from the current path. +func (p SourcePath) Join(ctx PathContext, paths ...string) SourcePath { + path := validatePath(ctx, paths...) + return PathForSource(ctx, p.path, path) } -// Returns a path relative to the top level source directory. Panics if path is not inside the -// top level source directory. -func SrcDirRelPath(ctx AndroidModuleContext, path string) string { - srcDir := ctx.AConfig().SrcDir() - relPath, err := filepath.Rel(srcDir, path) +// OverlayPath returns the overlay for `path' if it exists. This assumes that the +// SourcePath is the path to a resource overlay directory. +func (p SourcePath) OverlayPath(ctx AndroidModuleContext, path Path) OptionalPath { + var relDir string + if moduleSrcPath, ok := path.(ModuleSrcPath); ok { + relDir = moduleSrcPath.sourcePath.path + } else if srcPath, ok := path.(SourcePath); ok { + relDir = srcPath.path + } else { + reportPathError(ctx, "Cannot find relative path for %s(%s)", reflect.TypeOf(path).Name(), path) + return OptionalPath{} + } + dir := filepath.Join(p.config.srcDir, p.path, relDir) + // Use Glob so that we are run again if the directory is added. + paths, err := Glob(ctx, PathForModuleOut(ctx, "overlay").String(), dir, []string{}) if err != nil { - panic(fmt.Errorf("%q is not inside %q: %s", path, srcDir, err.Error())) + reportPathError(ctx, "glob: %s", err.Error()) + return OptionalPath{} + } + if len(paths) == 0 { + return OptionalPath{} + } + relPath, err := filepath.Rel(p.config.srcDir, paths[0]) + if err != nil { + reportPathError(ctx, "%s", err.Error()) + return OptionalPath{} + } + return OptionalPathForPath(PathForSource(ctx, relPath)) +} + +// OutputPath is a Path representing a file path rooted from the build directory +type OutputPath struct { + basePath +} + +var _ Path = OutputPath{} + +// PathForOutput returns an OutputPath for the provided paths... (which are +// joined together with filepath.Join). This also validates that the path +// does not escape the build dir. On error, it will return a usable, but invalid +// OutputPath, and report a ModuleError. +func PathForOutput(ctx PathContext, paths ...string) OutputPath { + path := validatePath(ctx, paths...) + return OutputPath{basePath{path, pathConfig(ctx)}} +} + +func (p OutputPath) writablePath() {} + +func (p OutputPath) String() string { + return filepath.Join(p.config.buildDir, p.path) +} + +// Join creates a new OutputPath with paths... joined with the current path. The +// provided paths... may not use '..' to escape from the current path. +func (p OutputPath) Join(ctx PathContext, paths ...string) OutputPath { + path := validatePath(ctx, paths...) + return PathForOutput(ctx, p.path, path) +} + +// PathForIntermediates returns an OutputPath representing the top-level +// intermediates directory. +func PathForIntermediates(ctx PathContext, paths ...string) OutputPath { + path := validatePath(ctx, paths...) + return PathForOutput(ctx, ".intermediates", path) +} + +// ModuleSrcPath is a Path representing a file rooted from a module's local source dir +type ModuleSrcPath struct { + basePath + sourcePath SourcePath + moduleDir string +} + +var _ Path = ModuleSrcPath{} +var _ genPathProvider = ModuleSrcPath{} +var _ objPathProvider = ModuleSrcPath{} +var _ resPathProvider = ModuleSrcPath{} + +// PathForModuleSrc returns a ModuleSrcPath representing the paths... under the +// module's local source directory. +func PathForModuleSrc(ctx AndroidModuleContext, paths ...string) ModuleSrcPath { + path := validatePath(ctx, paths...) + return ModuleSrcPath{basePath{path, ctx.AConfig()}, PathForSource(ctx, ctx.ModuleDir(), path), ctx.ModuleDir()} +} + +// OptionalPathForModuleSrc returns an OptionalPath. The OptionalPath contains a +// valid path if p is non-nil. +func OptionalPathForModuleSrc(ctx AndroidModuleContext, p *string) OptionalPath { + if p == nil { + return OptionalPath{} + } + return OptionalPathForPath(PathForModuleSrc(ctx, *p)) +} + +func (p ModuleSrcPath) String() string { + return p.sourcePath.String() +} + +func (p ModuleSrcPath) genPathWithExt(ctx AndroidModuleContext, ext string) ModuleGenPath { + return PathForModuleGen(ctx, p.moduleDir, pathtools.ReplaceExtension(p.path, ext)) +} + +func (p ModuleSrcPath) objPathWithExt(ctx AndroidModuleContext, subdir, ext string) ModuleObjPath { + return PathForModuleObj(ctx, subdir, p.moduleDir, pathtools.ReplaceExtension(p.path, ext)) +} + +func (p ModuleSrcPath) resPathWithName(ctx AndroidModuleContext, name string) ModuleResPath { + // TODO: Use full directory if the new ctx is not the current ctx? + return PathForModuleRes(ctx, p.path, name) +} + +// ModuleOutPath is a Path representing a module's output directory. +type ModuleOutPath struct { + OutputPath +} + +var _ Path = ModuleOutPath{} + +// PathForModuleOut returns a Path representing the paths... under the module's +// output directory. +func PathForModuleOut(ctx AndroidModuleContext, paths ...string) ModuleOutPath { + p := validatePath(ctx, paths...) + return ModuleOutPath{PathForOutput(ctx, ".intermediates", ctx.ModuleDir(), ctx.ModuleName(), ctx.ModuleSubDir(), p)} +} + +// ModuleGenPath is a Path representing the 'gen' directory in a module's output +// directory. Mainly used for generated sources. +type ModuleGenPath struct { + ModuleOutPath + path string +} + +var _ Path = ModuleGenPath{} +var _ genPathProvider = ModuleGenPath{} +var _ objPathProvider = ModuleGenPath{} + +// PathForModuleGen returns a Path representing the paths... under the module's +// `gen' directory. +func PathForModuleGen(ctx AndroidModuleContext, paths ...string) ModuleGenPath { + p := validatePath(ctx, paths...) + return ModuleGenPath{ + PathForModuleOut(ctx, "gen", p), + p, } +} + +func (p ModuleGenPath) genPathWithExt(ctx AndroidModuleContext, ext string) ModuleGenPath { + // TODO: make a different path for local vs remote generated files? + return PathForModuleGen(ctx, pathtools.ReplaceExtension(p.path, ext)) +} + +func (p ModuleGenPath) objPathWithExt(ctx AndroidModuleContext, subdir, ext string) ModuleObjPath { + return PathForModuleObj(ctx, subdir, pathtools.ReplaceExtension(p.path, ext)) +} - return relPath +// ModuleObjPath is a Path representing the 'obj' directory in a module's output +// directory. Used for compiled objects. +type ModuleObjPath struct { + ModuleOutPath +} + +var _ Path = ModuleObjPath{} + +// PathForModuleObj returns a Path representing the paths... under the module's +// 'obj' directory. +func PathForModuleObj(ctx AndroidModuleContext, paths ...string) ModuleObjPath { + p := validatePath(ctx, paths...) + return ModuleObjPath{PathForModuleOut(ctx, "obj", p)} +} + +// ModuleResPath is a a Path representing the 'res' directory in a module's +// output directory. +type ModuleResPath struct { + ModuleOutPath +} + +var _ Path = ModuleResPath{} + +// PathForModuleRes returns a Path representing the paths... under the module's +// 'res' directory. +func PathForModuleRes(ctx AndroidModuleContext, paths ...string) ModuleResPath { + p := validatePath(ctx, paths...) + return ModuleResPath{PathForModuleOut(ctx, "res", p)} +} + +// PathForModuleInstall returns a Path representing the install path for the +// module appended with paths... +func PathForModuleInstall(ctx AndroidModuleContext, paths ...string) OutputPath { + var outPaths []string + if ctx.Device() { + outPaths = []string{"target", "product", ctx.AConfig().DeviceName(), "system"} + } else { + outPaths = []string{"host", ctx.HostType().String() + "-x86"} + } + outPaths = append(outPaths, paths...) + return PathForOutput(ctx, outPaths...) +} + +// validateSafePath validates a path that we trust (may contain ninja variables). +// Ensures that it does not attempt to leave the containing directory. +func validateSafePath(ctx PathContext, paths ...string) string { + // TODO: filepath.Join isn't necessarily correct with embedded ninja + // variables. '..' may remove the entire ninja variable, even if it + // will be expanded to multiple nested directories. + p := filepath.Join(paths...) + if p == ".." || strings.HasPrefix(p, "../") || strings.HasPrefix(p, "/") { + reportPathError(ctx, "Path is outside directory: %s", p) + return "" + } + return p +} + +// validatePath validates that a path does not include ninja variables, and does +// not attempt to leave the containing directory. +func validatePath(ctx PathContext, paths ...string) string { + for _, path := range paths { + if strings.Contains(path, "$") { + reportPathError(ctx, "Path contains invalid character($): %s", path) + return "" + } + } + return validateSafePath(ctx, paths...) } diff --git a/common/paths_test.go b/common/paths_test.go new file mode 100644 index 00000000..16ede0d2 --- /dev/null +++ b/common/paths_test.go @@ -0,0 +1,167 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Licensed 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 common + +import ( + "errors" + "fmt" + "reflect" + "strings" + "testing" +) + +type strsTestCase struct { + in []string + out string + err []error +} + +var commonValidatePathTestCases = []strsTestCase{ + { + in: []string{""}, + out: "", + }, + { + in: []string{"a/b"}, + out: "a/b", + }, + { + in: []string{"a/b", "c"}, + out: "a/b/c", + }, + { + in: []string{"a/.."}, + out: ".", + }, + { + in: []string{"."}, + out: ".", + }, + { + in: []string{".."}, + out: "", + err: []error{errors.New("Path is outside directory: ..")}, + }, + { + in: []string{"../a"}, + out: "", + err: []error{errors.New("Path is outside directory: ../a")}, + }, + { + in: []string{"b/../../a"}, + out: "", + err: []error{errors.New("Path is outside directory: ../a")}, + }, + { + in: []string{"/a"}, + out: "", + err: []error{errors.New("Path is outside directory: /a")}, + }, +} + +var validateSafePathTestCases = append(commonValidatePathTestCases, []strsTestCase{ + { + in: []string{"$host/../$a"}, + out: "$a", + }, +}...) + +var validatePathTestCases = append(commonValidatePathTestCases, []strsTestCase{ + { + in: []string{"$host/../$a"}, + out: "", + err: []error{errors.New("Path contains invalid character($): $host/../$a")}, + }, + { + in: []string{"$host/.."}, + out: "", + err: []error{errors.New("Path contains invalid character($): $host/..")}, + }, +}...) + +func TestValidateSafePath(t *testing.T) { + for _, testCase := range validateSafePathTestCases { + ctx := &configErrorWrapper{} + out := validateSafePath(ctx, testCase.in...) + check(t, "validateSafePath", p(testCase.in), out, ctx.errors, testCase.out, testCase.err) + } +} + +func TestValidatePath(t *testing.T) { + for _, testCase := range validatePathTestCases { + ctx := &configErrorWrapper{} + out := validatePath(ctx, testCase.in...) + check(t, "validatePath", p(testCase.in), out, ctx.errors, testCase.out, testCase.err) + } +} + +func TestOptionalPath(t *testing.T) { + var path OptionalPath + checkInvalidOptionalPath(t, path) + + path = OptionalPathForPath(nil) + checkInvalidOptionalPath(t, path) +} + +func checkInvalidOptionalPath(t *testing.T, path OptionalPath) { + if path.Valid() { + t.Errorf("Uninitialized OptionalPath should not be valid") + } + if path.String() != "" { + t.Errorf("Uninitialized OptionalPath String() should return \"\", not %q", path.String()) + } + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected a panic when calling Path() on an uninitialized OptionalPath") + } + }() + path.Path() +} + +func check(t *testing.T, testType, testString string, + got interface{}, err []error, + expected interface{}, expectedErr []error) { + + printedTestCase := false + e := func(s string, expected, got interface{}) { + if !printedTestCase { + t.Errorf("test case %s: %s", testType, testString) + printedTestCase = true + } + t.Errorf("incorrect %s", s) + t.Errorf(" expected: %s", p(expected)) + t.Errorf(" got: %s", p(got)) + } + + if !reflect.DeepEqual(expectedErr, err) { + e("errors:", expectedErr, err) + } + + if !reflect.DeepEqual(expected, got) { + e("output:", expected, got) + } +} + +func p(in interface{}) string { + if v, ok := in.([]interface{}); ok { + s := make([]string, len(v)) + for i := range v { + s[i] = fmt.Sprintf("%#v", v[i]) + } + return "[" + strings.Join(s, ", ") + "]" + } else { + return fmt.Sprintf("%#v", in) + } +} |