Scenario:
I now have the following setup, which is also available at GitHub: https://github.com/ComFreek/sbt-multi-project-question
| - .git
|
| - projectA
| | - src
| | - build.sbt
|
| - projectB (Git submodule)
| | - src
| | | - build.sbt
| | | - project
| | | - project.sbt
| | | - ...
In projectA/build.sbt
tried:
unmanagedBase := baseDirectory.value / ".." / "projectB" / "deploy" / "lib"
lazy val projectB = RootProject(file("../projectB/src/project"))
lazy val projectA = Project(id = "projectA", base = file(".")).settings(
name := "projectA",
version := "0.1",
scalaVersion := "2.12.8",
scalacOptions in ThisBuild ++= Seq("-unchecked", "-deprecation")
).dependsOn(projectB)
However, it seems that projectB/src/build.sbt
uses unmanaged libraries put into projectB/deploy/lib
which cannot be found when sbt compile
is run from within the scope of projectA - even with the unmanagedBase
property set.
Concretely, you can reproduce it as follows:
projectA
compile
and get
[IJ]sbt:projectA> compile
[info] Compiling 13 Scala sources to ...\sbt-multi-project-question\projectB\src\project\target\scala-2.12\classes ...
[error] ...\sbt-multi-project-question\projectB\src\project\File.scala:19:11: object tools is not a member of package scala
[error] scala.tools.nsc.io.File(f.toString).appendAll(strings:_*)
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\Utils.scala:31:14: object Keys is not a member of package sbt
[error] import sbt.Keys.packageBin
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\Utils.scala:33:36: not found: value Def
[error] def deployPackage(name: String): Def.Initialize[Task[Unit]] =
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\Utils.scala:34:5: not found: value packageBin
[error] packageBin in Compile map {jar => deployTo(Utils.deploy / name)(jar)}
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\Utils.scala:34:19: not found: value Compile
[error] packageBin in Compile map {jar => deployTo(Utils.deploy / name)(jar)}
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\Utils.scala:45:39: type File is not a member of package sbt
[error] def deployTo(target: File)(jar: sbt.File): Unit = {
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\Utils.scala:39:36: not found: value Def
[error] def deployMathHub(target: File): Def.Initialize[Task[Unit]] =
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\Utils.scala:40:5: not found: value packageBin
[error] packageBin in Compile map {jar => deployTo(target)(jar)}
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\Utils.scala:40:19: not found: value Compile
[error] packageBin in Compile map {jar => deployTo(target)(jar)}
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\Utils.scala:123:25: not found: type Logger
[error] def delRecursive(log: Logger, path: File): Unit = {
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:8:44: not found: type Project
[error] case class VersionSpecificProject(project: Project, excludes: Exclusions) {
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:13:48: not found: type Project
[error] def aggregate(projects: ProjectReference*) : Project = project.aggregate(excludes(projects.toList) :_*)
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:13:27: not found: type ProjectReference
[error] def aggregate(projects: ProjectReference*) : Project = project.aggregate(excludes(projects.toList) :_*)
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:17:48: not found: type Project
[error] def dependsOn(projects: ProjectReference*) : Project = {
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:17:27: not found: type ProjectReference
[error] def dependsOn(projects: ProjectReference*) : Project = {
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:18:47: not found: type ClasspathDep
[error] def toClassPathDep(p: ProjectReference) : ClasspathDep[ProjectReference] = p
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:18:27: not found: type ProjectReference
[error] def toClassPathDep(p: ProjectReference) : ClasspathDep[ProjectReference] = p
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:23:59: not found: type Project
[error] def aggregatesAndDepends(projects: ProjectReference*) : Project = {
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:23:38: not found: type ProjectReference
[error] def aggregatesAndDepends(projects: ProjectReference*) : Project = {
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:24:47: not found: type ClasspathDep
[error] def toClassPathDep(p: ProjectReference) : ClasspathDep[ProjectReference] = p
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:24:27: not found: type ProjectReference
[error] def toClassPathDep(p: ProjectReference) : ClasspathDep[ProjectReference] = p
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:30:37: not found: type Project
[error] implicit def fromProject(project: Project) : VersionSpecificProject = VersionSpecificProject(project, Exclusions())
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:34:28: not found: type ProjectReference
[error] case class Exclusions(lst: ProjectReference*) {
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:31:61: not found: type Project
[error] implicit def toProject(vProject: VersionSpecificProject): Project = vProject.project
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:35:68: not found: type ProjectReference
[error] private def javaVersion(versions: List[String], exclusions: List[ProjectReference]) : Exclusions = {
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:43:22: not found: type ProjectReference
[error] def :::(lst2: List[ProjectReference]) = Exclusions(lst.toList ::: lst2 : _*)
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:39:25: not found: type ProjectReference
[error] def java7(exclusions: ProjectReference*): Exclusions = javaVersion(List("1.7", "7"), exclusions.toList)
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:40:25: not found: type ProjectReference
[error] def java8(exclusions: ProjectReference*): Exclusions = javaVersion(List("1.8", "8"), exclusions.toList)
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:41:25: not found: type ProjectReference
[error] def java9(exclusions: ProjectReference*): Exclusions = javaVersion(List("1.9", "9"), exclusions.toList)
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:46:18: not found: value ScopeFilter
[error] def toFilter : ScopeFilter.ProjectFilter = {
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:47:5: not found: value inAnyProject
[error] inAnyProject -- inProjects(lst :_*)
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:47:21: not found: value inProjects
[error] inAnyProject -- inProjects(lst :_*)
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:51:28: not found: type ProjectReference
[error] private def equals(left: ProjectReference, right: ProjectReference) : Boolean = {
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:51:53: not found: type ProjectReference
[error] private def equals(left: ProjectReference, right: ProjectReference) : Boolean = {
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:61:25: not found: type ProjectReference
[error] def excludes(project: ProjectReference) : Boolean = lst.exists(equals(_, project))
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:62:54: not found: type ProjectReference
[error] def apply(projects: List[ProjectReference]) : List[ProjectReference] = projects.filterNot(this.excludes)
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:62:28: not found: type ProjectReference
[error] def apply(projects: List[ProjectReference]) : List[ProjectReference] = projects.filterNot(this.excludes)
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:64:17: not found: type ProjectReference
[error] def map[B](f: ProjectReference => B) : Seq[B] = lst.map(f)
[error] ^
[error] ...\sbt-multi-project-question\projectB\src\project\VersionSpecificProject.scala:65:21: not found: type ProjectReference
[error] def foreach[U](f: ProjectReference => U) : Exclusions = {lst.foreach[U](f); this }
[error] ^
[error] 39 errors found
[error] (ProjectRef(uri("file:/.../sbt-multi-project-question/projectB/src/project/"), "project") / Compile / compileIncremental) Compilation failed
[error] Total time: 5 s, completed 04.03.2019, 10:10:34
[IJ]sbt:projectA>
However, the following works:
projectB/src
.compile
Research. There are some resources explaining how to share unmanaged libraries between subprojects (e.g. 1 and 2 below), but none them seem to face the issue that the build setup (not only the code!) also depends on those unmanaged libraries.
The libraryDependencies key Most of the time, you can simply list your dependencies in the setting libraryDependencies . It's also possible to write a Maven POM file or Ivy configuration file to externally configure your dependencies, and have sbt use those external configuration files.
If you have JAR files (unmanaged dependencies) that you want to use in your project, simply copy them to the lib folder in the root directory of your SBT project, and SBT will find them automatically.
When you uses Simple Build Tool Command 'sbt package', it creates a jar file that includes the class files from your source code and also the content from your src/main/resources folder. Your project dependencies (JAR files in your project's lib folder or managed dependencies declared in build. sbt).
clean Deletes all generated files (in the target directory). compile Compiles the main sources (in src/main/scala and src/main/java directories). test Compiles and runs all tests. console Starts the Scala interpreter with a classpath including the compiled sources and all dependencies.
I managed to compile the projects with the following changes.
First, I got compilation errors when trying to compile the projectB @ b558245 revision, which is referenced in your GitHub project. I checked the latest tag, v15.0.0, and it compiled.
Second, in projectA/build.sbt
, projectB
should be defined as
lazy val projectB = RootProject(file("../projectB/src"))
../projectB/src/project
is the builder project that builds projectB. It requires things in scala compiler and sbt, and thus produced the errors you saw when you tried to compile it directly.
Third, projectB
failed to compile as a dependency of projectA
. That's because, the Utils
object (in projectB/src/project/Utils.scala
) which was intended to refer to the layout of projectB
, was initialized with a wrong directory (../projectA
)
object Utils {
/** MMT root directory */
val root = File("..").canonical // Got the wrong directory when compiled with projectA
So, we have to do some modifications to Utils.scala
and it dependents, to ensure it always find the right position of projectB
. Here is the patch to projectB
to make it compile from projectA
. Might not be the best solution, but it works on my laptop.
diff --git a/src/build.sbt b/src/build.sbt
index 059ef9c8e..b7495c8eb 100644
--- a/src/build.sbt
+++ b/src/build.sbt
@@ -1,10 +1,13 @@
import scala.io.Source
import sbt.Keys._
+import Utils.utils
+
+utils in ThisBuild := Utils((baseDirectory in src).value)
// =================================
// META-DATA and Versioning
// =================================
-version in ThisBuild := {Source.fromFile("mmt-api/resources/versioning/system.txt").getLines.mkString.trim}
+version in ThisBuild := {Source.fromFile(baseDirectory.value / "mmt-api/resources/versioning/system.txt").getLines.mkString.trim}
val now = {
import java.text.SimpleDateFormat
@@ -106,7 +109,7 @@ def mmtProjectsSettings(nameStr: String) = commonSettings(nameStr) ++ Seq(
unmanagedBase := baseDirectory.value / "lib",
- publishTo := Some(Resolver.file("file", Utils.deploy.toJava / " main")),
+ publishTo := Some(Resolver.file("file", utils.value.deploy.toJava / " main")),
install := {},
deploy := Utils.deployPackage("main/" + nameStr + ".jar").value
@@ -153,8 +156,12 @@ lazy val mmt = (project in file("mmt")).
settings(
exportJars := false,
publish := {},
- deploy := {
- assembly in Compile map Utils.deployTo(Utils.deploy / "mmt.jar")
+ deploy := Def.taskDyn {
+ val jar = (assembly in Compile).value
+ val u = utils.value
+ Def.task {
+ Utils.deployTo(u.deploy / "mmt.jar")(jar)
+ }
}.value,
assemblyExcludedJars in assembly := {
val cp = (fullClasspath in assembly).value
@@ -172,13 +179,13 @@ lazy val mmt = (project in file("mmt")).
// MMT is split into multiple subprojects to that are managed independently.
-val apiJars = Seq(
+def apiJars(u: Utils) = Seq(
"scala-compiler.jar",
"scala-reflect.jar",
"scala-parser-combinators.jar",
"scala-xml.jar",
"xz.jar",
-).map(Utils.lib.toJava / _ )
+).map(u.lib.toJava / _ )
// The kernel upon which everything else depends. Maintainer: Florian
lazy val api = (project in file("mmt-api")).
@@ -188,8 +195,8 @@ lazy val api = (project in file("mmt-api")).
settings(
scalacOptions in Compile ++= Seq("-language:existentials"),
scalaSource in Compile := baseDirectory.value / "src" / "main",
- unmanagedJars in Compile ++= apiJars,
- unmanagedJars in Test ++= apiJars,
+ unmanagedJars in Compile ++= apiJars(utils.value),
+ unmanagedJars in Test ++= apiJars(utils.value),
)
@@ -226,7 +233,7 @@ lazy val jedit = (project in file("jEdit-mmt")).
resourceDirectory in Compile := baseDirectory.value / "src/resources",
unmanagedJars in Compile ++= jeditJars map (baseDirectory.value / "lib" / _),
deploy := Utils.deployPackage("main/MMTPlugin.jar").value,
- install := Utils.installJEditJars
+ install := utils.value.installJEditJars
)
// MMT IntelliJ-Plugin. Maintainer: Dennis
@@ -299,7 +306,7 @@ lazy val concepts = (project in file("concept-browser")).
libraryDependencies ++= Seq(
"org.ccil.cowan.tagsoup" % "tagsoup" % "1.2"
),
- unmanagedJars in Compile += Utils.lib.toJava / "scala-xml.jar"
+ unmanagedJars in Compile += utils.value.lib.toJava / "scala-xml.jar"
)
// =================================
@@ -389,7 +396,7 @@ lazy val oeis = (project in file("mmt-oeis")).
dependsOn(planetary).
settings(mmtProjectsSettings("mmt-oeis"): _*).
settings(
- unmanagedJars in Compile += Utils.lib.toJava / "scala-parser-combinators.jar"
+ unmanagedJars in Compile += utils.value.lib.toJava / "scala-parser-combinators.jar"
)
// =================================
@@ -416,11 +423,15 @@ lazy val lfcatalog = (project in file("lfcatalog")).
settings(commonSettings("lfcatalog")).
settings(
scalaSource in Compile := baseDirectory.value / "src",
- publishTo := Some(Resolver.file("file", Utils.deploy.toJava / " main")),
- deployLFCatalog := {
- assembly in Compile map Utils.deployTo(Utils.deploy / "lfcatalog" / "lfcatalog.jar")
+ publishTo := Some(Resolver.file("file", utils.value.deploy.toJava / " main")),
+ deployLFCatalog := Def.taskDyn {
+ val jar = (assembly in Compile).value
+ val u = utils.value
+ Def.task {
+ Utils.deployTo(u.deploy / "lfcatalog" / "lfcatalog.jar")(jar)
+ }
}.value,
- unmanagedJars in Compile += Utils.lib.toJava / "scala-xml.jar"
+ unmanagedJars in Compile += utils.value.lib.toJava / "scala-xml.jar"
)
// =================================
diff --git a/src/project/Utils.scala b/src/project/Utils.scala
index 2f9b94fd4..8f862666e 100644
--- a/src/project/Utils.scala
+++ b/src/project/Utils.scala
@@ -1,9 +1,84 @@
import java.nio.file.Files
import java.nio.file.StandardCopyOption._
+import sbt.Keys.packageBin
+import sbt._
object Utils {
+
+ val utils = settingKey[Utils]("Utils")
+
+ def apply(base: java.io.File) = new Utils(File(base))
+
+ def error(s: String) = throw new Exception(s)
+
+ // ************************************************** deploy-specific code (see also the TaskKey's deploy and deployFull)
+
+ /*
+ * copies files to deploy folder
+ */
+ def deployTo(target: File)(jar: sbt.File): Unit = {
+ Files.copy(jar.toPath, target.toPath, REPLACE_EXISTING)
+ println("copied file: " + jar)
+ println("to file: " + target)
+ }
+
+ /**
+ * packages the compiled binaries and copies to deploy
+ */
+ def deployPackage(name: String) : Def.Initialize[Task[Unit]] = Def.taskDyn {
+ val j = (packageBin in Compile).value
+ val u = utils.value
+ Def.task {
+ deployTo(u.deploy / name)(j)
+ }
+ }
+
+ /**
+ * packages the compiled binaries and copies to deploy
+ */
+ def deployMathHub(target: File): Def.Initialize[Task[Unit]] =
+ packageBin in Compile map {jar => deployTo(target)(jar)}
+
+ // ************************************************** file system utilities
+
+ /** copy a file */
+ def copy(from: File, to: File) {
+ println(s"copying $from to $to")
+ if (!from.exists) {
+ error(s"error: file $from not found (when trying to copy it to $to)")
+ } else if (!to.exists || from.lastModified > to.lastModified) {
+ Files.copy(from.toPath, to.toPath, REPLACE_EXISTING)
+ } else {
+ println("skipped (up-to-date)")
+ }
+ println("\n")
+ }
+
+ /**
+ * Recursively deletes a given folder
+ * @param log
+ * @param path
+ */
+ def delRecursive(log: Logger, path: File): Unit = {
+ def delRecursive(path: File): Unit = {
+ path.listFiles foreach { f =>
+ if (f.isDirectory) delRecursive(f)
+ else {
+ f.delete()
+ log.debug("deleted file: " + path)
+ }
+ }
+ path.delete()
+ log.debug("deleted directory: " + path)
+ }
+ if (path.exists && path.isDirectory) delRecursive(path)
+ else log.warn("ignoring missing directory: " + path)
+ }
+}
+
+class Utils(base: File) {
/** MMT root directory */
- val root = File("..").canonical
+ val root = (base / "..").canonical
/** source folder */
val src = root / "src"
/** MMT deploy directory */
@@ -21,33 +96,6 @@ object Utils {
/** executes a shell command (in the src folder) */
def runscript(command: String) = sys.process.Process(Seq(command), src.getAbsoluteFile).!!
- def error(s: String) = throw new Exception(s)
-
- // ************************************************** deploy-specific code (see also the TaskKey's deploy and deployFull)
-
- /**
- * packages the compiled binaries and copies to deploy
- */
- import sbt.Keys.packageBin
- import sbt._
- def deployPackage(name: String): Def.Initialize[Task[Unit]] =
- packageBin in Compile map {jar => deployTo(Utils.deploy / name)(jar)}
-
- /**
- * packages the compiled binaries and copies to deploy
- */
- def deployMathHub(target: File): Def.Initialize[Task[Unit]] =
- packageBin in Compile map {jar => deployTo(target)(jar)}
-
- /*
- * copies files to deploy folder
- */
- def deployTo(target: File)(jar: sbt.File): Unit = {
- Files.copy(jar.toPath, target.toPath, REPLACE_EXISTING)
- println("copied file: " + jar)
- println("to file: " + target)
- }
-
// ************************************************** MathHub-specific code
@@ -79,7 +127,7 @@ object Utils {
settings.get(killJEdit).foreach {x => runscript(x)}
Thread.sleep(500)
val fname = settings.get(jeditSettingsFolder).getOrElse {
- error(s"cannot copy jars because there is no setting '$jeditSettingsFolder' in $settingsFile")
+ Utils.error(s"cannot copy jars because there is no setting '$jeditSettingsFolder' in $settingsFile")
return
}
val jsf = File(fname) / "jars"
@@ -92,47 +140,10 @@ object Utils {
}
/** copy all jEdit jars to a directory */
def copyJEditJars(to: File) {
- copy(deploy/"mmt.jar", to/"MMTPlugin.jar")
+ Utils.copy(deploy/"mmt.jar", to/"MMTPlugin.jar")
// all other jars are bundled with the above
// val jEditDeps = List("scala-library.jar","scala-parser-combinators.jar","scala-reflect.jar","scala-xml.jar","tiscaf.jar")
// jEditDeps.foreach {f => copy(deploy/"lib"/f, to/f)}
// copy(deploy/"lfcatalog"/"lfcatalog.jar", to/"lfcatalog.jar")
}
-
-
- // ************************************************** file system utilities
-
- /** copy a file */
- def copy(from: File, to: File) {
- println(s"copying $from to $to")
- if (!from.exists) {
- error(s"error: file $from not found (when trying to copy it to $to)")
- } else if (!to.exists || from.lastModified > to.lastModified) {
- Files.copy(from.toPath, to.toPath, REPLACE_EXISTING)
- } else {
- println("skipped (up-to-date)")
- }
- println("\n")
- }
-
- /**
- * Recursively deletes a given folder
- * @param log
- * @param path
- */
- def delRecursive(log: Logger, path: File): Unit = {
- def delRecursive(path: File): Unit = {
- path.listFiles foreach { f =>
- if (f.isDirectory) delRecursive(f)
- else {
- f.delete()
- log.debug("deleted file: " + path)
- }
- }
- path.delete()
- log.debug("deleted directory: " + path)
- }
- if (path.exists && path.isDirectory) delRecursive(path)
- else log.warn("ignoring missing directory: " + path)
- }
}
diff --git a/src/project/build.properties b/src/project/build.properties
index 9f782f704..1fc4b8093 100644
--- a/src/project/build.properties
+++ b/src/project/build.properties
@@ -1 +1 @@
-sbt.version=1.1.1
\ No newline at end of file
+sbt.version=1.2.8
\ No newline at end of file
diff --git a/src/travis.sbt b/src/travis.sbt
index 88fb446d3..a71be9d2e 100644
--- a/src/travis.sbt
+++ b/src/travis.sbt
@@ -2,6 +2,7 @@ import sbt._
import sbt.Keys._
import travis.Matrix._
import travis.Config._
+import Utils.utils
import scala.io.Source
@@ -86,11 +87,11 @@ travisConfig := {
val genTravisYML = taskKey[Unit]("Print out travis.yml configuration")
genTravisYML := {
// read the prefix and the config
- val prefix = Source.fromFile(Utils.src / "project" / "prefix.travis.yml").getLines.filter(!_.startsWith("##")).mkString("\n")
+ val prefix = Source.fromFile(utils.value.src / "project" / "prefix.travis.yml").getLines.filter(!_.startsWith("##")).mkString("\n")
val config = travisConfig.value.serialize
// and write it into .travis.yml
- val outFile = Utils.root / ".travis.yml"
+ val outFile = utils.value.root / ".travis.yml"
IO.write(outFile, prefix + "\n" + config)
streams.value.log.info(s"Wrote $outFile")
}
Ok I think my first answer hit a dead-end;(.
Here a total new idea:
Add your project (projectA) as a submodule of projectB.
This is the structure:
The build.sbt
from projectB
needs a small adjustment:
// added for my project
lazy val projectA = (project in file("projectA")).
dependsOn(api)
If you can handle the git
stuff - this may be the solution you are looking for;).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With