Skip to content

Instantly share code, notes, and snippets.

@kxbmap
Created December 12, 2010 18:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kxbmap/738232 to your computer and use it in GitHub Desktop.
Save kxbmap/738232 to your computer and use it in GitHub Desktop.
#!/bin/sh
exec scala -deprecation `cygpath -m $0` "$@"
!#
object CreateProject {
import scala.util.control.Exception._
def usage() {
println("create_project <name> <pkg> [--scala-version <version>] " +
"[--android] " +
"[--api-level <level>] [--platform <name>] [--activity <name>] " +
"[--idea] " +
"[--project-jdk-name <name>] [--project-output-path <path>] " +
"[--java-language-level <level>] [--exclude-sbt-project-definition-module] " +
"[--exclude-libmanaged-folders] [--compile-with-idea] " +
"[--gitignore]")
exit(1)
}
// sbt build.properties settings
val sbtVersion = "0.7.4"
val projectVersion = "0.1"
val defScalaVersion = "2.7.7"
val projectInitialize = false
// android
val androidPlatformPrefix = "android-"
val defaultApiLevel = 7
case class ProjectConfig(
name: String,
target: String,
pkg: String,
scalaVersion: String = """\d\.\d\.\d""".r.findFirstIn(
scala.util.Properties.versionString) getOrElse defScalaVersion)
case class AndroidConfig(
apiLevel: Int = defaultApiLevel,
platform: String = androidPlatformPrefix + defaultApiLevel,
activity: String = "MainActivity")
case class IdeaConfig(
projectJdkName:String = "1.6",
projectOutputPath:Option[String] = None,
javaLanguageLevel: String = "JDK_1_6",
includeSbtProjectDefinitionModule: Boolean = true,
excludeLibManagedFolders: Boolean = false,
compileWithIdea: Boolean = false)
case class GitConfig(gitIgnore: Boolean = true)
case class Config(
project: ProjectConfig,
android: Option[AndroidConfig] = None,
idea: Option[IdeaConfig] = None,
git: Option[GitConfig] = None)
def config(i: Iterator[String]) = {
@annotation.tailrec
def cfg(i: Iterator[String], c: Config = null): Option[Config] =
if (c == null) {
if (!i.hasNext) None else {
val name = i.next()
if ((name matches """[a-zA-Z]\w+""" tap { b => if(!b) println("invalid project name: "+name) }) && i.hasNext){
val pkg = i.next()
if (pkg matches """[a-z]+\.([a-z]+\.?)+[a-z]$""") cfg(i, Config(ProjectConfig(name, name, pkg)))
else None tap { _ => println("invalid package name, need 2 package components (e.g. com.bar)") }
} else None
}
} else {
if (!i.hasNext) Some(c) else {
def setAndroid(c: Config)(f: AndroidConfig => AndroidConfig):Config =
c.copy(android = Some(f(c.android getOrElse AndroidConfig())))
def setIdea(c: Config)(f: IdeaConfig => IdeaConfig): Config =
c.copy(idea = Some(f(c.idea getOrElse IdeaConfig())))
def setGit(c: Config)(f: GitConfig => GitConfig): Config =
c.copy(git = Some(f(c.git getOrElse GitConfig())))
i.next() match {
case "--scala-version" =>
if (!i.hasNext) None else cfg(i, c.copy(project = c.project.copy(scalaVersion = i.next())))
case "--android" =>
cfg(i, setAndroid(c){ ac => ac })
case "--api-level" =>
if (!i.hasNext) None else {
catching(classOf[NumberFormatException]) opt i.next().toInt match {
case Some(level) => cfg(i, setAndroid(c) {
_.copy(apiLevel = level, platform = androidPlatformPrefix + level) })
case None => None
}
}
case "--platform" =>
if (!i.hasNext) None else cfg(i, setAndroid(c) { _.copy(platform = i.next()) })
case "--activity" =>
if (!i.hasNext) None else cfg(i, setAndroid(c) { _.copy(activity = i.next()) })
case "--idea" =>
cfg(i, setIdea(c){ ic => ic })
case "--project-jdk-name" =>
if (!i.hasNext) None else cfg(i, setIdea(c) { _.copy(projectJdkName = i.next()) })
case "--project-output-path" =>
if (!i.hasNext) None else {
val path = i.next()
// TODO check path
cfg(i, setIdea(c) { _.copy(projectOutputPath = Some(path)) })
}
case "--java-language-level" =>
if (!i.hasNext) None else cfg(i, setIdea(c) { _.copy(javaLanguageLevel = i.next()) })
case "--exclude-sbt-project-definition-module" =>
cfg(i, setIdea(c) { _.copy(includeSbtProjectDefinitionModule = false) })
case "--exclude-libmanaged-folders" =>
cfg(i, setIdea(c) { _.copy(excludeLibManagedFolders = true) })
case "--compile-with-idea" =>
cfg(i, setIdea(c) { _.copy(compileWithIdea = true) })
case "--gitignore" =>
cfg(i, setGit(c) { _.copy(gitIgnore = true) })
case _ => None
}
}
}
cfg(i)
}
def generate(config: Config) {
import scala.collection.mutable.ListBuffer
import config.{project => pc, android => aco, idea => ico, git => gco}
val withAndroid = aco match {
case Some(_) => true
case None => false
}
val withIdea = ico match {
case Some(_) => true
case None => false
}
val buildProperties = """
|project.name=%s
|sbt.version=%s
|project.version=%s
|def.scala.version=%s
|build.scala.versions=%s
|project.initialize=%b
|""".stripMargin.trim format (pc.name, sbtVersion, projectVersion, defScalaVersion,
pc.scalaVersion, projectInitialize) // tap { println("---------"); println }
val pluginDef = ("""
|import sbt._
|
|class Plugins(info: ProjectInfo) extends PluginDefinition(info) {""" +
(if (withAndroid) """
| val android = "org.scala-tools.sbt" % "sbt-android-plugin" % "0.5.0"
|""" else "") +
(if (withIdea) """
| val sbtIdeaRepo = "sbt-idea-repo" at "http://mpeltonen.github.com/maven/"
| val sbtIdea = "com.github.mpeltonen" % "sbt-idea-plugin" % "0.1.0"
|""" else "") + "}").stripMargin.trim // tap { println("---------"); println }
val parentMix = {
val buf = new ListBuffer[String]
buf += "ParentProject(info)"
if (withIdea) buf += "IdeaProject"
buf.toList
} mkString ("extends ", " with ", "")
val mainMix = {
val buf = new ListBuffer[String]
buf += (if (withAndroid) "AndroidProject(info)" else "DefaultProject(info)")
if (withAndroid) {
buf += "Defaults"
buf += "MarketPublish"
buf += "TypedResources"
}
if (withIdea) buf += "IdeaProject"
buf.toList
} mkString ("extends ", " with ", "")
val testMix = {
val buf = new ListBuffer[String]
buf += (if (withAndroid) "AndroidTestProject(info)" else "DefaultProject(info)")
if (withAndroid) buf += "Defaults"
if (withIdea) buf += "IdeaProject"
buf.toList
} mkString ("extends ", " with ", "")
val projectDef = ("""
|import sbt._
|""" + (if (withAndroid) ("""
|trait Defaults extends AndroidProject {
| def androidPlatformName = "%s"
| def androidPlatformToolsPath = androidSdkPath / "platform-tools"
| override def adbPath = androidPlatformToolsPath / adbName
|}
|""" format aco.get.platform) else "") + ("""
|class %s(info: ProjectInfo) %s {
| override def shouldCheckOutputDirectories = false
| override def updateAction = task { None }
|
| lazy val main = project(".", "%s", new MainProject(_))
| lazy val tests = project("tests", "tests", new TestProject(_), main)
|""" format (pc.name.capitalize, parentMix, pc.name)) + ("""
| class MainProject(info: ProjectInfo) %s {""" format mainMix) + (if (withAndroid) ("""
| val keyalias = "change-me"
|""") else "") + ("""
| val scalatest = "org.scalatest" % "scalatest" % "1.0" % "test"
| }
|""") + ("""
| class TestProject(info: ProjectInfo) %s
|}
|""" format testMix)).stripMargin.trim // tap { println("---------"); println }
val specDef = """
|import %s
|import org.scalatest.matchers.ShouldMatchers
|import org.scalatest.Spec
|
|class Specs extends Spec with ShouldMatchers {
| describe("a spec") {
| it("should do something") {
| }
| }
|}""".stripMargin.trim format pc.pkg // tap { println("---------"); println }
class AndroidDef(ac: AndroidConfig) {
val manifestXml =
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package={pc.pkg}
android:versionCode="1"
android:versionName="0.1">
<uses-sdk android:minSdkVersion={ac.apiLevel.toString}/>
<application android:label="@string/app_name" android:icon="@drawable/app_icon">
<activity android:name={"."+ac.activity} android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
val testManifestXml =
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package={pc.pkg + ".tests"}>
<uses-sdk android:minSdkVersion={ac.apiLevel.toString}/>
<application>
<uses-library android:name="android.test.runner"/>
</application>
<instrumentation android:name="android.test.InstrumentationTestRunner"
android:targetPackage={pc.pkg}
android:label="Tests"/>
</manifest>
val activityDef = ("""
|package %s
|
|import _root_.android.app.Activity
|import _root_.android.os.Bundle
|import _root_.android.widget.TextView
|
|class %s extends Activity """ + (if (withAndroid) "with TypedActivity" else "") + """{
| override def onCreate(savedInstanceState: Bundle) {
| super.onCreate(savedInstanceState)
| setContentView(new TextView(this) {
| setText("hello, world")
| })
| }
|}""").stripMargin.trim format (pc.pkg, ac.activity) // tap { println("---------"); println }
val testDef = """
|package %s.tests
|
|import junit.framework.Assert._
|import _root_.android.test.AndroidTestCase
|
|class UnitTests extends AndroidTestCase {
| def testPackageIsCorrect {
| assertEquals("%s", getContext.getPackageName)
| }
|}""".stripMargin.trim format (pc.pkg, pc.pkg) // tap { println("---------"); println }
}
val androidDef = for (ac <- aco) yield new AndroidDef(ac)
class IdeaDef(ic: IdeaConfig) {
val ideaPropertiesDef = (("""
|project.jdk.name=%s""" format ic.projectJdkName) +
(ic.projectOutputPath match {
case Some(outputPath) => """
|project.output.path=%s""" format outputPath
case None => ""
}) + """
|java.language.level=%s
|include.sbt.project.definition.module=%b
|exclude.libmanaged.folders=%b
|compile.with.idea=%b
|""" format (
ic.javaLanguageLevel,
ic.includeSbtProjectDefinitionModule,
ic.excludeLibManagedFolders,
ic.compileWithIdea)
).stripMargin.trim // tap { println("---------"); println }
}
val ideaDef = for (ic <- ico) yield new IdeaDef(ic)
class GitDef(gc: GitConfig) {
val gitIgnoreDef = ("""
|/project/boot/
|target/
|lib_managed/
|src_managed/
|test-output/""" + (if (withIdea) """
|/.idea/
|*.iml
|""" else """"
|""")).stripMargin.trim // tap { println("---------"); println }
}
val gitDef = for (gc <- gco) yield new GitDef(gc)
class Generator {
import java.io.File
import scala.xml.Node
val Project = new File(pc.target, "project/build/" + pc.name.capitalize + ".scala")
val Plugins = new File(pc.target, "project/plugins/Plugins.scala")
val BuildProperties = new File(pc.target, "project/build.properties")
val Spec = new File(pc.target, "src/test/scala/Specs.scala")
// android
val Resources = List("drawable", "layout", "values", "xml")
val Manifest = new File(pc.target, "src/main/AndroidManifest.xml")
val TestManifest = new File(pc.target, "tests/src/main/AndroidManifest.xml")
val Strings = new File(pc.target, "src/main/res/values/strings.xml")
val Test = new File(pc.target, "tests/src/main/scala/UnitTests.scala")
val Activity = new File(pc.target, "src/main/scala/Activity.scala")
val AppIcon = new File(pc.target, "src/main/res/drawable/app_icon.png")
// idea
val IdeaProperties = new File(pc.target, "project/idea.properties")
// git
val GitIgnore = new File(pc.target, ".gitignore")
val dirs2create = {
val dirs = List(
"src" :: "main" :: "scala" :: Nil,
"src" :: "test" :: "scala" :: Nil,
"project" :: "plugins" :: Nil,
"project" :: "build" :: Nil
)
if (!withAndroid) dirs
else dirs ++ List(
"src" :: "main" :: "java" :: Nil,
"src" :: "main" :: "assets" :: Nil
) ++ Resources.map("src" :: "main" :: "res" :: _ :: Nil)
} // tap { println("-- dirs2create ----"); _ foreach println }
val testdirs2create = {
val dirs = List(
"tests" :: "src" :: "main" :: "scala" :: Nil
)
if (!withAndroid) dirs
else dirs ++ List(
"tests" :: "src" :: "main" :: "res" :: Nil,
"tests" :: "src" :: "main" :: "assets" :: Nil
)
} // tap { println("-- testdirs2create ----"); _ foreach println }
def resources(m: Map[String,String]) = {
val strings = for ((name,value) <- m) yield { <string name={name}>{value}</string> }
<resources>{strings}</resources>
}
def fail(s: String) = throw new RuntimeException(s)
def pretty(n: Node) = new scala.xml.PrettyPrinter(100, 4).format(n)
def writeFile(f: File, s: String) {
val w = new java.io.FileWriter(f)
try {
w.write(s)
} finally {
w.close()
}
}
def createDirs() {
if (new File(pc.target).exists) fail("target directory " + pc.target + " already exists")
implicit def stringSeq2File(s: Seq[String]):File = s.foldLeft[File](new File(".")) { (f,p) => new File(f, p) }
def mkdir(f: File) = if (!f.mkdirs()) fail("could not create directory "+f.getPath)
(dirs2create ++ testdirs2create) foreach { d=> mkdir(pc.target :: d) }
}
def createIcon() { createIcon(AppIcon, 48, 48) }
def createIcon(file: File, width: Int, height:Int) {
val bi = new java.awt.image.BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_ARGB)
val g = bi.createGraphics()
val gradient = new java.awt.GradientPaint(0, 0, java.awt.Color.red, width, height, java.awt.Color.blue)
g.setPaint(gradient)
g.fill(new java.awt.geom.RoundRectangle2D.Double(0, 0, width, height, 10, 10))
//g.setPaint(java.awt.Color.black)
//g.drawString(config.project, 10, height / 2)
javax.imageio.ImageIO.write(bi, "PNG", file)
}
def createManifests(manifestXml: Node, testManifestXml:Node) {
writeFile(Manifest, pretty(manifestXml))
writeFile(TestManifest, pretty(testManifestXml))
}
def createProject(pluginDef:String, projectDef:String, buildProperties:String) {
writeFile(Plugins, pluginDef)
writeFile(Project, projectDef)
writeFile(BuildProperties, buildProperties)
}
def createActivity(activityDef: String) { writeFile(Activity, activityDef) }
def createResources() { writeFile(Strings, pretty(resources(Map("app_name" -> pc.name.capitalize)))) }
def createSpecs(specDef: String) { writeFile(Spec, specDef) }
def createTests(testDef: String) { writeFile(Test, testDef) }
def createIdeaProperties(ideaProperties: String) { writeFile(IdeaProperties, ideaProperties) }
def createGitIgnore(gitIgnoreDef: String) { writeFile(GitIgnore, gitIgnoreDef) }
}
val gen = new Generator
gen.createDirs()
gen.createProject(pluginDef, projectDef, buildProperties)
gen.createSpecs(specDef)
for (ad <- androidDef) {
gen.createManifests(ad.manifestXml, ad.testManifestXml)
gen.createActivity(ad.activityDef)
gen.createTests(ad.testDef)
gen.createIcon()
gen.createResources()
}
for (id <- ideaDef) {
gen.createIdeaProperties(id.ideaPropertiesDef)
}
for (gd <- gitDef) {
gen.createGitIgnore(gd.gitIgnoreDef)
}
}
implicit def any2Tap[A](toTap: A): { def tap(f: (A) => Unit): A } = new {
def tap(f: (A) => Unit): A = {
f(toTap)
toTap
}
}
def run(args: Array[String]) {
config(args.iterator) /* tap { println } */ match {
case Some(c) =>
catching(classOf[RuntimeException], classOf[java.io.IOException]) either generate(c) match {
case Left(e) => println(e.getMessage)
case _ => println("generated project in directory '"+c.project.target+"'")
}
case None => usage()
}
}
def main(args: Array[String]) { run(args) }
}
CreateProject.run(args)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment