** FpmConfig is gathered from a hierarchy of 'fpm.props' files. These files are looked for in the following locations:
**
** - '<FAN_HOME>/etc/afFpm/fpm.props'
** - '<WORK_DIR>/etc/afFpm/fpm.props'
** - './fpm.props'
**
** Note that the config files are additive but the values are not. If all 3 files exist, then all 3 files are merged together,
** with config values from a more specific file replacing (or overriding) values found in less specific one.
**
** '<WORK_DIR>' may be specified with the 'FPM_ENV_PATH' environment variable. This means that **ALL** the config for FPM may
** live outside of the Fantom installation. The only FPM file that needs to live in the Fantom installation is the 'afFpm.pod'
** file itself.
**
** Config may be removed by specifying an empty string as a value. For example, to remove the eggbox repository:
**
** fanrRepo.eggbox =
**
** Read the comments in the actual 'fpm.props' file itself for more details.
const class FpmConfig {
** The directory used to resolve relative files.
const File baseDir
** The Fantom installation.
const File homeDir
** The temp directory.
const File tempDir
** A list of working directories.
** The 'workDir' as returned by 'FpmEnv' is is always first item in this list.
** 'homeDir' is always the last entry in the list, so it is never empty.
const File[] workDirs
** A map of named directory repositories. These are essentially just dirs of pods.
const Str:File dirRepos
** A map of named fanr repositories, may be local or remote.
const Str:Uri fanrRepos
** A list of libraries used to launch applications
const Str[] launchPods
** The config files used to generate this class.
const File[] configFiles
** The raw FPM config gleaned from the 'configFiles'.
** Does not include fanr credentials.
const Str:Str rawConfig
private const Str:Str _rawConfig
private new makePrivate(|This|in) { in(this) }
@NoDoc
static new make() {
makeFromDirs(File(`./`), Env.cur.homeDir, Env.cur.vars["FAN_ENV_PATH"])
}
@NoDoc
static new makeFromDirs(File baseDir, File homeDir, Str? envPaths) {
configFilename := `fpm.props`
// grab the config filename from an env var, but only if the version matches
// this is useful for testing and development
t1 := Env.cur.vars["FPM_CONFIG_FILENAME"]
if (t1 != null) {
t2 := Uri(t1, false)
if (t2 != null) {
t3 := t2.path
if (t3.getSafe(0) == FpmEnv#.pod.version.toStr) {
t4 := t3.getSafe(1)
if (t4 != null) {
t5 := Uri(t4, false)
if (t5 != null)
configFilename = t5
}
}
}
}
baseDir = baseDir.normalize
fpmFile := (File?) baseDir.plus(configFilename).normalize
while (fpmFile != null && !fpmFile.exists)
fpmFile = fpmFile.parent.parent?.plus(configFilename)
// this is a little bit chicken and egg - we use the workDir to find config.props to find the workDir!
workDirs := "" as Str
workDirs = (workDirs?.trimToNull == null ? "" : workDirs + File.pathSep) + (envPaths ?: "")
workDirs = (workDirs?.trimToNull == null ? "" : workDirs + File.pathSep) + homeDir.osPath
workFile := workDirs.split(File.pathSep.chars.first).exclude { it.isEmpty }.map { toAbsDir(it) + `etc/afFpm/` + configFilename }.unique as File[]
if (fpmFile != null)
workFile.insert(0, fpmFile)
workFile = workFile.findAll { it.exists }
fpmProps := Str:Str[:] { it.ordered = true }
workFile.eachr {
newProps := it.readProps
if (newProps["configCmd"] == "clearExisting")
fpmProps.clear
fpmProps.setAll(it.readProps)
}
return makeInternal(baseDir, homeDir, envPaths, fpmProps, workFile.reverse)
}
@NoDoc
internal new makeInternal(File baseDir, File homeDir, Str? envPaths, Str:Str fpmProps, File[]? configFiles) {
this.baseDir = baseDir = baseDir.normalize
if (baseDir.isDir.not || baseDir.exists.not)
throw ArgErr("Base directory is not valid: ${baseDir.osPath}")
homeDir = homeDir.normalize
if (homeDir.isDir.not || homeDir.exists.not)
throw ArgErr("Home directory is not valid: ${homeDir.osPath}")
this.homeDir = homeDir
strInterpol := |Str? str->Str?| {
if (str == null)
return null
// don't replace with osPath because it has a trailing slash on nix, but not on windows!
if ((this.homeDir as File) != null)
str = str.replace("\$fanHome", homeDir.uri.toStr).replace("\${fanHome}", homeDir.uri.toStr)
if ((this.workDirs as File[]) != null)
str = str.replace("\$workDir", workDirs.first.uri.toStr).replace("\${workDir}", workDirs.first.uri.toStr)
if ((this.tempDir as File) != null)
str = str.replace("\$tempDir", tempDir.uri.toStr).replace("\${tempDir}", tempDir.uri.toStr)
return str
}
workDirs := strInterpol(fpmProps["workDirs"])
workDirs = (workDirs?.trimToNull == null ? "" : workDirs + File.pathSep) + (envPaths ?: "")
workDirs = (workDirs?.trimToNull == null ? "" : workDirs + File.pathSep) + homeDir.osPath.toStr
this.workDirs = workDirs.split(File.pathSep.chars.first).exclude { it.isEmpty }.map { toAbsDir(it) }.unique
tempDir := strInterpol(fpmProps["tempDir"])
if (tempDir == null)
tempDir = this.workDirs.first.plus(`temp/`, false).osPath.toStr
this.tempDir = toAbsDir(tempDir)
dirRepos := (Str:File) fpmProps.findAll |path, name| {
name.startsWith("dirRepo.")
}.keys.sort.reduce(Str:File[:] { ordered=true }) |Str:File repos, key| {
path := fpmProps[key].trimToNull
if (path == null) return repos // allow config to be removed
name := key["dirRepo.".size..-1]
file := toRelDir(strInterpol(path.trim), baseDir)
repos[name] = file
return repos
}
fanrRepos := (Str:Uri) fpmProps.findAll |path, name| {
name.startsWith("fanrRepo.") && !name.endsWith(".username") && !name.endsWith(".password")
}.keys.sort.reduce(Str:Uri[:] { ordered=true }) |Str:Uri repos, key| {
path := fpmProps[key].trimToNull
if (path == null) return repos // allow config to be removed
name := key["fanrRepo.".size..-1]
url := Uri(path, false)
if (url?.scheme != "http" && url?.scheme != "https")
url = toRelDir(strInterpol(path.trim), baseDir).uri
else
if (url.userInfo != null) {
userInfo := url.userInfo.split(':')
repoName := key["fanrRepo.".size..-1]
username := Uri.decodeToken(userInfo.getSafe(0) ?: "", Uri.sectionPath).trimToNull
password := Uri.decodeToken(userInfo.getSafe(1) ?: "", Uri.sectionPath).trimToNull
fpmProps["fanrRepo.${repoName}.username"] = username
fpmProps["fanrRepo.${repoName}.password"] = password
url = url.toStr.replace("${url.userInfo}@", "").toUri
}
repos[name] = url
return repos
}
// add workDirs to dirRepos (note the last dir is always fanHome, so ignore that one)
if (this.workDirs.size > 1)
if (!dirRepos.containsKey("workDir"))
dirRepos["workDir"] = this.workDirs.first + `lib/fan/`
if (this.workDirs.size > 2)
this.workDirs.eachRange(1..<-1) |dir, i| {
if (!dirRepos.containsKey("workDir[$i]"))
dirRepos["workDir[$i]"] = dir + `lib/fan/`
}
// if not defined, add "fanHome" as a new directory repo
// add fanHome last as these pods are _least_ important when resolving environment runtime pods
if (!dirRepos.containsKey("fanHome"))
dirRepos["fanHome"] = homeDir + `lib/fan/`
// if "default" is not defined, set it to fanHome so FPM becomes a drop in replacement for fanr
// this allows people to update their fanHome env by default
if (!fanrRepos.containsKey("default") && !dirRepos.containsKey("default"))
dirRepos["default"] = homeDir + `lib/fan/`
// as Env is available to the entire FVM, be nice and remove any credentials
// it's just lip service really, as anyone could re-read the fpm.config files
rawConfig := fpmProps.exclude |val, key| { key.endsWith(".username") || key.endsWith(".password") }
rawConfig = rawConfig.map |val, key| {
userInfo := Uri(val, false)?.userInfo
return userInfo == null ? val : val.replace("${userInfo}@", "")
}
both := dirRepos.keys.intersection(fanrRepos.keys)
if (both.size > 0)
throw Err("Repository '" + both.join(", ") + "' is defined as both a dirRepo AND a fanrRepo")
// not sure if I want to cache them all here
// allRepos := Str:Repository[:]
// dirRepos .each |file, name| { allRepos[name] = LocalDirRepository(name, file) }
// fanrRepos.each | url, name| {
// username := fpmProps["fanrRepo.${name}.username"]
// password := fpmProps["fanrRepo.${name}.password"]
// if (url.scheme == null || url.scheme == "file")
// allRepos[name] = LocalFanrRepository(name, url.toFile)
// if (url.scheme == "http" || url.scheme == "https")
// allRepos[name] = RemoteFanrRepository(name, url, username, password)
// }
this.dirRepos = dirRepos
this.fanrRepos = fanrRepos
this.launchPods = fpmProps["launchPods"]?.split(',') ?: Str#.emptyList
this.configFiles = configFiles ?: File[,]
this.rawConfig = rawConfig
this._rawConfig = fpmProps
}
** Returns a 'Repository' instance for the named repository.
** 'repoName' may be either a 'dirRepo' or a 'fanrRepo'.
Repository? repository(Str repoName, Str? username := null, Str? password := null) {
if (dirRepos.containsKey(repoName))
return LocalDirRepository(repoName, dirRepos[repoName])
if (fanrRepos.containsKey(repoName)) {
if (username == null)
username = _rawConfig["fanrRepo.${repoName}.username"]
if (password == null)
password = _rawConfig["fanrRepo.${repoName}.password"]
url := fanrRepos[repoName]
if (url.scheme == null || url.scheme == "file")
return LocalFanrRepository(repoName, url.toFile)
if (url.scheme == "http" || url.scheme == "https")
return RemoteFanrRepository(repoName, url, username, password)
throw ArgErr("Unknown scheme '${url.scheme}' in $url")
}
return null
}
** Returns a list of all repositories.
** Note that some repositories may point to the same directory / URL.
Repository[] repositories() {
repos1 := dirRepos.keys.map { repository(it) }
repos2 := fanrRepos.keys.map { repository(it) }
// note that default, workDir, and fanHome may be the same
return repos1.addAll(repos2)
}
** Dumps debug output to a string. The string will look similar to:
**
** pre>
** FPM Environment:
** Target Pod : afIoc 3.0+
** Base Dir : C:\
** Fan Home Dir : C:\Apps\fantom-1.0.70
** Work Dirs : C:\Repositories\Fantom
** C:\Apps\fantom-1.0.70
** Temp Dir : C:\Repositories\Fantom\temp
** Config Files : C:\Apps\fantom-1.0.70\etc\afFpm\config.props
**
** Dir Repos :
** workDir = C:\Repositories\Fantom
** fanHome = C:\Apps\fantom-1.0.70/lib/fan/
**
** Fanr Repos :
** default = C:\Repositories\Fantom\repo-default
** eggbox = http://eggbox.fantomfactory.org/fanr/
** release = C:\Repositories\Fantom\repo-release
** repo302 = http://repo.status302.com/fanr/
** <pre
Str dump() {
str := ""
str += " Base Dir : " + dumpList([baseDir])
str += " Fan Home Dir : " + dumpList([homeDir])
str += " Work Dirs : " + dumpList(workDirs)
str += " Temp Dir : " + dumpList([tempDir])
str += " Config Files : " + dumpList(configFiles)
if (dirRepos.size > 0) str += "\n"
str += " Dir Repos : " + (dirRepos.isEmpty ? "(none)" : "") + "\n"
maxDir := dirRepos.keys.reduce(14) |Int size, repoName| { size.max(repoName.size) } as Int
dirRepos.each |repoFile, repoName| {
exists := repoFile.exists ? "" : " (does not exist)"
str += repoName.justr(maxDir) + " = " + repoFile.osPath + exists + "\n"
}
if (fanrRepos.size > 0) str += "\n"
str += " Fanr Repos : " + (fanrRepos.isEmpty ? "(none)" : "") + "\n"
maxDir = fanrRepos.keys.reduce(14) |Int size, repoName| { size.max(repoName.size) } as Int
fanrRepos.each |repoUrl, repoName| {
usr := repoUrl.userInfo == null ? "" : repoUrl.userInfo + "@"
url := repoUrl.toStr.replace(usr, "")
str += repoName.justr(maxDir) + " = " + url + "\n"
}
return str
}
private Str dumpList(File[] files) {
if (files.isEmpty)
return "(none)\n"
str := "${files.first.osPath}\n"
if (files.size > 1)
files[1..-1].each {
exists := it.exists ? "" : " (does not exist)"
str += "".justr(14) + " ${it.osPath}${exists}\n"
}
return str
}
private static File toAbsDir(Str dirPath) {
FileUtils.toAbsDir(dirPath)
}
private static File toRelDir(Str dirPath, File baseDir) {
FileUtils.toAbsDir(dirPath, baseDir)
}
}