Skip to content

Commit

Permalink
Merge branch 'scope-to-profile'
Browse files Browse the repository at this point in the history
  • Loading branch information
memo33 committed Apr 18, 2024
2 parents ab3299e + e9ea157 commit 672e2cb
Show file tree
Hide file tree
Showing 12 changed files with 93 additions and 89 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

### Changed
- decreased caching period of channel table-of-contents file from 24 hours to 30 minutes to receive package updates sooner
- The API was upgraded to version 1.2.
- The `sc4pac server` option `--scope-root` was renamed to `--profile-root` and the corresponding error to `/error/profile-not-initialized`.


## [0.4.1] - 2024-03-25
Expand All @@ -27,7 +29,7 @@

### Changed
- The API was upgraded to version 1.1.
- The API now sends `/error/scope-not-initialized` & `/error/init/not-allowed` with HTTP status code 409 instead of 405.
- The API now sends `/error/profile-not-initialized` & `/error/init/not-allowed` with HTTP status code 409 instead of 405.
- The API endpoint `/packages.list` now includes a `category` for each package.

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ host-web: # channel-testing-web
# sbt -Dcoursier.credentials="$(realpath sc4pac-credentials.properties)"

clean:
rm -rf plugins temp sc4pac-plugins.json sc4pac-plugins-lock.json scopes
rm -rf plugins temp sc4pac-plugins.json sc4pac-plugins-lock.json profiles
clean-cache: clean
rm -rf cache

Expand Down
14 changes: 7 additions & 7 deletions api.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# API - version 1.1
# API - version 1.2

The API allows other programs to control *sc4pac* in a client-server fashion.

Expand Down Expand Up @@ -29,7 +29,7 @@ GET /update (websocket)
- All endpoints may return some generic errors:
- 400 (incorrect input)
- 404 (non-existing packages, assets, etc.)
- 409 `/error/scope-not-initialized` (when not initialized)
- 409 `/error/profile-not-initialized` (when not initialized)
- 500 (unexpected unresolvable situations)
- 502 (download failures)
- Errors are of the form
Expand All @@ -43,8 +43,8 @@ GET /update (websocket)

## init

Initialize the scope by configuring the location of plugins and cache.
Scopes are used to manage multiple plugins folders.
Initialize the profile by configuring the location of plugins and cache.
Profiles are used to manage multiple plugins folders.

Synopsis: `POST /init {plugins: "<path>", cache: "<path>"}`

Expand All @@ -55,7 +55,7 @@ Returns:
`platformDefaults: {plugins: ["<path>", …], cache: ["<path>", …]}`
for recommended platform-specific locations to use.

?> When managing multiple scopes, use the same cache for all of them.
?> When managing multiple profiles, use the same cache for all of them.

- 200 `{"$type": "/result", "ok": true}` on success.

Expand All @@ -74,11 +74,11 @@ Returns:
"platformDefaults": {
"plugins": [
"/home/memo/Documents/SimCity 4/Plugins",
"/home/memo/git/sc4/sc4pac/scopes/scope-1/plugins"
"/home/memo/git/sc4/sc4pac/profiles/profile-1/plugins"
],
"cache": [
"/home/memo/.cache/sc4pac",
"/home/memo/git/sc4/sc4pac/scopes/scope-1/cache"
"/home/memo/git/sc4/sc4pac/profiles/profile-1/cache"
]
}
}
Expand Down
50 changes: 25 additions & 25 deletions src/main/scala/sc4pac/Data.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ object JsonData extends SharedData {
variant: Variant,
channels: Seq[java.net.URI]
) derives ReadWriter {
val pluginsRootAbs: RIO[ScopeRoot, os.Path] = ZIO.service[ScopeRoot].map(scopeRoot => os.Path(pluginsRoot, scopeRoot.path))
val cacheRootAbs: RIO[ScopeRoot, os.Path] = ZIO.service[ScopeRoot].map(scopeRoot => os.Path(cacheRoot, scopeRoot.path))
val tempRootAbs: RIO[ScopeRoot, os.Path] = ZIO.service[ScopeRoot].map(scopeRoot => os.Path(tempRoot, scopeRoot.path))
val pluginsRootAbs: RIO[ProfileRoot, os.Path] = ZIO.service[ProfileRoot].map(profileRoot => os.Path(pluginsRoot, profileRoot.path))
val cacheRootAbs: RIO[ProfileRoot, os.Path] = ZIO.service[ProfileRoot].map(profileRoot => os.Path(cacheRoot, profileRoot.path))
val tempRootAbs: RIO[ProfileRoot, os.Path] = ZIO.service[ProfileRoot].map(profileRoot => os.Path(tempRoot, profileRoot.path))
}
object Config {
/** Turns an absolute path into a relative one if it is a subpath of scopeRoot, otherwise returns an absolute path. */
def subRelativize(path: os.Path, scopeRoot: ScopeRoot): NioPath = {
/** Turns an absolute path into a relative one if it is a subpath of profileRoot, otherwise returns an absolute path. */
def subRelativize(path: os.Path, profileRoot: ProfileRoot): NioPath = {
try {
val sub: os.SubPath = path.subRelativeTo(scopeRoot.path)
val sub: os.SubPath = path.subRelativeTo(profileRoot.path)
sub.toNIO
} catch {
case _: IllegalArgumentException => path.toNIO
Expand All @@ -63,24 +63,24 @@ object JsonData extends SharedData {

case class Plugins(config: Config, explicit: Seq[BareModule]) derives ReadWriter
object Plugins {
def path(scopeRoot: os.Path): os.Path = scopeRoot / "sc4pac-plugins.json"
def path(profileRoot: os.Path): os.Path = profileRoot / "sc4pac-plugins.json"

def pathURIO: URIO[ScopeRoot, os.Path] = ZIO.service[ScopeRoot].map(scopeRoot => Plugins.path(scopeRoot.path))
def pathURIO: URIO[ProfileRoot, os.Path] = ZIO.service[ProfileRoot].map(profileRoot => Plugins.path(profileRoot.path))

private val projDirs = dev.dirs.ProjectDirectories.from("", cli.BuildInfo.organization, cli.BuildInfo.name) // qualifier, organization, application

val defaultPluginsRoot: URIO[ScopeRoot, Seq[os.Path]] = ZIO.serviceWith[ScopeRoot](scopeRoot => Seq(
val defaultPluginsRoot: URIO[ProfileRoot, Seq[os.Path]] = ZIO.serviceWith[ProfileRoot](profileRoot => Seq(
os.home / "Documents" / "SimCity 4" / "Plugins",
scopeRoot.path / "plugins"
profileRoot.path / "plugins"
))

val defaultCacheRoot: URIO[ScopeRoot, Seq[os.Path]] = ZIO.serviceWith[ScopeRoot](scopeRoot => Seq(
val defaultCacheRoot: URIO[ProfileRoot, Seq[os.Path]] = ZIO.serviceWith[ProfileRoot](profileRoot => Seq(
os.Path(java.nio.file.Paths.get(projDirs.cacheDir)),
scopeRoot.path / "cache"
profileRoot.path / "cache"
))

/** Prompt for pluginsRoot and cacheRoot. This has a `CliPrompter` constraint as we only want to prompt about this using the CLI. */
val promptForPaths: RIO[ScopeRoot & CliPrompter, (os.Path, os.Path)] = {
val promptForPaths: RIO[ProfileRoot & CliPrompter, (os.Path, os.Path)] = {
val task = for {
defaultPlugins <- defaultPluginsRoot
pluginsRoot <- Prompt.paths("Choose the location of your Plugins folder. (It is recommended to start with an empty folder.)", defaultPlugins)
Expand All @@ -95,15 +95,15 @@ object JsonData extends SharedData {
}

/** Init and write. */
def init(pluginsRoot: os.Path, cacheRoot: os.Path): RIO[ScopeRoot, Plugins] = {
def init(pluginsRoot: os.Path, cacheRoot: os.Path): RIO[ProfileRoot, Plugins] = {
for {
scopeRoot <- ZIO.service[ScopeRoot]
tempRoot <- ZIO.succeed(scopeRoot.path / "temp") // customization not needed
profileRoot <- ZIO.service[ProfileRoot]
tempRoot <- ZIO.succeed(profileRoot.path / "temp") // customization not needed
data = Plugins(
config = Config(
pluginsRoot = Config.subRelativize(pluginsRoot, scopeRoot),
cacheRoot = Config.subRelativize(cacheRoot, scopeRoot),
tempRoot = Config.subRelativize(tempRoot, scopeRoot),
pluginsRoot = Config.subRelativize(pluginsRoot, profileRoot),
cacheRoot = Config.subRelativize(cacheRoot, profileRoot),
tempRoot = Config.subRelativize(tempRoot, profileRoot),
variant = Map.empty,
channels = Constants.defaultChannelUrls),
explicit = Seq.empty)
Expand All @@ -112,7 +112,7 @@ object JsonData extends SharedData {
} yield data
}

val read: ZIO[ScopeRoot, ErrStr, Plugins] = Plugins.pathURIO.flatMap { pluginsPath =>
val read: ZIO[ProfileRoot, ErrStr, Plugins] = Plugins.pathURIO.flatMap { pluginsPath =>
val task: IO[ErrStr | java.io.IOException, Plugins] =
ZIO.ifZIO(ZIO.attemptBlockingIO(os.exists(pluginsPath)))(
onFalse = ZIO.fail(s"Configuration file does not exist: $pluginsPath"),
Expand All @@ -122,7 +122,7 @@ object JsonData extends SharedData {
}

/** Read Plugins from file if it exists, else create it and write it to file. */
val readOrInit: RIO[ScopeRoot & CliPrompter, Plugins] = Plugins.pathURIO.flatMap { pluginsPath =>
val readOrInit: RIO[ProfileRoot & CliPrompter, Plugins] = Plugins.pathURIO.flatMap { pluginsPath =>
ZIO.ifZIO(ZIO.attemptBlocking(os.exists(pluginsPath)))(
onTrue = JsonIo.read[Plugins](pluginsPath),
onFalse = for {
Expand Down Expand Up @@ -171,12 +171,12 @@ object JsonData extends SharedData {
}
}
object PluginsLock {
def path(scopeRoot: os.Path): os.Path = scopeRoot / "sc4pac-plugins-lock.json"
def path(profileRoot: os.Path): os.Path = profileRoot / "sc4pac-plugins-lock.json"

def pathURIO: URIO[ScopeRoot, os.Path] = ZIO.service[ScopeRoot].map(scopeRoot => PluginsLock.path(scopeRoot.path))
def pathURIO: URIO[ProfileRoot, os.Path] = ZIO.service[ProfileRoot].map(profileRoot => PluginsLock.path(profileRoot.path))

/** Read PluginsLock from file if it exists, else create it and write it to file. */
val readOrInit: RIO[ScopeRoot, PluginsLock] = PluginsLock.pathURIO.flatMap { pluginsLockPath =>
val readOrInit: RIO[ProfileRoot, PluginsLock] = PluginsLock.pathURIO.flatMap { pluginsLockPath =>
ZIO.ifZIO(ZIO.attemptBlocking(os.exists(pluginsLockPath)))(
onTrue = JsonIo.read[PluginsLock](pluginsLockPath),
onFalse = {
Expand All @@ -186,7 +186,7 @@ object JsonData extends SharedData {
)
}

val listInstalled: RIO[ScopeRoot, Seq[DepModule]] = PluginsLock.pathURIO.flatMap { pluginsLockPath =>
val listInstalled: RIO[ProfileRoot, Seq[DepModule]] = PluginsLock.pathURIO.flatMap { pluginsLockPath =>
ZIO.ifZIO(ZIO.attemptBlocking(os.exists(pluginsLockPath)))(
onTrue = JsonIo.read[PluginsLock](pluginsLockPath).map(_.installed.map(_.toDepModule)),
onFalse = ZIO.succeed(Seq.empty)
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/sc4pac/Find.scala
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ object Find {
context.logger.log(s"Could not find metadata of ${module}. Trying to update channel contents.")
for {
repos <- Sc4pac.initializeRepositories(repoUris, context.cache, Constants.channelContentsTtlShort) // 60 seconds
.provideLayer(zio.ZLayer.succeed(ScopeRoot(context.scopeRoot)))
.provideLayer(zio.ZLayer.succeed(ProfileRoot(context.profileRoot)))
result <- tryAllRepos(repos, context) // 2nd try
} yield result
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/sc4pac/ResolutionContext.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class ResolutionContext(
val repositories: Seq[MetadataRepository],
val cache: FileCache,
val logger: Logger,
val scopeRoot: os.Path
val profileRoot: os.Path
) {

object coursierApi {
Expand Down
34 changes: 17 additions & 17 deletions src/main/scala/sc4pac/Sc4pac.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,23 @@ import sc4pac.Resolution.{Dep, DepModule, DepAsset}

// TODO Use `Runtime#reportFatal` or `Runtime.setReportFatal` to log fatal errors like stack overflow

class Sc4pac(val repositories: Seq[MetadataRepository], val cache: FileCache, val tempRoot: os.Path, val logger: Logger, val scopeRoot: os.Path) extends UpdateService { // TODO defaults
class Sc4pac(val repositories: Seq[MetadataRepository], val cache: FileCache, val tempRoot: os.Path, val logger: Logger, val profileRoot: os.Path) extends UpdateService { // TODO defaults

given context: ResolutionContext = new ResolutionContext(repositories, cache, logger, scopeRoot)
given context: ResolutionContext = new ResolutionContext(repositories, cache, logger, profileRoot)

import CoursierZio.* // implicit coursier-zio interop

// TODO check resolution.conflicts

private def modifyExplicitModules[R](modify: Seq[BareModule] => ZIO[R, Throwable, Seq[BareModule]]): ZIO[R, Throwable, Seq[BareModule]] = {
for {
pluginsData <- JsonIo.read[JD.Plugins](JD.Plugins.path(scopeRoot)) // at this point, file should already exist
pluginsData <- JsonIo.read[JD.Plugins](JD.Plugins.path(profileRoot)) // at this point, file should already exist
modsOrig = pluginsData.explicit
modsNext <- modify(modsOrig)
_ <- ZIO.unless(modsNext == modsOrig) {
val pluginsDataNext = pluginsData.copy(explicit = modsNext)
// we do not check whether file was modified as this entire operation is synchronous and fast, in most cases
JsonIo.write(JD.Plugins.path(scopeRoot), pluginsDataNext, None)(ZIO.succeed(()))
JsonIo.write(JD.Plugins.path(profileRoot), pluginsDataNext, None)(ZIO.succeed(()))
}
} yield modsNext
}
Expand Down Expand Up @@ -191,7 +191,7 @@ trait UpdateService { this: Sc4pac =>
fallbackFilename,
tempPluginsRoot / pkgFolder,
recipe,
Some(Extractor.JarExtraction.fromUrl(art.url, cache, jarsRoot = jarsRoot, scopeRoot = scopeRoot)),
Some(Extractor.JarExtraction.fromUrl(art.url, cache, jarsRoot = jarsRoot, profileRoot = profileRoot)),
hints = depAsset.archiveType,
stagingRoot)
// TODO catch IOExceptions
Expand Down Expand Up @@ -263,7 +263,7 @@ trait UpdateService { this: Sc4pac =>
}

/** Update all installed packages from modules (the list of explicitly added packages). */
def update(modules: Seq[BareModule], globalVariant0: Variant, pluginsRoot: os.Path): RIO[ScopeRoot & Prompter, Boolean] = {
def update(modules: Seq[BareModule], globalVariant0: Variant, pluginsRoot: os.Path): RIO[ProfileRoot & Prompter, Boolean] = {

// - before starting to remove anything, we download and extract everything
// to install into temp folders (staging)
Expand Down Expand Up @@ -342,7 +342,7 @@ trait UpdateService { this: Sc4pac =>
// - remove old packages
// - move new packages into plugins folder
// - write json database and release lock
val task = JsonIo.write(JD.PluginsLock.path(scopeRoot), pluginsLockData.updateTo(plan, staged.files.toMap), Some(pluginsLockData)) {
val task = JsonIo.write(JD.PluginsLock.path(profileRoot), pluginsLockData.updateTo(plan, staged.files.toMap), Some(pluginsLockData)) {
for {
_ <- remove(plan.toRemove, pluginsLockData.installed, pluginsRoot)
// .catchAll(???) // TODO catch exceptions
Expand Down Expand Up @@ -403,8 +403,8 @@ trait UpdateService { this: Sc4pac =>
}

def storeGlobalVariant(globalVariant: Variant): Task[Unit] = for {
pluginsData <- JsonIo.read[JD.Plugins](JD.Plugins.path(scopeRoot)) // json file should exist already
_ <- JsonIo.write(JD.Plugins.path(scopeRoot), pluginsData.copy(config = pluginsData.config.copy(variant = globalVariant)), None)(ZIO.succeed(()))
pluginsData <- JsonIo.read[JD.Plugins](JD.Plugins.path(profileRoot)) // json file should exist already
_ <- JsonIo.write(JD.Plugins.path(profileRoot), pluginsData.copy(config = pluginsData.config.copy(variant = globalVariant)), None)(ZIO.succeed(()))
} yield ()

// TODO catch coursier.error.ResolutionError$CantDownloadModule (e.g. when json files have syntax issues)
Expand Down Expand Up @@ -471,7 +471,7 @@ object Sc4pac {
case class StageResult(tempPluginsRoot: os.Path, files: Seq[(DepModule, Seq[os.SubPath])], stagingRoot: os.Path)


private def fetchChannelData(repoUri: java.net.URI, cache: FileCache, channelContentsTtl: scala.concurrent.duration.Duration): ZIO[ScopeRoot, ErrStr, MetadataRepository] = {
private def fetchChannelData(repoUri: java.net.URI, cache: FileCache, channelContentsTtl: scala.concurrent.duration.Duration): ZIO[ProfileRoot, ErrStr, MetadataRepository] = {
import CoursierZio.* // implicit coursier-zio interop
val contentsUrl = MetadataRepository.channelContentsUrl(repoUri).toString
val artifact = Artifact(contentsUrl).withChanging(true) // changing as the remote file is updated whenever any remote package is added or updated
Expand All @@ -481,8 +481,8 @@ object Sc4pac {
.file(artifact) // requires initialized logger
.run.absolve
.mapError { case e @ (_: coursier.cache.ArtifactError | scala.util.control.NonFatal(_)) => e.getMessage }
scopeRoot <- ZIO.service[ScopeRoot]
repo <- MetadataRepository.create(os.Path(channelContentsFile: java.io.File, scopeRoot.path), repoUri)
profileRoot <- ZIO.service[ProfileRoot]
repo <- MetadataRepository.create(os.Path(channelContentsFile: java.io.File, profileRoot.path), repoUri)
} yield repo
}

Expand All @@ -494,8 +494,8 @@ object Sc4pac {
} yield result
}

private[sc4pac] def initializeRepositories(repoUris: Seq[java.net.URI], cache: FileCache, channelContentsTtl: scala.concurrent.duration.Duration): RIO[ScopeRoot, Seq[MetadataRepository]] = {
val task: RIO[ScopeRoot, Seq[MetadataRepository]] = ZIO.collectPar(repoUris) { url =>
private[sc4pac] def initializeRepositories(repoUris: Seq[java.net.URI], cache: FileCache, channelContentsTtl: scala.concurrent.duration.Duration): RIO[ProfileRoot, Seq[MetadataRepository]] = {
val task: RIO[ProfileRoot, Seq[MetadataRepository]] = ZIO.collectPar(repoUris) { url =>
fetchChannelData(url, cache, channelContentsTtl)
.mapError((err: ErrStr) => { System.err.println(s"Failed to read channel data: $err"); None })
}.filterOrFail(_.nonEmpty)(error.NoChannelsAvailable("No channels available", repoUris.toString))
Expand All @@ -505,7 +505,7 @@ object Sc4pac {
wrapService(cache.logger.using(_), task) // properly initializes logger (avoids Uninitialized TermDisplay)
}

def init(config: JD.Config): RIO[ScopeRoot & Logger, Sc4pac] = {
def init(config: JD.Config): RIO[ProfileRoot & Logger, Sc4pac] = {
import CoursierZio.* // implicit coursier-zio interop
// val refreshLogger = coursier.cache.loggers.RefreshLogger.create(System.err) // TODO System.err seems to cause less collisions between refreshing progress and ordinary log messages
val coursierPool = coursier.cache.internal.ThreadUtil.fixedThreadPool(size = 2) // limit parallel downloads to 2 (ST rejects too many connections)
Expand All @@ -517,8 +517,8 @@ object Sc4pac {
// .withCachePolicies(Seq(coursier.cache.CachePolicy.ForceDownload)) // TODO cache policy
repos <- initializeRepositories(config.channels, cache, Constants.channelContentsTtl) // 30 minutes
tempRoot <- config.tempRootAbs
scopeRoot <- ZIO.service[ScopeRoot]
} yield Sc4pac(repos, cache, tempRoot, logger, scopeRoot.path)
profileRoot <- ZIO.service[ProfileRoot]
} yield Sc4pac(repos, cache, tempRoot, logger, profileRoot.path)
}

def parseModules(modules: Seq[String]): Either[ErrStr, Seq[BareModule]] = {
Expand Down
Loading

0 comments on commit 672e2cb

Please sign in to comment.