sourceafFpm::FpmEnv.fan

using concurrent::Actor

** Provides a targeted environment for a specific pod. 
** 
** The WorkDirs and HomeDir are always queried if a pod is not found in a local repository.
const class FpmEnv : Env {
    @NoDoc  // so F4 can set it's own
    const Log               log     := FpmConfig#.pod.log

    ** The error, if any, encountered when resolving pods for the target environment.
    const Err?              error
    
    ** The config used for this environment.
    const FpmConfig         fpmConfig
    
    ** The pod this environment is targeted to.
    const Depend            targetPod

    ** Dependent pods that have been resolved specifically for 'targetPod'. 
    ** Either this or 'unresolvedPods' will be empty.
    const Str:PodFile       resolvedPods
    
    ** Dependent pods for which FPM could not reach a consensus on which version to use.
    ** Either this or 'resolvedPods' will be empty.
    const Str:UnresolvedPod unresolvedPods

    ** Pods used in this environment.
    ** This is a combination of pods from directory repositories, overridden by any resolved pods.
    ** 
    ** If the target could not be resolved, then this defaults to the latest version of all known
    ** local pods.
    ** 
    ** By acknowledging pods from fanHome and workDirs, this environment works in a more expected 
    ** manner whereby pods not explicitly referenced can still be discovered at runtime (e.g. icons)
    ** and index meta inspected.
    const Str:PodFile       environmentPods

    // TODO add targetPod depends to this std ctor - but beware, it's a backwards breaking change!
    @NoDoc  // ensure we have the standard Env ctor: new make(Env env)
    new make(Env? env := null, |This|? in := null) : super.make(env ?: Env.cur) {
        // this was supposed to be thrown when calling "fan -version", but
        // sys::Err: Method not mapped to java.lang.reflect correctly afFpm::FpmEnv.make
        // is thrown at "MethodFunc.isStatic(Method.java:496)" before make() is called
        try     f := File(`./`)
        catch   throw Err("FpmEnv cannot be used if NOT executing a Fantom pod")

        if (Env.cur.vars["FPM_DEBUG"] == "true")
            log.level = LogLevel.debug

        in?.call(this)  // let F4 & LSP set its own logger and fpmConfig

        if (fpmConfig == null)
            fpmConfig = FpmConfig()

        if (log.isDebug) {
            title := "Fantom Pod Manager (FPM) v${typeof.pod.version}"
            log.debug("")
            log.debug("${title}")
            log.debug("".padl(title.size, '-'))     
            log.debug("")
        }

        resolver := Resolver(fpmConfig.repositories).localOnly { it.log = this.log }
        
        try {
            targetPod   := findTarget
            if (targetPod != null) {
                satisfied   := resolver.satisfy(targetPod, fpmConfig.extraPods)
                resolver.cleanUp
                
                this.targetPod      = satisfied.targetPod
                this.resolvedPods   = satisfied.resolvedPods
                this.unresolvedPods = satisfied.unresolvedPods
                this.environmentPods= resolver.resolveAll(true).setAll(satisfied.resolvedPods)
            }
            
        } catch (UnknownPodErr err) {
            // todo auto-download / install the pod dependency!??
            // beware, also thrown by BuildPod on malformed dependency str
            error = err

        } catch (Err err) {
            error = err

        } finally {
            this.environmentPods= this.environmentPods  != null ? this.environmentPods  : [:]
            this.unresolvedPods = this.unresolvedPods   != null ? this.unresolvedPods   : [:]
            this.resolvedPods   = this.resolvedPods     != null ? this.resolvedPods     : [:]
            this.targetPod      = this.targetPod        != null ? this.targetPod        : Depend("??? 0")
        }
        
        loggedLatest := false
        if (targetPod.name == "???")
            if (!loggedLatest) {
                loggedLatest = true
                if (error != null)
                    log.warn("FPM: Pod resolve error - defaulting to latest pod versions")
                else
                    log.warn("FPM: Could not find target - defaulting to latest pod versions")
                this.environmentPods = resolver.resolveAll(false).setAll(resolvedPods)
            }

        if (Env.cur.vars["FPM_ALL_PODS"]?.lower?.toBool(false) == true)
            if (!loggedLatest) {
                loggedLatest = true
                log.info("FPM: Found env var: FPM_ALL_PODS = true; making all pods available")
                this.environmentPods = resolver.resolveAll(false).setAll(resolvedPods)
            }
        
        // ---- dump stuff to logs ----

        dumped := false

        // if there's something wrong, then make sure the user sees the dump
        if (error != null || unresolvedPods.size > 0) {
            log.warn(dump)
            dumped = true
        }

        if (!dumped && log.isDebug) {
            log.debug(dump)
            dumped = true
        }

        if (unresolvedPods.size > 0) {
            log.warn(FpmUtils.dumpUnresolved(unresolvedPods.vals))
            if (!loggedLatest) {
                loggedLatest = true
                log.warn("Defaulting to latest pod versions")
                this.environmentPods = resolver.resolveAll(false).setAll(resolvedPods)
            }
        }

        if (error != null) {
            log.err  (error.toStr)
            log.debug(error.traceToStr)
        }       
        
        if (Env.cur.vars["FPM_DUMP"]?.lower?.toBool(false) == true) {
            resolver.resolveAll(false)
            resolver.dumpToOut
            echo("\n\nAll pods:\n")
            this.environmentPods.vals.sort.each {
                echo(" = $it.name $it.version")
            }
        }
    }

    @NoDoc
    override File workDir() {
        fpmConfig.workDirs.first
    }

    @NoDoc
    override File tempDir() {
        fpmConfig.tempDir
    }
    
    ** Return the list of pod names for all the pods currently installed in this environment.
    override Str[] findAllPodNames() {
        environmentPods.keys 
    }

    ** Resolve the pod file for the given pod name.
    override File? findPodFile(Str podName) {
        environmentPods[podName]?.file
    }

    ** Find all the files in the environment which match a relative path such as 'etc/foo/config.props'. 
    override File[] findAllFiles(Uri uri) {
        if (uri.isPathAbs) throw ArgErr("Uri must be rel: $uri")
        return fpmConfig.workDirs.map { it + uri }.exclude |File f->Bool| { f.exists.not }
    }

    ** Find a file in the environment using a relative path such as 'etc/foo/config.props'. 
    override File? findFile(Uri uri, Bool checked := true) {
        if (uri.isPathAbs) throw ArgErr("Uri must be rel: $uri")
        return fpmConfig.workDirs.eachWhile |dir| {
            f := dir.plus(uri, false)
            return f.exists ? f : null
        } ?: (checked ? throw UnresolvedErr(uri.toStr) : null)
    }

    @NoDoc
    virtual TargetPod? findTarget() {
        target := Actor.locals["afFpm.target"]

        // allow target to be either Depend or TargetPod
        if (target is Depend)
            target = TargetPod((Depend) target)

        target = target as TargetPod
        
        if (target == null)
            target = FpmUtils.findTarget(this)
        
        return target
    }

    ** Dumps the FPM environment to a string. This includes the FPM Config and a list of resolved pods.
    virtual Str dump() {
        dumpEnv(targetPod, resolvedPods.vals, fpmConfig)
    }
    
    @NoDoc  // used by F4 FPM
    static Str dumpEnv(Depend targetPod, PodFile[] resolvedPods, FpmConfig? fpmConfig) {
        str := "\n\n"
        str += "FPM (${FpmConfig#.pod.version}) Environment:\n"
        str += "\n"
        str += "    Target Pod : ${targetPod}\n"
        str += fpmConfig?.dump ?: ""
        str += "\n"
        str += "Resolved ${resolvedPods.size} pod" + (resolvedPods.size == 1 ? "" : "s") + (resolvedPods.size == 0 ? "" : ":") + "\n"
        
        maxNom := resolvedPods.reduce(0) |Int size, pod| { size.max(pod.name.size) } as Int
        maxVer := resolvedPods.reduce(0) |Int size, pod| { size.max(pod.version.toStr.size) }
        resolvedPods.sort.each |podFile| {
            str += podFile.name.justr(maxNom + 2) + " " + podFile.version.toStr.justl(maxVer) + " - " + podFile.file.osPath + "\n"
        }
        str += "\n"
        
        // unsatisfied constraints and errors should be logged separately after this dump 

        return str
    }
}

@NoDoc
const class TargetPod {
    const Depend    pod
    const Depend[]? dependencies    // used for build.fan

    // used by F4's FpmCompileEnv and lspFantom
    new make(Depend pod, Depend[]? dependencies := null) {
        this.pod            = pod
        this.dependencies   = dependencies
    }
    
    internal static new fromBuildPod(BuildPod buildPod) {
        podName := buildPod.podName 
        version := buildPod.version 
        depends := buildPod.depends 
        return TargetPod(Depend("$podName $version"), depends.map { Depend(it, false) }.exclude { it == null })
    }
    
    PodFile podFile() {
        PodFile(pod.name, pod.version, dependencies, `targetpod:${pod.name}`, StubPodRepository.instance)
    }
}