├── .gitignore ├── CHANGELOG ├── LICENSE ├── README.md ├── project ├── build.properties ├── build │ └── StandardProjectPlugin.scala ├── plugins │ └── Plugins.scala └── release.properties └── src └── main ├── resources ├── index.template ├── markdown.template └── parent-index.template └── scala └── com └── twitter └── sbt ├── AdhocInlines.scala ├── ArtifactoryPublisher.scala ├── BuildProperties.scala ├── CorrectDependencies.scala ├── DefaultRepos.scala ├── DependencyChecking.scala ├── EnsimeGenerator.scala ├── Environmentalist.scala ├── FileFilter.scala ├── GitHelpers.scala ├── GithubPublisher.scala ├── InlineDependencies.scala ├── IntegrationSpecs.scala ├── IntransitiveCompiles.scala ├── LibDirClasspath.scala ├── ManagedClasspathFilter.scala ├── NoisyDependencies.scala ├── NullLogger.scala ├── PackageDist.scala ├── PimpedVersion.scala ├── ProjectCache.scala ├── ProjectDependencies.scala ├── ProjectWrapper.scala ├── PublishLocalWithMavenStyleBasePattern.scala ├── PublishSite.scala ├── PublishSourcesAndJavadocs.scala ├── PublishThrift.scala ├── Ramdiskable.scala ├── ReleaseManagement.scala ├── SourceControlledProject.scala ├── StandardProject.scala ├── StrictDependencies.scala ├── SubversionPublisher.scala ├── Tartifactory.scala ├── TemplateProject.scala ├── UnpublishedProject.scala └── Versions.scala /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | lib_managed/ 3 | src_managed/ 4 | project/boot/ 5 | project/plugins/project/ 6 | src/main/scala/com/twitter/sbt/BuildInfo.scala 7 | 8 | /standard-project.tmproj 9 | *.iml 10 | .idea 11 | .classpath 12 | .project 13 | .scala_dependencies 14 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 1.0.3 2 | ----- 3 | release: 21 Feb 2012 4 | 5 | - fix "publish-local" to work with scala-build-specific jars (like "foo_2.9.1") 6 | 7 | 1.0.2 8 | ----- 9 | release: 27 Jan 2012 10 | 11 | - use SBT_PROXY_PUBLISH for publishing to a proxy repo 12 | 13 | 1.0.1 14 | ----- 15 | release: 27 Jan 2012 16 | 17 | - allow publishing to an artifactory repo 18 | 19 | 1.0.0 20 | ----- 21 | release: 24 Sep 2011 22 | 23 | - dont validate config files by default; make it a setting in the project 24 | file to turn on 25 | - expose a way to narrow the set of config files to validate 26 | - allow publish-to-github's remote repo to be overridden 27 | 28 | 0.12.12 29 | ------- 30 | release: 24 Aug 2011 31 | 32 | - fix bug in ProjectDependencies 33 | 34 | 0.12.11 35 | ------- 36 | release: 24 Aug 2011 37 | 38 | - fix bug in PublishLocalWithMavenStyleBasePattern 39 | 40 | 0.12.10 41 | ------- 42 | 43 | - test-project tests relevant parent projects, and ensures the project is actually testable 44 | - added a phase to validate config files during packaging 45 | 46 | 0.12.7 47 | ------ 48 | release: 24 may 2011 49 | 50 | - fixed ivyRepositories in DefaultRepos to honor a proxy repo 51 | 52 | 53 | 0.12.6 54 | ------ 55 | release: 10 may 2011 56 | 57 | - moved generate-ensime into a trait: EnsimeGenerator. 58 | 59 | 60 | 0.12.5 61 | ------ 62 | release: 10 May 2011 63 | 64 | - added a generate-ensime task to StandardProject which will generate a .ensime file for your project. 65 | 66 | 67 | 0.12.4 68 | ------ 69 | release: 9 May 2011 70 | 71 | - Added branch name and last few commit summaries to `build.properties`. 72 | 73 | 74 | 0.12.3 75 | ------ 76 | release: 5 May 2011 77 | 78 | - Source and javadocs are published by default now. 79 | 80 | - Fixed the run classpath to include scala jars. 81 | 82 | - Files in `doc/` will be copied over into the generated site. Any markdown files found in either 83 | `doc/` or `site/` will be compiled into HTML. 84 | 85 | 86 | 0.11.16 87 | ------- 88 | release: 20 Apr 2011 89 | 90 | - added a PublishThrift trait to publish everything in src/main/thrift 91 | with a qualifier of -thrift 92 | 93 | 94 | 0.11.14 95 | ------- 96 | release: 15 Apr 2011 97 | 98 | - build-site supports subprojects/parent projects 99 | 100 | 101 | 0.11.13 102 | ------- 103 | release: 14 apr 2011 104 | 105 | - Updated README to more thoroughly document all our base classes, traits, and general usage 106 | 107 | - Made StandardProjectPlugin extend from StandardProject to pull in all the niceness StandardProject provides 108 | 109 | 110 | 0.11.12 111 | ------- 112 | release: 14 apr 2011 113 | 114 | - don't automatically do "sbt idea" ever 115 | 116 | - don't try to generate API documentation for thrift-generated code 117 | 118 | - pretter generated site, and anything in site/ will now get copied over 119 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # standard-project 2 | 3 | Standard-project is a basic set of extensions to sbt to codify best 4 | practices. 5 | 6 | # General Usage 7 | 8 | ## Getting StandardProject 9 | 10 | To make a plugin available to your project definition you need to 11 | create a file called "Plugins.scala" in the project/plugins directory 12 | of your project. This is pretty straightforward boilerplate. An 13 | example is 14 | 15 | import sbt._ 16 | 17 | class Plugins(info: ProjectInfo) extends PluginDefinition(info) { 18 | val twitterRepo = "twitter-repo" at "http://maven.twttr.com/" 19 | val standardProject = "com.twitter" % "standard-project" % "0.11.16" 20 | } 21 | 22 | ## Extending a StandardProject base 23 | 24 | In general a project will extend either 25 | `StandardServiceProject` (if it's are an application) or 26 | `StandardLibraryProject` (if it's a library). It will then 27 | 28 | * Specify its specific dependencies 29 | * Optionally specify its main class 30 | 31 | A full project specification follows 32 | 33 | import sbt._ 34 | import com.twitter.sbt._ 35 | 36 | class MyProject(info: ProjectInfo) extends StandardServiceProject(info) { 37 | val utilCore = "com.twitter" % "util-core" % "1.2.4" 38 | override def mainClass = Some("com.example.awesome.MyClass") 39 | } 40 | 41 | # Reference 42 | 43 | ## Extensible Classes 44 | 45 | Instead of extending SBT's DefaultProject or DefaultParentProject, 46 | standard-project provides the following project classes you can 47 | extend. 48 | 49 | ### StandardProject 50 | 51 | This extends SBT's DefaultProject, and mixes in the following traits 52 | 53 | * StandardManagedProject 54 | * DependencyChecking 55 | * PublishLocalWithMavenStyleBasePattern 56 | * PublishSourcesAndJavadocs 57 | * BuildProperties 58 | * Ramdiskable 59 | 60 | It sets up an optional ivy cache directory specified by the SBT_CACHE 61 | environment variable, and sets up a resolver to point to `libs/`, 62 | allowing you to stick jars of the form `[artifact]-[revision].jar` in libs. 63 | 64 | The Scala/Java compile order is set to JavaThenScala, and some handy 65 | default compile options are set. 66 | 67 | The test action is overridden to allow disabling by setting the 68 | environment variable NO_TESTS to 1. 69 | 70 | ### StandardParentProject 71 | 72 | Extends SBT's ParentProject and mixes in StandardManagedProject. This 73 | pretty much just lets you have subproject in a StandardProject 74 | setting. 75 | 76 | ### StandardLibraryProject 77 | 78 | Extends StandardProject and mixes in PackageDist. This is intended for 79 | use by libraries, i.e. projects that don't package and distribute an executable. 80 | 81 | ### StandardServiceProject 82 | 83 | Extends StandardProject and mixes in PackageDist. This is intended for 84 | use by "services", i.e. those applications that package and distribute 85 | something with a commonly executed main method 86 | 87 | ## Traits 88 | 89 | ### AdhocInlines 90 | 91 | Allows specifying dependencies that you have the source for, via the 92 | following construct 93 | 94 | val util = "com.twitter" % "util" % "1.1.3" relativePath("util") 95 | 96 | This tells SBT to build util from source when possible, and use the 97 | artifact it puts into target as your dependency. Using adhoc inlines 98 | allows you to more easily make changes across multiple projects. 99 | 100 | In order to use AdhocInlines functionality you must set the 101 | SBT\_ADHOC\_INLINE environment variable. 102 | 103 | ### BuildProperties 104 | 105 | Generates an object that contains your build info, e.g. 106 | 107 | // AUTOGENERATED BY STANDARD-PROJECT. TURN BACK WHILE YOU STILL CAN! 108 | package com.twitter.sbt 109 | object BuildInfo { 110 | val version = "0.11.12-SNAPSHOT" 111 | val date = "2011-04-14" 112 | } 113 | 114 | It will be placed into your projects main package as defined in build.properties 115 | 116 | ### CorrectDependencies 117 | 118 | Enforces stronger maven/ivy dependency checking, and whines if there are 119 | version incompatibilities in the dependency tree. 120 | 121 | ### DefaultRepos 122 | 123 | Sets up a standard set of repositories for your project. It uses the 124 | following environment variables 125 | 126 | * SBT\_PROXY\_REPO - If defined, use the given url as the _only_ 127 | resolver. 128 | * SBT\_OPEN\_TWITTER - If defined, use Twitter's internal open-source 129 | artifactory repo as the _only_ resolver. This is intended for use by 130 | open source projects that can't pull from Twitter's private repo. 131 | * SBT\_TWITTER - If defined, use Twitter's internal artifactory repo. 132 | (deprecated) 133 | 134 | If none of these are set, it falls back to a list of "standard" repos. 135 | 136 | ### DependencyChecking 137 | 138 | Fails the build if your managed libraries directory doesn't exist 139 | (i.e. you haven't run `sbt update`) 140 | 141 | ### EnsimeGenerator 142 | 143 | Adds an action to generate a .ensime file for the project. Should 144 | really be a processor. 145 | 146 | ### Environmentalist 147 | 148 | Sets up a Map representation of the current environment in the val 149 | "environment" 150 | 151 | ### FileFilter 152 | 153 | Simple token replacement for source files. To replace all instances of 154 | "@foo@" with "bar" in source and write it to destination, do 155 | 156 | filter(source, destination, Map("foo" -> "bar") 157 | 158 | ### GitHelpers 159 | 160 | Adds several utilities to tag/commit/check the current git tree 161 | 162 | ### GithubPublisher 163 | 164 | Support for publishing artifacts to github. 165 | 166 | ### InlineDependencies 167 | 168 | Predecessor of AdhocInlines??? Used by AdhocInlines??? 169 | 170 | ### IntegrationSpecs 171 | 172 | Adds an integration-test action that will run all specs that end with 173 | "IntegrationSpec". Also excludes all IntegrationSpecs from the regular 174 | test actian. 175 | 176 | ### LibDirClasspath 177 | 178 | Adds all jar files in "lib" to the classpath. Semi-deprecated? 179 | 180 | ### ManagedClasspathFilter 181 | 182 | Scary stuff I don't understand very well. 183 | 184 | ### NoisyDependencies 185 | 186 | Make SBT bitch more about version mismatches? 187 | 188 | ### PackageDist 189 | 190 | Adds a package-dist action that wraps up scala stuff into our standard 191 | zip layout for deploys. 192 | 193 | ### ProjectCache 194 | 195 | Used to avoid multiple instantiation of projects across 196 | inline/birdcage builds. 197 | 198 | ### ProjectDependencies 199 | 200 | Predecessor to AdhocInlines? 201 | 202 | ### PublishLocalWithMavenStyleBasePattern 203 | 204 | Force publish-local to lay things out maven style. There were problems 205 | in the past with publish-local'd ivy style artifacts not picking up 206 | transitive dependencies. Bludgeoning SBT into maven style for this 207 | action seemed to fix it. 208 | 209 | ### PublishSite 210 | 211 | Builds a dope website including a processed README.md, anything in 212 | your src/site directory, and your generated javadoc. 213 | 214 | Also adds a task to publish said site to a git repo. 215 | 216 | ### PublishSourcesAndJavadocs 217 | 218 | make publish and publish-local build/package/publish a -javadoc.jar 219 | and -sources.jar. Makes IDE users happy. 220 | 221 | ### Ramdiskable 222 | 223 | Provides the capability to compile to a ramdisk instead of the regular 224 | "target" subdirectory. If the environment `SBT\_RAMDISK\_ROOT` is set, a 225 | target-ramdisk directory will be softlinked to `SBT\_RAMDISK\_ROOT` and 226 | will be used as the output path. 227 | 228 | ### ReleaseManagement 229 | 230 | A helper for bumping versions and publishing artifacts. If you're 231 | releasing stable versions you should mix this in. If you mix it in you 232 | should _only_ use this to bump versions in build.properties. It's 233 | better that way. 234 | 235 | ### SourceControlledProject 236 | 237 | Support for getting git shas into currentRevision 238 | 239 | ### StandardManagedProject 240 | 241 | Mixes in 242 | 243 | * SourceControlledProject 244 | * ReleaseManagement 245 | * Versions 246 | * Environmentalist 247 | 248 | Also disables cross compiling, sets managed style to maven and clears 249 | your local repo list 250 | 251 | ### StrictDependencies 252 | 253 | Have Ivy resolve conflicts by failing the build. 254 | 255 | ### SubversionPublisher 256 | 257 | Publish to an SVN repository (which presumably then has some magic in 258 | place to get artifacts published to an actual publc repo). Typically 259 | used by overriding subversionRepository, e.g. 260 | 261 | override def subversionRepository = Some("http://svn.me.com/repo") 262 | 263 | ### ArtifactoryPublisher 264 | 265 | publish to a standard ibiblio resolver. This has odd interactions with 266 | SubversionPublisher for historical reasons. Be careful: you _must_ mix 267 | this in _after_ SubversionPublisher if you use both. 268 | 269 | There are two additional environment variables that control the actual 270 | resolver used to publish. 271 | 272 | 1. if SBT\_PROXY\_PUBLISH is set and proxyPublishRepo is defined, proxyPublishRepo is used 273 | 2. if SBT\_PROXY\_PUBLISH\_REPO is set, it is used as the root URL to publish to 274 | 275 | Credentials are read from ~/.artifactory-credentials, and look like 276 | the following 277 | {code} 278 | realm=Artifactory Realm 279 | host= 280 | user= 281 | password= 282 | {code} 283 | 284 | #### Settings of interest 285 | * proxyPublishRepo: Option[String] - the _base_ url of the repo to 286 | publish to. 287 | * proxyQualifier: String - used to build up proxyRepoPublishTarget 288 | * proxySnapshotOrRelease: String - used to define the resolver type as 289 | well as build up proxyRepoPublishtarget 290 | * proxyRepoPublishTarget: String - the uri within proxyPublishRepo to publish 291 | to. Defaults to 292 | {code} 293 | "libs-%ss-%s".format(proxySnapshotOrRelease, proxyQualifier) 294 | {code} 295 | * proxyPublish: Boolean - whether or not to use the proxy to publish. 296 | Defaults to 297 | {code} 298 | environment.get("SBT_CI").isDefined 299 | {code} 300 | * repositories: Seq[Resolver] - adds the proxy resolver to 301 | repositories if it's defined 302 | 303 | ### TartifactoryRepos 304 | 305 | Sort of deprecated, more or less does what DefaultRepos does now. Use 306 | DefaultRepos instead. 307 | 308 | ### TartifactoryPublisher 309 | 310 | Deprecated. See ArtifactoryPublisher. 311 | 312 | Publish to an artifactory instance. You'll need to enter credentials 313 | at the command line for each publish, or override publishtask 314 | yourself. 315 | 316 | ### TemplateProject 317 | 318 | Allow preprocessing of source files using fmpp? 319 | 320 | ### UnpublishedProject 321 | 322 | Overrides publish/deliver to do nothing 323 | 324 | ### Versions 325 | 326 | Adds tasks for bumping the major, minor and patch versions of a 327 | project. 328 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | #Project properties 2 | #Tue Feb 21 10:32:06 PST 2012 3 | project.organization=com.twitter 4 | project.name=standard-project 5 | sbt.version=0.7.4 6 | project.version=1.0.4-SNAPSHOT 7 | build.scala.versions=2.7.7 8 | project.initialize=false 9 | -------------------------------------------------------------------------------- /project/build/StandardProjectPlugin.scala: -------------------------------------------------------------------------------- 1 | import java.io.{File, FileWriter} 2 | import java.text.SimpleDateFormat 3 | import java.util.Date 4 | import com.twitter.sbt._ 5 | import _root_.sbt._ 6 | 7 | class StandardProjectPlugin(info: ProjectInfo) extends PluginProject(info) 8 | with SubversionPublisher 9 | with IdeaProject 10 | with TartifactoryRepos 11 | with PublishSite 12 | with ReleaseManagement { 13 | override def disableCrossPaths = true 14 | 15 | override def subversionRepository = Some("https://svn.twitter.biz/maven-public") 16 | 17 | val ivySvn = "ivysvn" % "ivysvn" % "2.1.0" from "http://maven.twttr.com/ivysvn/ivysvn/2.1.0/ivysvn-2.1.0.jar" 18 | 19 | val markdown = "org.markdownj" % "markdownj" % "0.3.0-1.0.2b4" 20 | val scalate = "org.freemarker" % "freemarker" % "2.3.16" 21 | 22 | override def managedStyle = ManagedStyle.Maven 23 | 24 | override def pomExtra = 25 | 26 | 27 | Apache 2 28 | http://www.apache.org/licenses/LICENSE-2.0.txt 29 | repo 30 | 31 | 32 | 33 | // oh man. 34 | override def compileAction = super.compileAction dependsOn(makeBanner) 35 | 36 | lazy val makeBanner = task { 37 | val timestamp = new SimpleDateFormat("yyyy-MM-dd").format(new Date) 38 | val fileWriter = new FileWriter(new File("src/main/scala/com/twitter/sbt/BuildInfo.scala")) 39 | fileWriter.write("// AUTOGENERATED BY STANDARD-PROJECT. TURN BACK WHILE YOU STILL CAN!\n") 40 | fileWriter.write("package com.twitter.sbt\n") 41 | fileWriter.write("object BuildInfo {\n") 42 | fileWriter.write(" val version = \"" + version.toString + "\"\n") 43 | fileWriter.write(" val date = \"" + timestamp + "\"\n") 44 | fileWriter.write("}\n") 45 | fileWriter.close() 46 | None 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /project/plugins/Plugins.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | class Plugins(info: ProjectInfo) extends PluginDefinition(info) { 4 | val ivySvn = "ivysvn" % "ivysvn" % "2.1.0" from "http://maven.twttr.com/ivysvn/ivysvn/2.1.0/ivysvn-2.1.0.jar" 5 | 6 | val sbtIdeaRepo = "sbt-idea-repo" at "http://mpeltonen.github.com/maven/" 7 | val sbtIdea = "com.github.mpeltonen" % "sbt-idea-plugin" % "0.3.0" 8 | val twitterRepo = "twitter-public-repo" at "http://maven.twttr.com/" 9 | 10 | val standardProject = "com.twitter" % "standard-project" % "0.11.12" 11 | } 12 | -------------------------------------------------------------------------------- /project/release.properties: -------------------------------------------------------------------------------- 1 | #Automatically generated by ReleaseManagement 2 | #Tue Feb 21 10:32:06 PST 2012 3 | version=1.0.3 4 | sha1=ca4e91bc3a830ed6083b37cdb8c3cef5b98858fc 5 | -------------------------------------------------------------------------------- /src/main/resources/index.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | ${projectName} 4 | 76 | 77 | 78 | 79 |

${projectName}

80 | 81 | 89 | 90 |

91 |


92 |

93 | 94 | <#if readme??> 95 | 96 | ${readme} 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/main/resources/markdown.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 75 | 76 | 77 | 78 | 79 | {{content}} 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/main/resources/parent-index.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | ${projectName} 4 | 76 | 77 | 78 | 79 |

${projectName}

80 | 81 | 92 | 93 |

94 |


95 |

96 | 97 | <#if readme??> 98 | 99 | ${readme} 100 | 101 | 102 | <#if subprojects??> 103 |

Sub Projects

104 | 105 |
    106 | <#list subprojects as subproject> 107 |
  • ${subproject}
  • 108 | 109 |
110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/AdhocInlines.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import scala.collection.jcl 4 | import scala.collection.jcl.Conversions._ 5 | import scala.collection.mutable.{HashSet, HashMap} 6 | 7 | import java.io.File 8 | 9 | import org.apache.ivy.Ivy 10 | import org.apache.ivy.core.module.descriptor.{ModuleDescriptor, DefaultModuleDescriptor} 11 | import org.apache.ivy.core.retrieve.RetrieveOptions 12 | import org.apache.ivy.core.resolve.ResolveOptions 13 | import org.apache.ivy.core.resolve.IvyNode 14 | import org.apache.ivy.core.LogOptions 15 | 16 | // TODO: check versions, display discrepancies, etc. 17 | // TODO: check that it's an actual sbt project there, otherwise skip (xrayspecs) 18 | // TODO: check versions 19 | // TODO: show whole project graph 20 | 21 | import _root_.sbt._ 22 | 23 | 24 | // object RawProjectCache { 25 | // private[this] val cache = new HashMap[String, Project] 26 | // def apply(path: Path)(make: => Project) = cache.getOrElseUpdate(path.absolutePath, make) 27 | // } 28 | 29 | object inline { 30 | // TODO: push as much of this into per-project caches as 31 | // possible. we need the global register to filter the classpath-- 32 | // but not much else? we can determinstically recreate these per 33 | // project. 34 | 35 | sealed abstract class ResolvedLibraryDependency 36 | case class ModuleDependency(m: ModuleID) extends ResolvedLibraryDependency 37 | case class InlineDependency(m: ModuleID, project: Project) extends ResolvedLibraryDependency 38 | 39 | case class ModuleDescriptor(organization: String, name: String) 40 | 41 | val noInlined = new HashSet[ModuleDescriptor] 42 | val inlined = new HashSet[ModuleDescriptor] 43 | } 44 | 45 | trait AdhocInlines extends BasicManagedProject with Environmentalist { 46 | class ProjectCache { 47 | private[this] var cache = new HashMap[String, Project] 48 | 49 | def getStore() = cache 50 | 51 | def setStore(underlying: HashMap[String, Project]) { 52 | cache = underlying 53 | } 54 | 55 | def apply(key: String)(make: => Option[Project]) = { 56 | cache.get(key) match { 57 | case someProject@Some(_) => someProject 58 | case None => 59 | val made = make 60 | made foreach { project => 61 | cache(key) = project 62 | } 63 | 64 | made 65 | } 66 | } 67 | } 68 | 69 | private[this] lazy val relPaths = new HashMap[(String, String), String] 70 | private[this] lazy val unpublishedInlines = new HashSet[(String, String)] 71 | private[this] lazy val explicitDependencies = new HashSet[Dependency] 72 | private[this] lazy val projectCache = new ProjectCache 73 | 74 | def setProjectCacheStore(store: HashMap[String, Project]) { 75 | println("SETTING PROJECT CACHE STORE (%d)".format(store.size)) 76 | // (merge?) 77 | println("OLD ONE (%d)".format(projectCache.getStore.size)) 78 | 79 | projectCache.setStore(store) 80 | subProjects foreach { 81 | case (name, adhoc: AdhocInlines) => 82 | println(":-) %s".format(name)) 83 | adhoc.setProjectCacheStore(store) 84 | case (name, _) => 85 | println(":-(%s)".format(name)) 86 | } 87 | } 88 | 89 | // For future use: 90 | val projectIsInlined = true 91 | 92 | // TODO: make a registry for these changes. 93 | class RichModuleID(m: ModuleID) { 94 | // This is side effecting. Nasty, but it gets the job done. 95 | def relativePath(name: String) = { 96 | relPaths((m.organization, m.name)) = name 97 | m 98 | } 99 | 100 | def withUnpublished() = { 101 | unpublishedInlines += Pair(m.organization, m.name) 102 | m 103 | } 104 | 105 | def noInline() = { 106 | // TODO: should noInlines really be global? 107 | inline.noInlined += inline.ModuleDescriptor(m.organization, m.name) 108 | m 109 | } 110 | } 111 | 112 | case class Dependency(relPath: String, name: String) { 113 | // TODO: do a better search? 114 | def projectPath: Path = Path.fromFile("..") / relPath 115 | 116 | def resolveProject: Option[Project] = { 117 | projectCache("project:%s".format(name)) { 118 | println("PROJECT CACHE MAKE MAKE: %s".format(name)) 119 | val parentProject = 120 | projectCache("path:%s".format(projectPath)) { Some(project(projectPath)) } 121 | val foundProject = parentProject flatMap { parentProject => 122 | if (parentProject.name != name) { 123 | // Try to find it in a subproject. 124 | parentProject.subProjects.find { _._2.name == name } map { _._2 } 125 | } else { 126 | Some(parentProject) 127 | } 128 | } 129 | 130 | foundProject map { wrapProject(_) } 131 | } 132 | } 133 | } 134 | 135 | implicit def moduleIDToRichModuleID(m: ModuleID) = new RichModuleID(m) 136 | implicit def stringToAlmostDependency(relPath: String) = new { 137 | def ~(name: String) = Dependency(relPath, name) 138 | } 139 | 140 | implicit def stringToDependency(relPath: String) = 141 | Dependency(relPath, relPath) 142 | 143 | override def shouldCheckOutputDirectories = false 144 | 145 | def isInlining = environment.get("SBT_ADHOC_INLINE").isDefined 146 | def inlineSearchPath = environment.getOrElse("SBT_ADHOC_INLINE_PATH", "..") 147 | 148 | def dependencies(deps: Dependency*) { 149 | explicitDependencies ++= deps 150 | } 151 | 152 | private def resolveProject(organization: String, name: String, path: Path) = 153 | projectCache("project:%s/%s".format(organization, name)) { 154 | if ((path / "project" / "build.properties").exists) { 155 | val rawProject = projectCache("path:%s".format(path)) { Some(project(path)) } 156 | val foundProject = rawProject flatMap { rawProject => 157 | if (rawProject.name != name) { 158 | // Try to find it in a subproject. 159 | rawProject.subProjects.find(_._2.name == name) map (_._2) 160 | } else { 161 | Some(rawProject) 162 | } 163 | } 164 | 165 | if (!foundProject.isDefined) { 166 | log.warn("project name mismatch @ %s (expected: %s got: %s)".format( 167 | path, name, rawProject.get.name)) 168 | None 169 | } else if (foundProject.get.organization != organization) { 170 | log.warn("project organization mismatch @ %s (expected: %s got: %s)".format( 171 | path, organization, foundProject.get.organization)) 172 | None 173 | } else { 174 | Some(wrapProject(foundProject.get)) 175 | } 176 | } else { 177 | log.warn("invalid sbt project @ %s".format(path)) 178 | None 179 | } 180 | } 181 | 182 | 183 | // println("project cache", info.projectDirectory, ProjectCache) 184 | 185 | // We use ``info.projectDirectory'' instead of ``name'' here because 186 | // for subprojects, names aren't initialized as of this point 187 | // [seemingly not before the constructor has finished running]. 188 | // if (isInlining) { 189 | // log.info("ad-hoc inlines enabled for " + info.projectDirectory) 190 | // } else { 191 | // log.info( 192 | // ("ad-hoc inlines NOT currently enabled " + 193 | // "for %s set SBT_ADHOC_INLINE=1 to enable") 194 | // .format(info.projectDirectory)) 195 | // } 196 | 197 | if (environment.get("SBT_INLINE").isDefined) { 198 | log.error("ad-hoc inlines are incompatible with SBT_INLINE") 199 | System.exit(1) 200 | } 201 | 202 | case class IvyJar(organization: String, name: String, jar: String) 203 | // I'm submitting to this sbt antipattern here. Lean into it. 204 | lazy val ivyJars = { 205 | val source = io.Source.fromFile(new java.io.File(info.projectDirectory, ".ivyjars")) 206 | Map() ++ { source.getLines map { dirtyLine => 207 | val line = dirtyLine.stripLineEnd 208 | // error out on parse error? 209 | val Array(organization, name, jar) = line.split("\t") 210 | (jar -> IvyJar(organization, name, jar)) 211 | }} 212 | } 213 | 214 | def resolvedPaths(relPath: String) = { 215 | inlineSearchPath.split(":").map{ file => Path.fromFile(file) / relPath }.filter(_.isDirectory) 216 | } 217 | 218 | private[this] lazy val resolvedLibraryDependencies = 219 | if (!isInlining) { 220 | super.libraryDependencies map inline.ModuleDependency 221 | } else { 222 | super.libraryDependencies map { module => 223 | val descriptor = inline.ModuleDescriptor(module.organization, module.name) 224 | if (inline.noInlined contains descriptor) { 225 | inline.ModuleDependency(module) 226 | } else { 227 | val relPath = 228 | relPaths.get((module.organization, module.name)).getOrElse(module.name) 229 | 230 | resolvedPaths(relPath).firstOption match { 231 | case Some(path) => 232 | resolveProject(module.organization, module.name, path) match { 233 | case Some(project) => 234 | // XXX: we can't do this test here because of sbt 235 | // architecture idiocy. in the cascade of lazily 236 | // initiated values that have side effects, querying 237 | // ``version'' at this juncture attempts retrieving 238 | // a hitherto undefined property when using 239 | // subprojects (those without their own 240 | // ``build.properties'' files). Sigh. Sigh. Sigh. 241 | 242 | // if (project.version.toString != module.revision) { 243 | // log.warn("version mismatch for %s (%s is inlined, %s is requested)".format( 244 | // module.name, project.version.toString, module.revision)) 245 | // } 246 | 247 | inline.inlined += descriptor 248 | inline.InlineDependency(module, project) 249 | 250 | case None => 251 | inline.ModuleDependency(module) 252 | } 253 | case None => 254 | inline.ModuleDependency(module) 255 | } 256 | } 257 | } 258 | } 259 | 260 | private[this] lazy val moduleDependencies = 261 | resolvedLibraryDependencies.flatMap { 262 | case inline.ModuleDependency(module) => Some(module) 263 | case _ => None 264 | } 265 | 266 | private[this] lazy val inlineDependencies = 267 | resolvedLibraryDependencies.flatMap { 268 | case inline.InlineDependency(module, project) => Some((module, project)) 269 | case _ => None 270 | } 271 | 272 | override def libraryDependencies = Set() ++ moduleDependencies 273 | 274 | lazy val showClasspath = task { 275 | log.info(fullClasspath(Configurations.Compile).toString) 276 | None 277 | } 278 | 279 | lazy val showLibraryDependencies = task { 280 | log.info("Inlined dependencies:") 281 | inlineDependencies foreach { case (m, project) => 282 | log.info(" %s @ %s".format(m, project.info.projectPath)) 283 | } 284 | 285 | log.info("Library dependencies:") 286 | moduleDependencies foreach { m => log.info(" %s".format(m)) } 287 | 288 | log.info("Explicit dependencies:") 289 | explicitDependencies foreach { dep => log.info(" %s".format(dep)) } 290 | 291 | None 292 | } 293 | 294 | lazy val showProjectClosure = task { 295 | log.info("Project closure:") 296 | projectClosure foreach { project => 297 | println(" " + project + " " + project.hashCode) 298 | } 299 | None 300 | } 301 | 302 | private def wrapProject(p: Project) = { 303 | println("WRAPPING PROJECT OF CLASS", p.getClass) 304 | // p.getClass.getDeclaredMethods foreach { method => 305 | // println("METHOD", method) 306 | // } 307 | val m = p.getClass.getDeclaredMethod( 308 | "setProjectCacheStore", classOf[HashMap[String, Project]]) 309 | if (m ne null) { 310 | m.invoke(p, projectCache.getStore) 311 | } else { 312 | println("whining loudly.") 313 | } 314 | 315 | p 316 | } 317 | 318 | 319 | // p match { 320 | // case _: AdhocInlines => p 321 | // case p: DefaultProject => new WrappedDefaultProject(p) with AdhocInlines 322 | // case p => p 323 | // } 324 | 325 | // Use the full set of dependencies (super.libraryDependencies) for 326 | // module updates. 327 | override def inlineSettings = { 328 | val filteredDeps = super.libraryDependencies.filter { m => 329 | !(unpublishedInlines contains (m.organization, m.name)) 330 | } 331 | 332 | new InlineConfiguration(projectID, filteredDeps, ivyXML, ivyConfigurations, 333 | defaultConfiguration, ivyScala, ivyValidate) 334 | } 335 | 336 | private def resolve(logging: UpdateLogging.Value)( 337 | ivy: Ivy, module: DefaultModuleDescriptor, 338 | defaultConf: String) = 339 | { 340 | val resolveOptions = new ResolveOptions 341 | resolveOptions.setLog(ivyLogLevel(logging)) 342 | val resolveReport = ivy.resolve(module, resolveOptions) 343 | if(resolveReport.hasError) { 344 | throw new ResolveException( 345 | resolveReport.getAllProblemMessages.toArray.map(_.toString).toList.removeDuplicates) 346 | } 347 | 348 | val file = new File(info.projectDirectory, ".ivyjars") 349 | val out = new java.io.PrintWriter(new java.io.FileWriter(file)) 350 | 351 | println("RESOLVE") 352 | for (dep <- resolveReport.getDependencies; id = dep.asInstanceOf[IvyNode].getId) { 353 | println("ID %s".format(id)) 354 | } 355 | 356 | for (dep <- resolveReport.getDependencies; id = dep.asInstanceOf[IvyNode].getId) { 357 | out.println("%s\t%s\t%s-%s.jar".format( 358 | id.getOrganisation, id.getName, 359 | id.getName, id.getRevision)) 360 | } 361 | 362 | out.close() 363 | } 364 | 365 | private def update(module: IvySbt#Module, configuration: UpdateConfiguration) { 366 | module.withModule { case (ivy, md, default) => 367 | import configuration._ 368 | resolve(logging)(ivy, md, default) 369 | val retrieveOptions = new RetrieveOptions 370 | retrieveOptions.setSync(synchronize) 371 | 372 | val patternBase = retrieveDirectory.getAbsolutePath 373 | val pattern = 374 | if(patternBase.endsWith(File.separator)) 375 | patternBase + configuration.outputPattern 376 | else 377 | patternBase + File.separatorChar + configuration.outputPattern 378 | 379 | ivy.retrieve(md.getModuleRevisionId, pattern, retrieveOptions) 380 | } 381 | } 382 | 383 | import UpdateLogging.{Quiet, Full, DownloadOnly} 384 | import LogOptions.{LOG_QUIET, LOG_DEFAULT, LOG_DOWNLOAD_ONLY} 385 | private def ivyLogLevel(level: UpdateLogging.Value) = 386 | level match { 387 | case Quiet => LOG_QUIET 388 | case DownloadOnly => LOG_DOWNLOAD_ONLY 389 | case Full => LOG_DEFAULT 390 | } 391 | 392 | override def updateTask(module: => IvySbt#Module, configuration: => UpdateConfiguration) = { 393 | ivyTask { update(module, configuration) } 394 | } 395 | 396 | private lazy val wtf: Boolean = { 397 | explicitDependencies foreach { dep => 398 | // XXX com.twitter 399 | inline.inlined += inline.ModuleDescriptor("com.twitter", dep.name) 400 | } 401 | 402 | super.subProjects foreach { case (_, p) => wrapProject(p) } 403 | 404 | true 405 | } 406 | 407 | override def subProjects = { 408 | require(wtf) 409 | 410 | val mapped = 411 | inlineDependencies map { case (m, project) => 412 | m.name -> project 413 | } 414 | 415 | val explicit = 416 | explicitDependencies map { dep => 417 | val project = dep.resolveProject 418 | if (!project.isDefined) { 419 | log.error("could not find dependency %s".format(dep)) 420 | System.exit(1) 421 | } 422 | 423 | dep.name -> project.get 424 | } 425 | 426 | Map() ++ super.subProjects ++ mapped ++ explicit 427 | } 428 | 429 | override def managedClasspath(config: Configuration): PathFinder = 430 | if (isInlining) { 431 | super.managedClasspath(config) filter { path => 432 | ivyJars.get(path.name) match { 433 | case Some(IvyJar(organization, name, jar)) => 434 | !(inline.inlined contains inline.ModuleDescriptor(organization, name)) 435 | 436 | case None => 437 | // Exclude stuff we don't know about. 438 | // log.warn("%s NOT FOUND".format(path.name)) error out? 439 | false 440 | } 441 | } 442 | } else { 443 | super.managedClasspath(config) 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/ArtifactoryPublisher.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | import _root_.sbt._ 3 | 4 | trait ArtifactoryPublisher extends SubversionPublisher with Environmentalist { 5 | def svnConfiguration: DefaultPublishConfiguration = { 6 | subversionResolver match { 7 | case None => 8 | super.publishConfiguration 9 | case Some(resolver) => 10 | new DefaultPublishConfiguration(resolver.getName(), "release", true) 11 | } 12 | } 13 | 14 | val proxyPublishRepo = environment.get("SBT_PROXY_PUBLISH_REPO") match { 15 | case None => 16 | Some("http://artifactory.local.twitter.com/") 17 | case url => 18 | url 19 | } 20 | 21 | /** 22 | * is this a public or local release? 23 | */ 24 | def proxyQualifier = "local" 25 | 26 | def proxySnapshotOrRelease = { 27 | if (version.toString.endsWith("SNAPSHOT")) { 28 | "snapshot" 29 | } else { 30 | "release" 31 | } 32 | } 33 | 34 | def proxyRepoPublishTarget = { 35 | // extra s is not a typo here. artifactory pluralizes 36 | "libs-%ss-%s".format(proxySnapshotOrRelease, proxyQualifier) 37 | } 38 | 39 | def proxyPublish = environment.get("SBT_PROXY_PUBLISH").isDefined 40 | 41 | override def repositories = { 42 | if (proxyPublish) { 43 | proxyPublishRepo match { 44 | case Some(repo) => { 45 | Credentials(Path.userHome / ".artifactory-credentials", log) 46 | val publishRepo = "%s%s".format(repo, proxyRepoPublishTarget) 47 | log.info("running under CI, will publish to %s".format(publishRepo)) 48 | super.repositories + ("proxy-publish" at publishRepo) 49 | } 50 | case _ => super.repositories 51 | } 52 | } else { 53 | super.repositories 54 | } 55 | } 56 | 57 | override def publishConfiguration: DefaultPublishConfiguration = { 58 | if (proxyPublish) { 59 | proxyPublishRepo match { 60 | case Some(repo) => { 61 | new DefaultPublishConfiguration("proxy-publish", proxySnapshotOrRelease, false) 62 | } 63 | case _ => svnConfiguration 64 | } 65 | } else { 66 | svnConfiguration 67 | } 68 | } 69 | } 70 | 71 | trait ArtifactoryPublicPublisher extends ArtifactoryPublisher { 72 | override def proxyQualifier = "public" 73 | } 74 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/BuildProperties.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | import java.io.FileWriter 5 | import java.util.{Date, Properties} 6 | import java.text.SimpleDateFormat 7 | 8 | trait BuildProperties extends DefaultProject with SourceControlledProject { 9 | def timestamp = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date) 10 | 11 | // make a build.properties file and sneak it into the packaged jar. 12 | def buildPackage = organization + "." + name 13 | def packageResourcesPath = buildPackage.split("\\.").foldLeft(mainResourcesOutputPath ##) { _ / _ } 14 | def buildPropertiesPath = packageResourcesPath / "build.properties" 15 | override def packagePaths = super.packagePaths +++ buildPropertiesPath 16 | 17 | def writeBuildPropertiesTask = task { 18 | packageResourcesPath.asFile.mkdirs() 19 | val buildProperties = new Properties 20 | buildProperties.setProperty("name", name) 21 | buildProperties.setProperty("version", version.toString) 22 | buildProperties.setProperty("build_name", timestamp) 23 | currentRevision.foreach(buildProperties.setProperty("build_revision", _)) 24 | branchName.foreach(buildProperties.setProperty("build_branch_name", _)) 25 | lastFewCommits.foreach(buildProperties.setProperty("build_last_few_commits", _)) 26 | 27 | val fileWriter = new FileWriter(buildPropertiesPath.asFile) 28 | buildProperties.store(fileWriter, "") 29 | fileWriter.close() 30 | None 31 | }.dependsOn(copyResources) 32 | 33 | val WriteBuildPropertiesDescription = "Writes a build.properties file into the target folder." 34 | lazy val writeBuildProperties = writeBuildPropertiesTask dependsOn(copyResources) describedAs WriteBuildPropertiesDescription 35 | 36 | override def packageAction = super.packageAction dependsOn(writeBuildProperties) 37 | } 38 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/CorrectDependencies.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | import org.apache.ivy.plugins._ 5 | 6 | trait CorrectDependencies extends BasicManagedProject { 7 | override def ivySbt: IvySbt = { 8 | val i = super.ivySbt 9 | i.withIvy { apacheIvy => 10 | val stricty = apacheIvy.getSettings().getConflictManager("latest-compatible") 11 | stricty.asInstanceOf[IvySettingsAware].setSettings(apacheIvy.getSettings()) 12 | apacheIvy.getSettings().setDefaultConflictManager(stricty) 13 | } 14 | i 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/DefaultRepos.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import scala.collection.Set 4 | import _root_.sbt._ 5 | 6 | /* 7 | * BasicManagedProject mixes in ReflectiveRepositories which defines "repositories" by 8 | * reflectively collecting all vals that are of type Repository. 9 | */ 10 | trait DefaultRepos extends StandardManagedProject with Environmentalist { 11 | val proxyRepo = environment.get("SBT_PROXY_REPO") match { 12 | case None => 13 | if (environment.get("SBT_OPEN_TWITTER").isDefined) { 14 | // backward compatibility: twitter's internal open source proxy 15 | Some("http://artifactory.local.twitter.com/open-source/") 16 | } else if (environment.get("SBT_TWITTER").isDefined) { 17 | // backward compatibility: twitter's internal proxy 18 | Some("http://artifactory.local.twitter.com/repo/") 19 | } else { 20 | None 21 | } 22 | case url => 23 | url 24 | } 25 | 26 | override def repositories = { 27 | val defaultRepos = List( 28 | "ibiblio" at "http://mirrors.ibiblio.org/pub/mirrors/maven2/", 29 | "twitter.com" at "http://maven.twttr.com/", 30 | "powermock-api" at "http://powermock.googlecode.com/svn/repo/", 31 | "scala-tools.org" at "http://scala-tools.org/repo-releases/", 32 | "testing.scala-tools.org" at "http://scala-tools.org/repo-releases/testing/", 33 | "oauth.net" at "http://oauth.googlecode.com/svn/code/maven", 34 | "download.java.net" at "http://download.java.net/maven/2/", 35 | "atlassian" at "https://m2proxy.atlassian.com/repository/public/", 36 | 37 | // for netty: 38 | "jboss" at "http://repository.jboss.org/nexus/content/groups/public/" 39 | ) 40 | 41 | proxyRepo match { 42 | case Some(url) => 43 | localRepos + ("proxy-repo" at url) 44 | case None => 45 | super.repositories ++ Set(defaultRepos: _*) 46 | } 47 | } 48 | 49 | override def ivyRepositories = { 50 | proxyRepo match { 51 | case Some(url) => 52 | Seq(Resolver.defaultLocal(None)) ++ repositories.toList 53 | case None => 54 | super.ivyRepositories 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/DependencyChecking.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | 5 | trait DependencyChecking extends DefaultProject { 6 | lazy val checkDepsExist = task { checkDepsExistOn(this) } 7 | 8 | protected def checkDepsExistOn(project: BasicManagedProject): Option[String] = { 9 | project.info.parent match { 10 | case Some(parent: BasicManagedProject) => checkDepsExistOn(parent) 11 | case _ => 12 | } 13 | 14 | if (!project.libraryDependencies.isEmpty && !project.managedDependencyRootPath.asFile.exists) { 15 | Some("You must run 'sbt update' first to download dependent jars.") 16 | } else if (!(organization contains ".")) { 17 | Some("Your organization name doesn't look like a valid package name. It needs to be something like 'com.example'.") 18 | } else { 19 | None 20 | } 21 | } 22 | 23 | override def compileAction = super.compileAction dependsOn(checkDepsExist) 24 | } -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/EnsimeGenerator.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | import java.io.{FileWriter, File} 5 | 6 | trait EnsimeGenerator extends DefaultProject { 7 | /** 8 | * A blacklist for dependencies that cause problems. Add the name of 9 | * any dependency that doesn't work correctly with ensime. 10 | * (e.g. "standard-project") 11 | */ 12 | val ensimeBlacklist: List[String] = Nil 13 | 14 | /** 15 | * Generates a .ensime file for the project and its dependencies. 16 | */ 17 | lazy val generateEnsime = task { _ => interactiveTask { 18 | val file = new java.io.File(info.projectDirectory, ".ensime") 19 | val out = new java.io.PrintWriter(new java.io.FileWriter(file)) 20 | 21 | out.println("(") 22 | out.println(":project-package \"com.twitter\"") 23 | out.println(":compile-jars (") 24 | 25 | testClasspath.get foreach { 26 | case path if path.absolutePath.endsWith(".jar") => 27 | out.println(" \"%s\"".format(path.absolutePath)) 28 | case _ => () 29 | } 30 | 31 | out.println(" )") 32 | out.println(":sources (") 33 | 34 | Seq("mainSourceRoots", "testSourceRoots") foreach { methodName => 35 | projectClosure foreach { project => 36 | try { 37 | if (!ensimeBlacklist.contains(project.name) || methodName != "testSourceRoots") { 38 | val m = project.getClass.getMethod(methodName) 39 | val finder = m.invoke(project).asInstanceOf[PathFinder] 40 | finder.get foreach { path => 41 | out.println(" \"%s\"".format(path.absolutePath)) 42 | } 43 | } 44 | } catch { case _ => () } 45 | } 46 | } 47 | out.println(" )") 48 | out.println(")") 49 | out.close() 50 | 51 | None 52 | } dependsOn(compile, testCompile) } describedAs("Generate a .ensime file for all projects together.") 53 | } 54 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/Environmentalist.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import scala.collection.jcl 4 | 5 | trait Environmentalist { 6 | val environment = jcl.Map(System.getenv()) 7 | } 8 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/FileFilter.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | import java.io.{BufferedReader, FileReader, FileWriter, File} 5 | 6 | 7 | object FileFilter { 8 | def filter(sourcePath: Path, destinationPath: Path, filters: Map[String, String]) { 9 | val in = new BufferedReader(new FileReader(sourcePath.asFile)) 10 | val out = new FileWriter(destinationPath.asFile) 11 | var line = in.readLine() 12 | while (line ne null) { 13 | filters.keys.foreach { token => 14 | line = line.replace("@" + token + "@", filters(token)) 15 | } 16 | out.write(line) 17 | out.write("\n") 18 | line = in.readLine() 19 | } 20 | in.close() 21 | out.close() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/GitHelpers.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt.{BasicManagedProject, DefaultProject, ProcessBuilder} 4 | import _root_.sbt.Process._ 5 | import java.io.File 6 | 7 | trait GitHelpers { 8 | private def run(command: ProcessBuilder) = command !! NullLogger 9 | 10 | def gitCommitFiles(message: String, paths: String*) { 11 | val task = paths.map { "git add %s".format(_): ProcessBuilder } reduceLeft { _ ## _ } 12 | run(task ## Seq("git", "commit", "-m", message)) 13 | } 14 | 15 | def gitCommitSavedEnvironment(message: Option[String]) { 16 | run( 17 | "git add project/release.properties" ## 18 | "git add project/build.properties" #&& 19 | Seq("git", "commit", "-m", message.getOrElse("Updating release properties files")) 20 | ) 21 | } 22 | 23 | def gitTag(tag: String) { run(git tag -m {tag} {tag}) } 24 | 25 | def gitIsCleanWorkingTree = run("git status").contains("nothing to commit (working directory clean)") 26 | } 27 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/GithubPublisher.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | import java.io.File 5 | import Process._ 6 | 7 | // use github for publishing (not finished yet) 8 | trait GithubPublisher extends BasicManagedProject { 9 | val githubFolder = new File(Resolver.userIvyRoot, "github") 10 | val githubLocalRepo = Resolver.file("github-local", githubFolder)(Resolver.mavenStylePatterns) mavenStyle() 11 | val git = "git --git-dir=" + githubFolder + "/.git --work-tree=" + githubFolder 12 | 13 | val prepGithubTask = task { 14 | if (!new File(githubFolder, ".git").isDirectory) { 15 | Some("No github folder found! Create it first: git clone git@github.com:twitter/repo.git ~/.ivy2/github") 16 | } else { 17 | val rv = (( 18 | {git} fetch #&& 19 | ({git} branch gh-pages origin/gh-pages #|| "echo Ignore that. We are fine.") #&& 20 | {git} checkout gh-pages #&& 21 | {git} merge origin/gh-pages 22 | )!) 23 | if (rv == 0) None else Some("ERROR: " + rv) 24 | } 25 | } named("prep-github") 26 | def prepGithubAction = prepGithubTask 27 | 28 | val commitGithubTask = task { 29 | val rv = (( 30 | {git} add . #&& 31 | {git} commit -m sbt-commit #&& 32 | {git} push origin gh-pages 33 | )!) 34 | if (rv == 0) None else Some("ERROR: " + rv) 35 | } named("commit-github") 36 | def commitGithubAction = commitGithubTask 37 | 38 | def publishGithubConfiguration = new DefaultPublishConfiguration("github-local", "release", false) 39 | def deliverGithubFolderAction = deliverTask(deliverIvyModule, publishGithubConfiguration, UpdateLogging.DownloadOnly) dependsOn(makePom) named("deliver-github") 40 | def publishGithubFolderAction = publishTask(deliverIvyModule, publishGithubConfiguration) dependsOn(deliverGithubFolderAction) 41 | 42 | def publishGithubAction = prepGithubAction && publishGithubFolderAction && commitGithubAction 43 | lazy val publishGithub: Task = publishGithubAction 44 | 45 | lazy val deliverGithub: Task = deliverGithubFolderAction dependsOn(packageToPublishActions: _*) 46 | } 47 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/InlineDependencies.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import collection.mutable.{HashSet, HashMap, ListBuffer} 4 | import scala.collection.jcl 5 | import _root_.sbt._ 6 | 7 | trait InlineDependencies extends BasicManagedProject { 8 | val inlineEnvironment = jcl.Map(System.getenv()) 9 | val inlinedLibraryDependencies = new HashSet[ModuleID]() 10 | val inlinedSubprojects = new ListBuffer[(String, _root_.sbt.Project)]() 11 | val inlinedModules = new HashMap[String, ModuleID]() 12 | 13 | override def libraryDependencies = { 14 | super.libraryDependencies ++ inlinedLibraryDependencies 15 | } 16 | 17 | override def subProjects = { 18 | Map() ++ super.subProjects ++ inlinedSubprojects 19 | } 20 | 21 | override def shouldCheckOutputDirectories = false 22 | 23 | def inline(m: ModuleID): Unit = inline(m, m.name) 24 | def inline(m: ModuleID, relativePath: String) { 25 | val path = Path.fromFile("../") / relativePath 26 | inlinedModules += (m.name -> m) 27 | if (inlineEnvironment.get("SBT_INLINE").isDefined && path.isDirectory) 28 | inlinedSubprojects += (m.name -> project(path)) 29 | else 30 | inlinedLibraryDependencies += m 31 | } 32 | 33 | // TODO: Can we do this transitively? 34 | lazy val checkInlineVersions = task { 35 | val namedSubprojects = Map() ++ inlinedSubprojects 36 | inlinedModules foreach { case (name, module) => 37 | namedSubprojects.get(name) foreach { subProject => 38 | if (module.revision != subProject.version.toString) { 39 | println("\"%s\" version mismatch: %s is specified but %s is inlined".format( 40 | name, module.revision, subProject.version)) 41 | } 42 | } 43 | } 44 | 45 | None 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/IntegrationSpecs.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | trait IntegrationSpecs extends StandardProject { 4 | val integrationTestSuffix = "IntegrationSpec" 5 | lazy val integrationTestOptions: Seq[TestOption] = 6 | TestListeners(testListeners) :: TestFilter(_ endsWith integrationTestSuffix) :: Nil 7 | lazy val integrationTest = defaultTestTask(integrationTestOptions) 8 | override def testOptions = 9 | super.testOptions ++ Seq(TestFilter(name => !(name endsWith integrationTestSuffix))) 10 | } -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/IntransitiveCompiles.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | /** 4 | * Support for intransitive dependence analysis in SBT. This uses the 5 | * "secret" sbt.intransitive options, plus a hack subclass of 6 | * CompileConditional to override dirty external dependencies. This is 7 | * necessary in order to build subprojects with intransitive 8 | * dependence analysis. 9 | */ 10 | 11 | import _root_.sbt._ 12 | import xsbt.AnalyzingCompiler 13 | import java.io.File 14 | 15 | trait IntransitiveCompiles extends DefaultProject with Environmentalist { 16 | environment.get("SBT_INTRANSITIVE") map { 17 | case "0" | "false" => "false" 18 | case _ => "true" 19 | } foreach { System.setProperty("sbt.intransitive", _) } 20 | 21 | protected def isIntransitive = java.lang.Boolean.getBoolean("sbt.intransitive") 22 | 23 | class IntransitiveCompileConditional( 24 | config: CompileConfiguration, 25 | compiler: AnalyzingCompiler) 26 | extends CompileConditional(config, compiler) 27 | { 28 | // This is the meat of the trick: we set last modified time to 0 29 | // for all external dependencies. 30 | override protected def externalInfo(externals: Iterable[File]) = 31 | externals map { e => (e, ExternalInfo(true, 0L)) } 32 | } 33 | 34 | lazy val intransitiveMainCompileConditional = 35 | new IntransitiveCompileConditional( 36 | mainCompileConfiguration, buildCompiler) 37 | 38 | lazy val intransitiveTestCompileConditional = 39 | new IntransitiveCompileConditional( 40 | testCompileConfiguration, buildCompiler) 41 | 42 | private def defaultMainCompileConditional = 43 | if (isIntransitive) intransitiveMainCompileConditional 44 | else mainCompileConditional 45 | 46 | private def defaultTestCompileConditional = 47 | if (isIntransitive) intransitiveTestCompileConditional 48 | else testCompileConditional 49 | 50 | override def compileAction = task { 51 | defaultMainCompileConditional.run 52 | } .dependsOn(super.compileAction.dependencies: _*) 53 | .describedAs("compile the project [possibly intransitively]") 54 | 55 | override def testCompileAction = task { 56 | defaultTestCompileConditional.run 57 | } .dependsOn(super.testCompileAction.dependencies: _*) 58 | .describedAs("test-compile the project [possibly intransitively]") 59 | 60 | override protected def defaultTestTask(testOptions: => Seq[TestOption]) = 61 | testTask( 62 | testFrameworks, 63 | testClasspath, 64 | defaultTestCompileConditional.analysis, 65 | testOptions).dependsOn(super.defaultTestTask(testOptions).dependencies: _*) 66 | 67 | override def cleanOptions: Seq[CleanOption] = 68 | ClearAnalysis(intransitiveMainCompileConditional.analysis) :: 69 | ClearAnalysis(intransitiveTestCompileConditional.analysis) :: 70 | super.cleanOptions.toList 71 | 72 | lazy val queryTransitive = task { 73 | val onOrOff = if (isIntransitive) "OFF" else "ON" 74 | log.info("transitivity is " + onOrOff) 75 | None 76 | } 77 | 78 | lazy val transitive = task { 79 | System.setProperty("sbt.intransitive", "false") 80 | log.info("project builds are now transitive") 81 | None 82 | } 83 | 84 | lazy val intransitive = task { 85 | System.setProperty("sbt.intransitive", "true") 86 | log.info("project builds are now intransitive") 87 | None 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/LibDirClasspath.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | 5 | trait LibDirClasspath extends StandardProject { 6 | def jarFileFilter: FileFilter = "*.jar" 7 | def libClasspath = descendents("lib", jarFileFilter) 8 | 9 | override def fullUnmanagedClasspath(config: Configuration) = 10 | super.fullUnmanagedClasspath(config) +++ libClasspath 11 | } -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/ManagedClasspathFilter.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | /** 4 | * ManagedClasspathFilter allows filtering of the classpath in terms 5 | * of packages. It maintains this mapping in a ".ivyjars" file in the 6 | * root (sub-)project directory. 7 | */ 8 | 9 | import java.io.File 10 | 11 | import scala.collection.jcl.Conversions._ 12 | 13 | import org.apache.ivy.Ivy 14 | import org.apache.ivy.core.module.descriptor.{ModuleDescriptor, DefaultModuleDescriptor} 15 | import org.apache.ivy.core.retrieve.RetrieveOptions 16 | import org.apache.ivy.core.resolve.ResolveOptions 17 | import org.apache.ivy.core.resolve.IvyNode 18 | import org.apache.ivy.core.LogOptions 19 | import org.apache.ivy.core.IvyPatternHelper 20 | import org.apache.ivy.core.report.ArtifactDownloadReport 21 | import org.apache.ivy.plugins.report.XmlReportParser 22 | import org.apache.ivy.core.resolve.ResolveOptions 23 | 24 | import _root_.sbt._ 25 | 26 | trait ManagedClasspathFilter extends BasicManagedProject { 27 | // I'm submitting to this sbt antipattern here. Lean into it. :-( 28 | lazy val ivyJars: Map[String, ModuleID] = { 29 | val source = io.Source.fromFile(new java.io.File(info.projectDirectory, ".ivyjars")) 30 | Map() ++ { source.getLines map { dirtyLine => 31 | val line = dirtyLine.stripLineEnd 32 | val Array(path, organization, name, revision) = line.split("\t") 33 | (path -> ModuleID(organization, name, revision)) 34 | }} 35 | } 36 | 37 | def filterPathFinderClasspath(finder: PathFinder)(f: (ModuleID => Boolean)): PathFinder = 38 | finder.filter { path => 39 | ivyJars.get(path.absolutePath) match { 40 | case Some(m) => f(m) 41 | case None => 42 | // Exclude stuff we don't know about. 43 | log.warn("ManagedClasspathFilter: %s NOT FOUND in .ivyjars, excluding".format(path)) 44 | false 45 | } 46 | } 47 | 48 | /** 49 | * Ivy resolution / updating. 50 | */ 51 | 52 | private def resolve( 53 | logging: UpdateLogging.Value, 54 | ivy: Ivy, 55 | module: DefaultModuleDescriptor 56 | ) = { 57 | val resolveOptions = new ResolveOptions 58 | resolveOptions.setLog(ivyLogLevel(logging)) 59 | val resolveReport = ivy.resolve(module, resolveOptions) 60 | if (resolveReport.hasError) { 61 | throw new ResolveException( 62 | resolveReport.getAllProblemMessages.toArray.map(_.toString).toList.removeDuplicates) 63 | } 64 | 65 | resolveReport 66 | } 67 | 68 | private def update(module: IvySbt#Module, configuration: UpdateConfiguration) { 69 | module.withModule { case (ivy, md, default) => 70 | import configuration._ 71 | val report = resolve(logging, ivy, md) 72 | val retrieveOptions = new RetrieveOptions 73 | retrieveOptions.setSync(synchronize) 74 | 75 | val patternBase = retrieveDirectory.getAbsolutePath 76 | val pattern = 77 | if (patternBase.endsWith(File.separator)) 78 | patternBase + configuration.outputPattern 79 | else 80 | patternBase + File.separatorChar + configuration.outputPattern 81 | 82 | val mrid = md.getModuleRevisionId 83 | ivy.retrieve(mrid, pattern, retrieveOptions) 84 | 85 | /** 86 | * Report on the retrieve. 87 | */ 88 | 89 | val settings = ivy.getSettings 90 | val cacheManager = settings.getResolutionCacheManager 91 | val configs = md.getConfigurationsNames() 92 | 93 | type ArtifactMap = java.util.Map[ArtifactDownloadReport, java.util.Set[String]] 94 | 95 | val artifactsToCopy = ivy.getRetrieveEngine.determineArtifactsToCopy( 96 | mrid, pattern, retrieveOptions).asInstanceOf[ArtifactMap] 97 | 98 | val file = new File(info.projectDirectory, ".ivyjars") 99 | val out = new java.io.PrintWriter(new java.io.FileWriter(file)) 100 | 101 | artifactsToCopy foreach { case (report, paths) => 102 | val artifact = report.getArtifact 103 | val artifactMrid = artifact.getModuleRevisionId 104 | paths foreach { path => 105 | out.println("%s\t%s\t%s\t%s".format( 106 | path, artifactMrid.getOrganisation, 107 | artifactMrid.getName, artifactMrid.getRevision)) 108 | } 109 | } 110 | 111 | out.close() 112 | } 113 | } 114 | 115 | import UpdateLogging.{Quiet, Full, DownloadOnly} 116 | import LogOptions.{LOG_QUIET, LOG_DEFAULT, LOG_DOWNLOAD_ONLY} 117 | private def ivyLogLevel(level: UpdateLogging.Value) = 118 | level match { 119 | case Quiet => LOG_QUIET 120 | case DownloadOnly => LOG_DOWNLOAD_ONLY 121 | case Full => LOG_DEFAULT 122 | } 123 | 124 | override def updateTask( 125 | module: => IvySbt#Module, 126 | configuration: => UpdateConfiguration 127 | ) = ivyTask { update(module, configuration) } 128 | 129 | lazy val showUpdateModule = task { 130 | updateIvyModule.withModule { case (_, md, _) => 131 | println(md) 132 | } 133 | None 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/NoisyDependencies.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | import org.apache.ivy.plugins._ 5 | import org.apache.ivy.core.resolve._ 6 | import org.apache.ivy.plugins.conflict._ 7 | import org.apache.ivy.core.settings.IvySettings 8 | import java.util.Collection 9 | 10 | class LatestCompatibleWarningsManager extends AbstractConflictManager { 11 | val loose = new LatestConflictManager 12 | val strict = new LatestCompatibleConflictManager 13 | 14 | override def setSettings(ivySettings: IvySettings) = { 15 | loose.setSettings(ivySettings) 16 | strict.setSettings(ivySettings) 17 | super.setSettings(ivySettings) 18 | } 19 | 20 | override def resolveConflicts(parent: IvyNode, conflicts: Collection[_]): Collection[_] = { 21 | try { 22 | strict.resolveConflicts(parent, conflicts) 23 | } catch { 24 | case e: StrictConflictException => { 25 | print("Warning: " + e + "\n") 26 | loose.resolveConflicts(parent, conflicts) 27 | } 28 | } 29 | } 30 | } 31 | 32 | trait NoisyDependencies extends BasicManagedProject { self: BasicDependencyProject => 33 | 34 | override def ivySbt: IvySbt = { 35 | val i = super.ivySbt 36 | i.withIvy { apacheIvy => 37 | val stricty = new LatestCompatibleWarningsManager 38 | stricty.asInstanceOf[IvySettingsAware].setSettings(apacheIvy.getSettings()) 39 | apacheIvy.getSettings().setDefaultConflictManager(stricty) 40 | } 41 | i 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/NullLogger.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | 5 | 6 | object NullLogger extends BasicLogger { 7 | def trace(t: => Throwable) {} 8 | def log(level: Level.Value, message: => String) {} 9 | def logAll(events: Seq[LogEvent]) {} 10 | def success(message: => String) {} 11 | def control(event: ControlEvent.Value, message: => String) {} 12 | } 13 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/PackageDist.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | import scala.collection.Set 5 | import java.io.File 6 | 7 | trait PackageDist extends DefaultProject with SourceControlledProject with Environmentalist { 8 | // override me for releases! 9 | def releaseBuild = false 10 | 11 | // workaround bug in sbt that hides scala-compiler. 12 | override def filterScalaJars = false 13 | 14 | private[this] def paths(f: BasicScalaProject => PathFinder) = 15 | Path.lazyPathFinder { 16 | topologicalSort flatMap { 17 | case sp: BasicScalaProject => f(sp).get 18 | case _ => Nil 19 | } 20 | } 21 | 22 | // build the executable jar's classpath. 23 | // (why is it necessary to explicitly remove the target/{classes,resources} paths? hm.) 24 | def dependentJars = { 25 | val jars = ( 26 | jarsOfProjectDependencies 27 | +++ runClasspath 28 | +++ mainDependencies.scalaJars 29 | --- paths(_.mainCompilePath) 30 | --- paths(_.mainResourcesOutputPath) 31 | ) 32 | 33 | if (jars.get.find { jar => jar.name.startsWith("scala-library-") }.isDefined) { 34 | // workaround bug in sbt: if the compiler is explicitly included, don't include 2 versions 35 | // of the library. 36 | jars --- jars.filter { jar => 37 | jar.absolutePath.contains("/boot/") && jar.name == "scala-library.jar" 38 | } 39 | } else { 40 | jars 41 | } 42 | } 43 | 44 | def dependentJarNames = dependentJars.getFiles.map(_.getName).filter(_.endsWith(".jar")) 45 | override def manifestClassPath = Some(dependentJarNames.map { "libs/" + _ }.mkString(" ")) 46 | 47 | def distName = if (releaseBuild) (name + "-" + version) else name 48 | def distPath = "dist" / distName ## 49 | 50 | def configPath = "config" ## 51 | def configOutputPath = distPath / "config" 52 | 53 | def scriptsPath = "src" / "scripts" ## 54 | def scriptsOutputPath = distPath / "scripts" 55 | 56 | def distZipName = { 57 | val revName = currentRevision.map(_.substring(0, 8)).getOrElse(version) 58 | "%s-%s.zip".format(name, if (releaseBuild) version else revName) 59 | } 60 | 61 | // copy scripts. 62 | val CopyScriptsDescription = "Copies scripts into the dist folder." 63 | val copyScripts = task { 64 | val rev = currentRevision.getOrElse("") 65 | val filters = Map( 66 | "CLASSPATH" -> (publicClasspath +++ mainDependencies.scalaJars).getPaths.mkString(":"), 67 | "TEST_CLASSPATH" -> testClasspath.getPaths.mkString(":"), 68 | "DIST_CLASSPATH" -> (dependentJarNames.map { "${DIST_HOME}/libs/" + _ }.mkString(":") + 69 | ":${DIST_HOME}/" + defaultJarName), 70 | "DIST_NAME" -> name, 71 | "VERSION" -> version.toString, 72 | "REVISION" -> rev 73 | ) 74 | 75 | scriptsOutputPath.asFile.mkdirs() 76 | (scriptsPath ***).filter { !_.isDirectory }.get.foreach { path => 77 | val dest = Path.fromString(scriptsOutputPath, path.relativePath) 78 | new File(dest.absolutePath.toString).getParentFile().mkdirs() 79 | FileFilter.filter(path, dest, filters) 80 | Runtime.getRuntime().exec(List("chmod", "+x", dest.absolutePath.toString).toArray).waitFor() 81 | } 82 | None 83 | } named("copy-scripts") dependsOn(`compile`) describedAs CopyScriptsDescription 84 | 85 | val allConfigFiles: Set[Path] = (configPath ** "*.scala").filter { !_.isDirectory }.get 86 | 87 | // the set of config files to validate during package-dist. 88 | // to validate everything in config/, set validateConfigFilesSet = allConfigFiles. 89 | def validateConfigFilesSet: Set[Path] = Set() 90 | 91 | // run --validate on requested config files to make sure they compile. 92 | def validateConfigFilesTask = task { 93 | if (environment.get("NO_VALIDATE").isDefined) { 94 | None 95 | } else { 96 | val distJar = (distPath / (jarPath.name)).absolutePath 97 | validateConfigFilesSet.map { path => 98 | val cmd = Array("java", "-jar", distJar, "-f", path.absolutePath, "--validate") 99 | val exitCode = Process(cmd).run().exitValue() 100 | if (exitCode == 0) { 101 | None 102 | } else { 103 | Some("Failed to validate " + path.toString) 104 | } 105 | }.foldLeft[Option[String]](None) { _ orElse _ } 106 | } 107 | } describedAs("Validate any config files.") 108 | lazy val validateConfigFiles = validateConfigFilesTask 109 | 110 | /** 111 | * copy into dist: 112 | * - packaged jar 113 | * - pom file for export 114 | * - dependent libs 115 | * - config files 116 | * - scripts 117 | */ 118 | def packageDistTask = interactiveTask { 119 | distPath.asFile.mkdirs() 120 | (distPath / "libs").asFile.mkdirs() 121 | configOutputPath.asFile.mkdirs() 122 | 123 | FileUtilities.copyFlat(List(jarPath), distPath, log).left.toOption orElse 124 | FileUtilities.copyFlat(dependentJars.get, distPath / "libs", log).left.toOption orElse 125 | FileUtilities.copy((configPath ***).get, configOutputPath, log).left.toOption orElse 126 | FileUtilities.copy(((outputPath ##) ** "*.pom").get, distPath, log).left.toOption orElse 127 | FileUtilities.zip((("dist" / distName) ##).get, "dist" / distZipName, true, log) 128 | } 129 | 130 | val PackageDistDescription = "Creates a deployable zip file with dependencies, config, and scripts." 131 | lazy val packageDist = packageDistTask.dependsOn(`package`, makePom, copyScripts).describedAs(PackageDistDescription) && validateConfigFilesTask 132 | 133 | val cleanDist = cleanTask("dist" ##) describedAs("Erase any packaged distributions.") 134 | override def cleanAction = super.cleanAction dependsOn(cleanDist) 135 | } 136 | 137 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/PimpedVersion.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt.{Version, BasicVersion} 4 | 5 | object pimpedversion { 6 | class PimpedVersion(wrapped: BasicVersion) { 7 | private def increment(i: Option[Int]) = Some(i.getOrElse(0) + 1) 8 | 9 | // these methods do the wrong thing in BasicVersion. :( 10 | def incMicro() = BasicVersion(wrapped.major, wrapped.minor.orElse(Some(0)), increment(wrapped.micro), wrapped.extra) 11 | def incMinor() = BasicVersion(wrapped.major, increment(wrapped.minor), wrapped.micro.map { _ => 0 }, wrapped.extra) 12 | def incMajor() = BasicVersion(wrapped.major + 1, wrapped.minor.map { _ => 0 }, wrapped.micro.map { _ => 0 }, wrapped.extra) 13 | 14 | def stripSnapshot() = { 15 | val stripped = wrapped.extra.map(_.replaceAll("""-?SNAPSHOT""", "")).flatMap { s => 16 | if ( s.length > 0 ) Some(s) else None 17 | } 18 | 19 | BasicVersion(wrapped.major, wrapped.minor, wrapped.micro, stripped) 20 | } 21 | 22 | def addSnapshot() = { 23 | val unstripped = stripSnapshot().extra.map( _ + "-SNAPSHOT").orElse(Some("SNAPSHOT")) 24 | BasicVersion(wrapped.major, wrapped.minor, wrapped.micro, unstripped) 25 | } 26 | } 27 | 28 | implicit def pimpVersion(wrapped: Version) = new PimpedVersion(wrapped.asInstanceOf[BasicVersion]) 29 | } 30 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/ProjectCache.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | /** 4 | * The ProjectCache allows for managing a cache of instantiated 5 | * projects across subprojects. 6 | */ 7 | 8 | import scala.collection.mutable.HashMap 9 | import _root_.sbt._ 10 | 11 | trait ProjectCache extends BasicManagedProject { 12 | private[this] var _projectCacheStore: HashMap[String, Project] = null 13 | 14 | def projectCacheStore: HashMap[String, Project] = 15 | info.parent match { 16 | case None => 17 | if (_projectCacheStore == null) 18 | _projectCacheStore = new HashMap[String, Project] 19 | _projectCacheStore 20 | 21 | case Some(parent) => 22 | val m = try { 23 | parent.getClass.getMethod("projectCacheStore") 24 | } catch { 25 | case e: NoSuchMethodException => 26 | log.error("Parent project is invalid!") 27 | throw e 28 | } 29 | 30 | m.invoke(parent).asInstanceOf[HashMap[String, Project]] 31 | } 32 | 33 | protected def projectCache(key: String)(make: => Option[Project]) = { 34 | projectCacheStore.get(key) match { 35 | case someProject@Some(_) => someProject 36 | case None => 37 | val made = make 38 | made foreach { project => 39 | projectCacheStore(key) = project 40 | } 41 | 42 | made 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/ProjectDependencies.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | /** 4 | * ProjectDependencies add explicit project dependencies 5 | * ala. adhoc-inlines, but it assumes that all projects in the project 6 | * dependency DAG are also ProjectDependency. This simplifies the 7 | * implementation and makes it more robust. 8 | */ 9 | 10 | import scala.collection.mutable.{HashSet, HashMap} 11 | 12 | import java.util.Properties 13 | import java.io.{FileInputStream, FileOutputStream, File, FileWriter, PrintWriter} 14 | import pimpedversion._ 15 | import collection.jcl.Conversions._ 16 | 17 | import _root_.sbt._ 18 | 19 | trait ParentProjectDependencies 20 | extends BasicDependencyProject 21 | with ProjectCache 22 | with ManagedClasspathFilter 23 | with Environmentalist 24 | with GitHelpers 25 | { 26 | if (this.isInstanceOf[AdhocInlines]) 27 | throw new Exception("AdhocInlines are not compatible with ProjectDependencies") 28 | 29 | private def signature(prop: Properties): String = { 30 | val keys: Array[String] = convertSet(prop.stringPropertyNames()).toArray 31 | util.Sorting.quickSort(keys) 32 | keys map { key => "%s+%s".format(key, prop.getProperty(key)) } mkString 33 | } 34 | 35 | /** 36 | * Returns the projectClosure for the root project. 37 | */ 38 | def rootProjectClosure: List[Project] = { 39 | info.parent match { 40 | case Some(parent) => 41 | try { 42 | val m = parent.getClass.getMethod("rootProjectClosure") 43 | m.invoke(parent).asInstanceOf[List[Project]] 44 | } catch { 45 | case e => 46 | log.error( 47 | ("Parent project of %s is not " + 48 | "a [Parent]ProjectDependencies project!").format(name)) 49 | throw e 50 | } 51 | 52 | case None => 53 | projectClosure 54 | } 55 | } 56 | 57 | /** 58 | * Flag management. 59 | */ 60 | val ProjectDependenciesFile = ".has_project_deps_f0b6608e" 61 | private var _useProjectDependencies: Option[Boolean] = None 62 | private lazy val projectDependenciesFilePresent = 63 | Seq(Path.fromFile(ProjectDependenciesFile), 64 | Path.fromFile("..") / ProjectDependenciesFile).exists { _.exists } 65 | 66 | def useProjectDependencies = 67 | _useProjectDependencies getOrElse { 68 | !environment.get("NO_PROJECT_DEPS").isDefined && 69 | projectDependenciesFilePresent 70 | } 71 | 72 | private lazy val _projectDependencies = new HashSet[ProjectDependency] 73 | def getProjectDependencies = _projectDependencies 74 | 75 | def setUseProjectDependencies(which: Boolean): Boolean = { 76 | val old = useProjectDependencies 77 | _useProjectDependencies = Some(which) 78 | old 79 | } 80 | 81 | /** 82 | * Finds the the parent of this subproject, returns ``this'' if we 83 | * already are the parent project. This is needed to distinguish 84 | * "parent" projects vs. external dependencies in the DAG. 85 | */ 86 | def subProjectParent: Project = 87 | info.parent match { 88 | case Some(parent: ParentProjectDependencies) => 89 | if (parent.isSubProject(this)) parent else this 90 | case _ => 91 | this 92 | } 93 | 94 | def projectSubProjects: Seq[Project] = 95 | info.parent match { 96 | case Some(parent: ParentProjectDependencies) => 97 | parent.actualSubProjects 98 | case _ => 99 | actualSubProjects 100 | } 101 | 102 | case class ProjectDependency(relPath: String, name: String) { 103 | def parentDependency: ProjectDependency = 104 | if (relPath == name) this 105 | else ProjectDependency(relPath, relPath) 106 | 107 | lazy val projectPath: Option[Path] = { 108 | val candidates = Seq( 109 | Path.fromFile(relPath), 110 | Path.fromFile("..") / relPath 111 | ) 112 | 113 | candidates.find { path => (path / "project" / "build.properties").exists } 114 | } 115 | 116 | def resolveProject: Option[Project] = { 117 | projectCache("project:%s:%s".format(relPath, name)) { 118 | projectPath flatMap { projectPath => 119 | val parentProject = 120 | projectCache("path:%s".format(projectPath)) { Some(project(projectPath)) } 121 | parentProject flatMap { parentProject => 122 | if (parentProject.name != name) { 123 | // Try to find it in a subproject. 124 | parentProject.subProjects.find { _._2.name == name } map { _._2 } 125 | } else { 126 | Some(parentProject) 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | def resolveModuleID: Option[ModuleID] = { 134 | val project = subProjectParent 135 | val versionsPath = 136 | Path.fromFile(project.info.projectDirectory) / "project" / "versions.properties" 137 | 138 | val prop = new Properties 139 | prop.load(new FileInputStream(versionsPath.toString)) 140 | 141 | val key = prop.getProperty("%s|%s".format(relPath, name)) 142 | if (key ne null) { 143 | val Array(org, name) = key.split("/", 2) 144 | val version = prop.getProperty(key) 145 | if (version ne null) 146 | Some(org % name % version) 147 | else 148 | None 149 | } else { 150 | None 151 | } 152 | } 153 | } 154 | 155 | implicit def stringToAlmostDependency(relPath: String) = new { 156 | def ~(name: String): ProjectDependency = ProjectDependency(relPath, name) 157 | } 158 | 159 | implicit def stringToDependency(relPath: String): ProjectDependency = 160 | ProjectDependency(relPath, relPath) 161 | 162 | def projectDependencies(deps: ProjectDependency*) { 163 | _projectDependencies ++= deps 164 | } 165 | 166 | def isSubProject(p: Project) = super.subProjects.values contains p 167 | 168 | def actualSubProjects: Seq[Project] = super.subProjects.map(_._2).toSeq 169 | 170 | override def subProjects = { 171 | if (useProjectDependencies) { 172 | val projects = _projectDependencies flatMap { dep => 173 | dep.resolveProject map { project => 174 | dep.name -> project 175 | } 176 | } 177 | 178 | Map() ++ super.subProjects ++ projects 179 | } else { 180 | super.subProjects 181 | } 182 | } 183 | 184 | /** 185 | * TODO: always expose all library depencies? what about 186 | * chicken-and-egg re. versioning? can we always query the 187 | * underlying module? [i think? it has to be released? as a fallback 188 | * we can use the current version-yes, do this. and warn when it 189 | * happens.] 190 | */ 191 | 192 | override def libraryDependencies = 193 | if (useProjectDependencies) { 194 | val missingProjectDependencies = 195 | _projectDependencies filter { !_.resolveProject.isDefined } 196 | super.libraryDependencies ++ { 197 | Set() ++ missingProjectDependencies flatMap { _.resolveModuleID } 198 | } 199 | } else { 200 | (Set() ++ _projectDependencies flatMap { _.resolveModuleID }) ++ super.libraryDependencies 201 | } 202 | 203 | /** 204 | * Filters out dependencies that are in our DAG. 205 | */ 206 | 207 | private def parentManagedDependencyFilter(config: Configuration, m: ModuleID): Boolean = 208 | info.parent match { 209 | case Some(parent) => 210 | try { 211 | val method = parent.getClass.getMethod( 212 | "managedDependencyFilter", 213 | classOf[Configuration], classOf[ModuleID]) 214 | method.invoke(parent, config, m).asInstanceOf[Boolean] 215 | } catch { 216 | case e => 217 | log.error( 218 | ("Parent project of %s is not " + 219 | "a [Parent]ProjectDependencies project!").format(name)) 220 | throw e 221 | } 222 | 223 | case None => 224 | true 225 | } 226 | 227 | def filteredProjectClasspath( 228 | config: Configuration, projects: List[Project] 229 | ): PathFinder = fullUnmanagedClasspath(config) +++ { 230 | config match { 231 | case Configurations.Provided => 232 | Path.emptyPathFinder 233 | case config => 234 | filterPathFinderClasspath(managedClasspath(config)) { m => 235 | !(projects exists { p => p.organization == m.organization && p.name == m.name }) 236 | } 237 | } 238 | } 239 | 240 | override def fullClasspath(config: Configuration): PathFinder = 241 | if (!useProjectDependencies) { 242 | super.fullClasspath(config) 243 | } else { 244 | Path.lazyPathFinder { 245 | val set = new HashSet[Path] 246 | for (project <- topologicalSort) { 247 | val method = project.getClass.getMethod( 248 | "filteredProjectClasspath", 249 | classOf[Configuration], classOf[List[Project]]) 250 | 251 | val queryConfig = 252 | if (config == Configurations.Test && 253 | (project ne this) && info.dependencies.forall(_ ne project)) { 254 | Configurations.Runtime 255 | } else { 256 | config 257 | } 258 | 259 | val projectClasspath = 260 | method.invoke(project, queryConfig, projectClosure).asInstanceOf[PathFinder] 261 | set ++= projectClasspath.get 262 | } 263 | set.toList 264 | } 265 | } 266 | 267 | /** 268 | * Release management. 269 | */ 270 | 271 | def lastReleasedVersion(): Option[Version] = { 272 | val project = subProjectParent 273 | val releasePropertiesPath = 274 | Path.fromFile(project.info.projectDirectory) / "project" / "release.properties" 275 | val prop = new Properties 276 | if (!releasePropertiesPath.exists) 277 | return None 278 | 279 | prop.load(new FileInputStream(releasePropertiesPath.toString)) 280 | val version = prop.getProperty("version") 281 | if (version eq null) 282 | return None 283 | 284 | Version.fromString(version) match { 285 | case Left(_) => None 286 | case Right(v) => Some(v) 287 | } 288 | } 289 | 290 | /** 291 | * Update versions for projectDependencies. We do so by querying our 292 | * dependencies for their currently released versions. 293 | */ 294 | lazy val updateVersions = task { 295 | // TODO: use builderPath? 296 | val project = subProjectParent 297 | 298 | val versionsPath = 299 | Path.fromFile(project.info.projectDirectory) / "project" / "versions.properties" 300 | 301 | // Merge the existing one when it exists. 302 | val prop = new Properties 303 | if (versionsPath.exists) 304 | prop.load(new FileInputStream(versionsPath.toString)) 305 | 306 | val oldSignature = signature(prop) 307 | 308 | val projects = _projectDependencies flatMap { dep => dep.resolveProject map { (_, dep) } } 309 | projects foreach { case (p, dep) => 310 | val m = p.getClass.getMethod("lastReleasedVersion") 311 | val version = if (m ne null) { 312 | m.invoke(p).asInstanceOf[Option[Version]] 313 | } else { 314 | log.error("project %s is not a ReleaseManagement project".format(p.name)) 315 | None 316 | } 317 | 318 | version foreach { version => 319 | val key = "%s/%s".format(p.organization, p.name) 320 | prop.setProperty("%s|%s".format(dep.relPath, dep.name), key) 321 | prop.setProperty(key, version.toString) 322 | } 323 | } 324 | 325 | if (signature(prop) != oldSignature) { 326 | val stream = new FileOutputStream(versionsPath.toString) 327 | prop.store(stream,"Automatically generated by ProjectDependencies") 328 | stream.close() 329 | gitCommitFiles("updated versions.properties", versionsPath.toString) 330 | } 331 | 332 | None 333 | } 334 | 335 | lazy val testProject = task { args => 336 | task { 337 | val allProjects = projectSubProjects ++ Seq(subProjectParent) 338 | val testableProjects = allProjects filter { _.methods contains "test-only" } 339 | val errors = testableProjects map { _.call("test-only", args) } 340 | // report only the first error. 341 | val error = errors find { _.isDefined } 342 | error flatMap { x => x } // using "identity" here fails type inference :-/ 343 | } 344 | } describedAs( 345 | "Run tests for the current project. " + 346 | "Analagous to running tests in a project " + 347 | "without project dependencies.") 348 | 349 | lazy val publishOnly = task { args => 350 | ivyTask { 351 | val publishConfig = publishConfiguration 352 | import publishConfig._ 353 | val deliveredIvy = if(publishIvy) Some(deliveredPattern) else None 354 | IvyActions.makePom(deliverIvyModule, makePomConfiguration, pomPath asFile) 355 | IvyActions.deliver(deliverIvyModule, status, deliveredPattern, extraDependencies, configurations, ivyUpdateLogging) 356 | IvyActions.publish(publishIvyModule, resolverName, srcArtifactPatterns, deliveredIvy, configurations) 357 | } 358 | } describedAs ("publish only the current project.") 359 | 360 | lazy val publishProject = interactiveTask { 361 | val allProjects = projectSubProjects ++ Seq(subProjectParent) 362 | val publishableProjects = allProjects filter { p => 363 | p.methods contains "publish-only" 364 | } 365 | log.info("publish-project will publish %s".format(publishableProjects.mkString(", "))) 366 | val errors = publishableProjects map { p => 367 | p.call("publish-only", Array()) 368 | } 369 | // report only the first error. 370 | val error = errors find { _.isDefined } 371 | error flatMap { x => x } // using "identity" here fails type inference :-/ 372 | } describedAs("publish only the current project and subprojects.") 373 | 374 | 375 | /** 376 | * Utilities / debugging. 377 | */ 378 | 379 | lazy val analyze = interactiveTask { 380 | val projectsAndDependencies = projectClosure map { project => 381 | val dependencies = try { 382 | val m = project.getClass.getMethod("libraryDependencies") 383 | m.invoke(project).asInstanceOf[Set[ModuleID]] 384 | } catch { case _ => Set() } 385 | 386 | (project, dependencies) 387 | } 388 | 389 | projectsAndDependencies foreach { case (outerProject, dependencies) => 390 | projectClosure foreach { innerProject => 391 | dependencies foreach { dep => 392 | if (innerProject.organization == dep.organization && 393 | innerProject.name == dep.name) { 394 | log.warn( 395 | ("Project %s brings in dependency %s, but this is " + 396 | "also provided by project %s").format(outerProject, dep, innerProject)) 397 | } 398 | } 399 | } 400 | } 401 | 402 | val dependencyToProject = 403 | projectsAndDependencies flatMap { case (project, dependencies) => 404 | (dependencies map { (_, project) }).toList 405 | } 406 | 407 | val seenDeps = 408 | new scala.collection.mutable.HashMap[(String, String), List[(ModuleID, Project)]] 409 | 410 | dependencyToProject foreach { case (dep, project) => 411 | val k = (dep.organization, dep.name) 412 | val l = seenDeps getOrElseUpdate(k, Nil: List[(ModuleID, Project)]) 413 | 414 | seenDeps((dep.organization, dep.name)) = (dep, project) :: l 415 | } 416 | 417 | seenDeps foreach { case ((org, name), deps) => 418 | val revisions = Set() ++ deps map { case (dep, _) => dep.revision } 419 | if (revisions.size > 1) { 420 | log.warn("Conflicting dependencies for %s:%s".format(org, name)) 421 | revisions foreach { rev => 422 | val projects = 423 | deps filter { case (dep, _) => dep.revision == rev } map { case (_, p) => p } 424 | val projectNames = projects map { _.name } 425 | log.warn("On revision %s: %s".format(rev, projectNames mkString ",")) 426 | } 427 | } 428 | } 429 | 430 | None 431 | } 432 | 433 | lazy val toggleProjectDependencies = task { 434 | _useProjectDependencies = Some(!useProjectDependencies) 435 | if (useProjectDependencies) 436 | log.info("project dependencies are on") 437 | else 438 | log.info("project dependencies are off") 439 | None 440 | } 441 | 442 | lazy val showParent = task { 443 | log.info("my name is: %s and my parent is: %s. my parentProject is: %s".format( 444 | name, info.parent, subProjectParent)) 445 | None 446 | } 447 | 448 | lazy val showDependencies = task { 449 | log.info("Library dependencies:") 450 | libraryDependencies foreach { dep => log.info(" %s".format(dep)) } 451 | 452 | log.info("Project dependencies:") 453 | if (!useProjectDependencies) 454 | log.info("* project dependencies are currently turned off") 455 | _projectDependencies foreach { dep => log.info(" %s".format(dep)) } 456 | 457 | None 458 | } 459 | 460 | lazy val showProjectClosure = task { 461 | log.info("Project closure:") 462 | projectClosure foreach { project => 463 | println(" " + project + " " + project.hashCode) 464 | } 465 | 466 | None 467 | } 468 | 469 | lazy val showRootProjectClosure = task { 470 | log.info("Root project closure:") 471 | rootProjectClosure foreach { project => 472 | println(" " + project + " " + project.hashCode) 473 | } 474 | 475 | None 476 | } 477 | 478 | lazy val showManagedClasspath = task { args => 479 | val name = args match { 480 | case Array(configName) => configName 481 | case _ => "compile" 482 | } 483 | 484 | task { 485 | val config = Configurations.config(name) 486 | managedClasspath(config).get foreach { path => 487 | println("> %s".format(path)) 488 | } 489 | 490 | None 491 | } 492 | } 493 | 494 | // override def managedClasspath(config: Configuration) = { 495 | // // println("MC: %s".format(config)) 496 | // super.managedClasspath(config) 497 | // } 498 | 499 | lazy val showMe = task { 500 | println(defaultConfigurationExtensions) 501 | None 502 | } 503 | 504 | lazy val showFullClasspath = task { args => 505 | val name = args match { 506 | case Array(configName) => configName 507 | case _ => "compile" 508 | } 509 | 510 | task { 511 | val config = Configurations.config(name) 512 | fullClasspath(config).get foreach { path => 513 | println("> %s".format(path.absolutePath)) 514 | } 515 | 516 | None 517 | } 518 | } 519 | 520 | lazy val showLastReleasedVersion = task { 521 | println(lastReleasedVersion) 522 | None 523 | } 524 | 525 | lazy val showProjectPath = task { 526 | println(info.projectDirectory) 527 | None 528 | } 529 | } 530 | 531 | trait ProjectDependencies extends BasicScalaProject with ParentProjectDependencies { 532 | lazy val generateRunClasspath = task { 533 | val file = new File(info.projectDirectory, ".run_classpath") 534 | val out = new PrintWriter(new FileWriter(file)) 535 | runClasspath.get foreach { path => out.println(path.absolutePath) } 536 | mainDependencies.scalaJars.get foreach { path => out.println(path.absolutePath) } 537 | out.close() 538 | None 539 | } dependsOn(compile, copyResources) 540 | 541 | lazy val showCompileClasspath = task { 542 | compileClasspath.get foreach { path => 543 | println("> %s".format(path)) 544 | } 545 | 546 | None 547 | } 548 | 549 | lazy val showOptionalClasspath = task { 550 | optionalClasspath.get foreach { path => 551 | println("> %s".format(path)) 552 | } 553 | None 554 | } 555 | 556 | lazy val showProvidedClasspath = task { 557 | providedClasspath.get foreach { path => 558 | println("> %s".format(path)) 559 | } 560 | None 561 | } 562 | 563 | lazy val showTestClasspath = task { 564 | testClasspath.get foreach { path => 565 | println("> %s".format(path.absolutePath)) 566 | } 567 | None 568 | } 569 | 570 | lazy val showRunClasspath = task { 571 | runClasspath.get foreach { path => 572 | println("> %s".format(path.absolutePath)) 573 | } 574 | None 575 | } 576 | 577 | lazy val showUnmanagedClasspath = task { args => 578 | val name = args match { 579 | case Array(configName) => configName 580 | case _ => "compile" 581 | } 582 | 583 | task { 584 | val config = Configurations.config(name) 585 | fullUnmanagedClasspath(config).get foreach { path => 586 | println("> %s".format(path)) 587 | } 588 | 589 | None 590 | } 591 | } 592 | } 593 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/ProjectWrapper.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import scala.reflect.Manifest 4 | 5 | import _root_.sbt._ 6 | 7 | class WrappedDefaultProject(val underlying: DefaultProject) 8 | extends StandardProject(underlying.info) 9 | { 10 | override def name = underlying.name 11 | override def version = underlying.version 12 | override def organization = underlying.organization 13 | override def scratch = underlying.scratch 14 | override def libraryDependencies = underlying.libraryDependencies 15 | override def subProjects = Map() ++ underlying.subProjects 16 | override def repositories = underlying.repositories 17 | 18 | // Ivy stuff. 19 | override def ivyUpdateConfiguration = underlying.ivyUpdateConfiguration 20 | override def ivyUpdateLogging = underlying.ivyUpdateLogging 21 | override def ivyRepositories = underlying.ivyRepositories 22 | override def otherRepositories = underlying.otherRepositories 23 | override def ivyValidate = underlying.ivyValidate 24 | override def ivyScala = underlying.ivyScala 25 | override def ivyCacheDirectory = underlying.ivyCacheDirectory 26 | override def ivyPaths = underlying.ivyPaths 27 | override def inlineIvyConfiguration = underlying.inlineIvyConfiguration 28 | 29 | override def ivyConfiguration = underlying.ivyConfiguration 30 | override def ivySbt = underlying.ivySbt 31 | override def ivyModule = underlying.ivyModule 32 | 33 | override def updateTask(module: => IvySbt#Module, configuration: => UpdateConfiguration) = task { 34 | underlying.updateTask(module, configuration).run 35 | } 36 | 37 | override def moduleSettings = underlying.moduleSettings 38 | override def inlineSettings = underlying.inlineSettings 39 | override def compatTestFramework = underlying.compatTestFramework 40 | override def defaultModuleSettings = underlying.defaultModuleSettings 41 | override def externalSettings = underlying.externalSettings 42 | override def outputPattern = underlying.outputPattern 43 | override def ivyXML = underlying.ivyXML 44 | override def pomExtra = underlying.pomExtra 45 | override def ivyConfigurations = underlying.ivyConfigurations 46 | 47 | override def extraDefaultConfigurations = underlying.extraDefaultConfigurations 48 | override def useIntegrationTestConfiguration = underlying.useIntegrationTestConfiguration 49 | override def defaultConfiguration = underlying.defaultConfiguration 50 | override def useMavenConfigurations = underlying.useMavenConfigurations 51 | override def useDefaultConfigurations = underlying.useDefaultConfigurations 52 | 53 | override def mainSourceRoots = underlying.mainSourceRoots 54 | 55 | override def updateModuleSettings = underlying.updateModuleSettings 56 | override def updateIvyModule = underlying.updateIvyModule 57 | override def deliverModuleSettings = underlying.deliverModuleSettings 58 | override def deliverIvyModule = underlying.deliverIvyModule 59 | override def publishModuleSettings = underlying.publishModuleSettings 60 | override def publishIvyModule = underlying.publishIvyModule 61 | 62 | override lazy val clean = task { underlying.clean.run } 63 | 64 | // override def cleanAction = underlying.cleanAction 65 | 66 | // override protected def updateAction = underlying.updateAction 67 | // override protected def cleanLibAction = underlying.cleanLibAction 68 | // override protected def cleanCacheAction = underlying.cleanCacheAction 69 | // override protected def deliverProjectDependencies = underlying.deliverProjectDependencies 70 | 71 | override def packageToPublishActions = underlying.packageToPublishActions 72 | override lazy val makePom = task { 73 | underlying.makePom.run 74 | } 75 | 76 | override def compileOptions = 77 | underlying.compileOptions map { opt => CompileOption(opt.asString) } 78 | override def compileOrder = underlying.compileOrder 79 | override def managedStyle = underlying.managedStyle 80 | 81 | override def fullUnmanagedClasspath(config: Configuration) = 82 | underlying.fullUnmanagedClasspath(config) 83 | 84 | override def managedClasspath(config: Configuration): PathFinder = 85 | underlying.managedClasspath(config) 86 | 87 | // Properties. 88 | override def property[T](implicit manifest: Manifest[T], format: Format[T]) = { 89 | lazy val p = underlying.property(manifest, format) 90 | new Property[T] with Proxy { 91 | def self = p 92 | def update(v: T) { self.update(v) } 93 | def resolve = self.resolve 94 | } 95 | } 96 | 97 | override def propertyLocal[T](implicit manifest: Manifest[T], format: Format[T]) = { 98 | lazy val p = underlying.propertyLocal(manifest, format) 99 | new Property[T] with Proxy { 100 | def self = p 101 | def update(v: T) { self.update(v) } 102 | def resolve = self.resolve 103 | } 104 | } 105 | 106 | override def propertyOptional[T] 107 | (defaultValue: => T) 108 | (implicit manifest: Manifest[T], format: Format[T]) = { 109 | lazy val p = underlying.propertyOptional(defaultValue)(manifest, format) 110 | new Property[T] with Proxy { 111 | def self = p 112 | def update(v: T) { self.update(v) } 113 | def resolve = self.resolve 114 | } 115 | } 116 | 117 | override def system[T](propName: String)(implicit format: Format[T]) = { 118 | lazy val p = underlying.system(propName)(format) 119 | new Property[T] with Proxy { 120 | def self = p 121 | def update(v: T) { self.update(v) } 122 | def resolve = self.resolve 123 | } 124 | } 125 | 126 | override def systemOptional[T] 127 | (propName: String, defaultValue: => T) 128 | (implicit format: Format[T]) = { 129 | lazy val p = underlying.systemOptional(propName, defaultValue)(format) 130 | new Property[T] with Proxy { 131 | def self = p 132 | def update(v: T) { self.update(v) } 133 | def resolve = self.resolve 134 | } 135 | } 136 | 137 | // TODO: as needed. 138 | } 139 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/PublishLocalWithMavenStyleBasePattern.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | import java.io.File 5 | 6 | trait PublishLocalWithMavenStyleBasePattern extends StandardManagedProject { 7 | val localIvyBasePattern = "[organisation]/[module]/[revision]/ivy-[revision](-[classifier]).[ext]" 8 | val localm2 = Resolver.file("localm2", new File(Resolver.userIvyRoot + "/local"))( 9 | Patterns(Seq(localIvyBasePattern), Seq(Resolver.mavenStyleBasePattern), true)) 10 | 11 | override def localRepos = super.localRepos + localm2 12 | 13 | def usesMavenStyleBasePatternInPublishLocalConfiguration: Boolean = info.parent match { 14 | case Some(parent: PublishLocalWithMavenStyleBasePattern) => 15 | parent.usesMavenStyleBasePatternInPublishLocalConfiguration 16 | case _ => 17 | true 18 | } 19 | 20 | override def publishLocalConfiguration = { 21 | if (usesMavenStyleBasePatternInPublishLocalConfiguration) 22 | new DefaultPublishConfiguration("localm2", "release", true) 23 | else 24 | super.publishLocalConfiguration 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/PublishSite.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | import _root_.sbt.Process._ 5 | import java.io._ 6 | import freemarker.template.{Configuration => FreeConfig, SimpleSequence} 7 | import freemarker.cache.StringTemplateLoader 8 | import com.petebevin.markdown._ 9 | import scala.io._ 10 | 11 | trait BuildSite extends MavenStyleScalaPaths { 12 | /** where a pre-generated web site might exist */ 13 | def sitePath: Path = "site" 14 | /** where pre-generated docs might exist */ 15 | def docsPath: Path = "docs" 16 | /** where we'll stick our generated web site */ 17 | def siteOutputPath = outputRootPath / "site" 18 | /** where scaladocs end up */ 19 | def docOutputPath = siteOutputPath / "api" 20 | /** where our generated doc goes */ 21 | def scalaDocDir: Option[Path] = Some(docPath) 22 | /** make the directory for our site */ 23 | def buildSiteDir() = siteOutputPath.asFile.mkdir 24 | /** is our readme in markdown format? */ 25 | def isReadmeMarkdown = true 26 | /** the filename of our readme */ 27 | def readmeFileName: Option[String] = None 28 | /** for github publishing: which remote to publish to */ 29 | def githubRemote: String = "origin" 30 | 31 | /** overridden */ 32 | val buildSite: Task 33 | 34 | def indexTemplate = Source.fromInputStream(getClass.getResourceAsStream("/index.template")).mkString 35 | lazy val markdownTemplate = Source.fromInputStream(getClass.getResourceAsStream("/markdown.template")).mkString 36 | 37 | // build an index.html if one doesn't already exist. 38 | def buildIndex(cfg: FreeConfig): Option[String] = { 39 | val oldIndex = (siteOutputPath / "index.html").asFile 40 | if (! oldIndex.exists) { 41 | val index = (siteOutputPath / "index.html").asFile 42 | val writer = new BufferedWriter(new FileWriter(index)) 43 | val model = buildModel() 44 | val template = cfg.getTemplate("index") 45 | template.process(model, writer) 46 | writer.close() 47 | } else { 48 | findReadme.foreach { f => copyMarkdownFile(f, (siteOutputPath / "readme.html").absolutePath.toString) } 49 | } 50 | None 51 | } 52 | 53 | def buildModel() = { 54 | val model = new java.util.HashMap[Any, Any]() 55 | model.put("projectName", projectName.value) 56 | scalaDocDir.foreach(model.put("scaladoc", _)) 57 | findReadme.map(processReadme(_)).foreach(model.put("readme", _)) 58 | model 59 | } 60 | 61 | def buildMarkdownFiles(sourcePath: Path, destinationPath: Path): Option[String] = { 62 | destinationPath.asFile.mkdirs() 63 | ((sourcePath ##) ***).filter { f => !f.isDirectory && List("md", "markdown").contains(f.ext) }.get.foreach { path => 64 | val destString = Path.fromString(destinationPath, path.relativePath).absolutePath.toString 65 | val dest = destString.substring(0, destString.size - path.ext.size) + "html" 66 | copyMarkdownFile(path.absolutePath.toString, dest) 67 | } 68 | None 69 | } 70 | 71 | def copyMarkdownFile(source: String, destination: String) { 72 | val text = new MarkdownProcessor().markdown(Source.fromFile(source).mkString) 73 | val writer = new BufferedWriter(new FileWriter(destination)) 74 | writer.write(markdownTemplate.replace("{{content}}", text)) 75 | writer.close() 76 | } 77 | 78 | def findReadme() = { 79 | readmeFileName orElse { 80 | List("README", "README.md").find { _.asFile.exists } 81 | } 82 | } 83 | 84 | def processReadme(fileName: String): String = { 85 | val text = Source.fromFile(fileName).mkString 86 | if (isReadmeMarkdown) { 87 | val processor = new MarkdownProcessor() 88 | processor.markdown(text) 89 | } else { 90 | text 91 | } 92 | } 93 | 94 | def copyGeneratedDoc(): Option[String] = { 95 | scalaDocDir.flatMap { path => FileUtilities.sync(path, docOutputPath, log) } 96 | } 97 | 98 | def copySite() = { 99 | FileUtilities.clean(siteOutputPath, log) orElse 100 | FileUtilities.sync(sitePath, siteOutputPath, log) orElse 101 | buildMarkdownFiles(sitePath, siteOutputPath) orElse 102 | FileUtilities.sync(docsPath, siteOutputPath / "docs", log) orElse 103 | buildMarkdownFiles(docsPath, siteOutputPath / "docs") orElse 104 | FileUtilities.createDirectory(siteOutputPath, log) 105 | } 106 | 107 | def buildSiteTask = task { 108 | val cfg = new FreeConfig() 109 | val templateLoader = new StringTemplateLoader() 110 | templateLoader.putTemplate("index", indexTemplate) 111 | cfg.setTemplateLoader(templateLoader) 112 | 113 | copySite() orElse 114 | buildIndex(cfg) orElse 115 | copyGeneratedDoc() 116 | } 117 | 118 | def gitPublishRepo = Some("http://git.local.twitter.com/blabber.git") 119 | 120 | def publishToGitTask = interactiveTask { 121 | gitPublishRepo.flatMap(repo => { 122 | val tmpdir = System.getProperty("java.io.tmpdir") match { 123 | case null => "/tmp" 124 | case t => t 125 | } 126 | val tmpLoc = tmpdir + File.separator + "blabber" 127 | val mySiteLoc = tmpLoc + File.separator + projectName.value 128 | val siteFullPath = siteOutputPath.asFile.getAbsolutePath 129 | val localGitRepoExists = if (!(new File(tmpLoc).exists)) { 130 | val res = ("mkdir -p %s".format(tmpLoc)) ! 131 | 132 | if (res == 0) { 133 | val cloneRes = ((new java.lang.ProcessBuilder("git", "clone", repo, ".") directory new File(tmpLoc) ) !) 134 | cloneRes == 0 135 | } else { 136 | false 137 | } 138 | } else { 139 | true 140 | } 141 | if (localGitRepoExists) { 142 | val res = if ((new File(mySiteLoc)).exists) { 143 | println("trying to mv %s to %s/%s.%s".format(mySiteLoc, tmpdir, projectName.value, System.currentTimeMillis)) 144 | ("mv -nf %s %s/%s.%s".format(mySiteLoc, tmpdir, projectName.value, System.currentTimeMillis)!) 145 | } else { 146 | 0 147 | } 148 | if (res == 0) { 149 | // doing this the hard way because we're in a tmp dir 150 | val copySite = new java.lang.ProcessBuilder("cp", "-r", siteFullPath + File.separator, mySiteLoc) directory new File(tmpLoc) 151 | val gitPull = new java.lang.ProcessBuilder("git", "pull") directory new File(tmpLoc) 152 | val gitAdd = new java.lang.ProcessBuilder("git", "add", ".") directory new File(tmpLoc) 153 | val gitCommit = new java.lang.ProcessBuilder("git", "commit", "--allow-empty", "-m", "%s site update".format(projectName.value)) directory new File(tmpLoc) 154 | val gitPush = new java.lang.ProcessBuilder("git", "push") directory new File(tmpLoc) 155 | 156 | val gitRes = copySite #&& gitPull #&& gitAdd #&& gitCommit #&& gitPush! 157 | 158 | if (gitRes == 0) { 159 | None 160 | } else { 161 | Some("error publishing to %s, exit code is %d".format(repo, gitRes)) 162 | } 163 | } else { 164 | Some("error moving old site directory aside, exit code is %d".format(res)) 165 | } 166 | } else { 167 | Some("error cloning repo %s".format(repo)) 168 | } 169 | }) 170 | } 171 | 172 | lazy val publishToGit = publishToGitTask dependsOn(buildSite) describedAs "publishes to a git repo" 173 | 174 | def publishToGithubTask = interactiveTask { 175 | // if gh-pages branch doesn't exist, bail 176 | val ghPagesSetup: String = ("git branch -lr" #| "grep gh-pages")!! 177 | 178 | if (ghPagesSetup == "") { 179 | Some("gh-pages branch is not present, not publishing") 180 | } else { 181 | val remoteRepo: String = ("git config --get remote." + githubRemote + ".url" !!).trim 182 | val tmpdir = System.getProperty("java.io.tmpdir") match { 183 | case null => "/tmp" 184 | case t => t 185 | } 186 | val tmpLoc = "%s/%s".format(tmpdir, projectName.value) 187 | val siteFullPath = siteOutputPath.asFile.getAbsolutePath 188 | 189 | // set up our working directory, clobbering any existing content 190 | val res = if ((new File(tmpLoc)).exists) { 191 | "mv -n %s %s.%s".format(tmpLoc, tmpLoc, System.currentTimeMillis) #&& 192 | "mkdir -p %s".format(tmpLoc) ! 193 | } else { 194 | "mkdir -p %s".format(tmpLoc) ! 195 | } 196 | 197 | if (res == 0) { 198 | // doing this the hard way because we're in a tmp dir 199 | val gitClone = new java.lang.ProcessBuilder("git", "clone", remoteRepo, "-b", "gh-pages", ".") directory new File(tmpLoc) 200 | val copySite = new java.lang.ProcessBuilder("cp", "-r", siteFullPath + File.separator, ".") directory new File(tmpLoc) 201 | val gitAdd = new java.lang.ProcessBuilder("git", "add", ".") directory new File(tmpLoc) 202 | val gitCommit = new java.lang.ProcessBuilder("git", "commit", "--allow-empty", "-m", "site update") directory new File(tmpLoc) 203 | val gitPush = new java.lang.ProcessBuilder("git", "push", "origin", "gh-pages") directory new File(tmpLoc) 204 | 205 | val gitRes = gitClone #&& copySite #&& gitAdd #&& gitCommit #&& gitPush! 206 | 207 | if (gitRes == 0) { 208 | None 209 | } else { 210 | Some("error publishing to github, exit code is " + gitRes) 211 | } 212 | } else { 213 | Some("error setting up tmp directory, exit code is " + res) 214 | } 215 | } 216 | } 217 | 218 | lazy val publishToGithub = publishToGithubTask dependsOn(buildSite) describedAs "publishes to github" 219 | 220 | } 221 | 222 | trait PublishParentSite extends ParentProject with BuildSite { 223 | lazy val buildSite = buildSiteTask describedAs "builds a dope site" 224 | 225 | override def indexTemplate = Source.fromInputStream(getClass.getResourceAsStream("/parent-index.template")).mkString 226 | 227 | lazy val siteSubprojects: List[PublishSite] = subProjects.values.filter(subp => { 228 | subp match { 229 | case siteProject: PublishSite => true 230 | case _ => false 231 | } 232 | }).toList.asInstanceOf[List[PublishSite]] 233 | 234 | override def buildModel() = { 235 | val model = super.buildModel 236 | val modelList = new SimpleSequence() 237 | siteSubprojects.foreach(p => modelList.add(p.name)) 238 | model.put("subprojects", modelList) 239 | model 240 | } 241 | 242 | override def copySite() = { 243 | FileUtilities.clean(siteOutputPath, log) orElse { 244 | val results = siteSubprojects.map(siteProject => { 245 | val from = siteProject.siteOutputPath 246 | val to = siteOutputPath / siteProject.name 247 | println("copying site for %s from %s to %s".format(siteProject.name, from, to)) 248 | FileUtilities.sync(from, to, log) 249 | }) 250 | results.find(res => res != None).flatMap(f => f) 251 | } 252 | } 253 | 254 | override def buildSiteTask = task { 255 | val cfg = new FreeConfig() 256 | val templateLoader = new StringTemplateLoader() 257 | templateLoader.putTemplate("index", indexTemplate) 258 | cfg.setTemplateLoader(templateLoader) 259 | 260 | copySite() orElse 261 | buildIndex(cfg) orElse 262 | copyGeneratedDoc() 263 | } 264 | } 265 | 266 | trait PublishSite extends DefaultProject with BuildSite with PublishSourcesAndJavadocs { 267 | lazy val buildSite = buildSiteTask dependsOn(packageDocs) describedAs "builds a dope site" 268 | } 269 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/PublishSourcesAndJavadocs.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | 5 | trait PublishSourcesAndJavadocs extends DefaultProject { 6 | // need to ask scaladoc not to try to understand any generated code (for example, thrift stuff): 7 | def docSources = sources((mainJavaSourcePath##) +++ (mainScalaSourcePath##)) 8 | override def docAction = scaladocTask(mainLabel, docSources, mainDocPath, docClasspath, documentOptions).dependsOn(compile) 9 | 10 | override def packageDocsJar = defaultJarPath("-javadoc.jar") 11 | override def packageSrcJar= defaultJarPath("-sources.jar") 12 | lazy val sourceArtifact = Artifact.sources(artifactID) 13 | lazy val docsArtifact = Artifact.javadoc(artifactID) 14 | override lazy val publish = super.publishAction dependsOn(packageDocs, packageSrc) 15 | override lazy val publishLocal = super.publishLocalAction dependsOn(packageDocs, packageSrc) 16 | override def packageToPublishActions = super.packageToPublishActions ++ Seq(packageDocs, packageSrc) 17 | } 18 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/PublishThrift.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | 5 | trait PublishThrift extends PublishSourcesAndJavadocs { 6 | def packageThriftJar = defaultJarPath("-thrift.jar") 7 | lazy val thriftArtifact = Artifact(artifactID, "thrift", "jar", "thrift") 8 | def packageThriftAction = packageTask(mainSourcePath / "thrift" ##, packageThriftJar, Recursive) describedAs "package thrift jar" 9 | lazy val packageThrift = packageThriftAction 10 | 11 | // don't nuke the publish sources/javadocs goodness 12 | override lazy val publish = super.publishAction dependsOn(packageDocs, packageSrc, packageThrift) 13 | override lazy val publishLocal = super.publishLocalAction dependsOn(packageDocs, packageSrc, packageThrift) 14 | override def packageToPublishActions = super.packageToPublishActions ++ Seq(packageDocs, packageSrc, packageThrift) 15 | } 16 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/Ramdiskable.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | import java.io.File 5 | 6 | /** 7 | * Use a ramdisk as the build target folder if one is specified in `SBT_RAMDISK_ROOT`. 8 | */ 9 | trait Ramdiskable extends DefaultProject with Environmentalist { 10 | private val ramdiskRoot = environment.get("SBT_RAMDISK_ROOT") 11 | private val ramdiskTargetName = "target-ramdisk" 12 | 13 | for (ramdiskRoot <- ramdiskRoot) { 14 | val ramdiskPath = new File("%s/%s".format(ramdiskRoot, name)) 15 | log.info("Compiling to ramdisk at %s".format(ramdiskPath)) 16 | 17 | val target = new File(ramdiskTargetName) 18 | val canonicalPath = target.getCanonicalPath 19 | val absolutePath = target.getAbsolutePath 20 | 21 | if (target.exists && canonicalPath != ramdiskRoot) { 22 | if (target.isFile || absolutePath != canonicalPath) { 23 | log.info("Deleting existing symlink at %s".format(target)) 24 | target.delete() 25 | } else { 26 | log.info("Removing existing directory at %s".format(target)) 27 | FileUtilities.clean(Path.fromFile(target), log) 28 | } 29 | } 30 | 31 | // Make symlink. 32 | if (!target.exists) { 33 | import Process._ 34 | log.info("Creating ramdisk build symlink %s".format(ramdiskPath)) 35 | ramdiskPath.mkdirs() 36 | (execTask { "ln -s %s %s".format(ramdiskPath, ramdiskTargetName) }).run 37 | } 38 | } 39 | 40 | override def outputRootPath = 41 | if (ramdiskRoot.isDefined) 42 | "target-ramdisk": Path 43 | else 44 | super.outputRootPath 45 | } 46 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/ReleaseManagement.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import java.util.Properties 4 | import java.io.{FileOutputStream, FileInputStream} 5 | 6 | import _root_.sbt.{BasicManagedProject, BasicDependencyProject, Version} 7 | import _root_.sbt.Process 8 | import _root_.sbt.Process._ 9 | import pimpedversion._ 10 | 11 | trait ReleaseManagement extends BasicManagedProject with GitHelpers { 12 | private def versionToString(version: Version): String = 13 | versionToString(version.toString) 14 | private def versionToString(version: String): String = 15 | "org=%s,name=%s,version=%s".format(organization, name, version) 16 | 17 | private val releasePropertiesPath = info.projectPath / "project" / "release.properties" 18 | 19 | def prepareForReleaseTask = task { 20 | val versionString = versionToString(projectVersion.value) 21 | 22 | val tags = ("git tag -l | grep " + versionString) !! 23 | 24 | if (!gitIsCleanWorkingTree) { 25 | Some("Cannot publish release. Working directory is not clean.") 26 | } else if (libraryDependencies.exists(_.revision.contains("SNAPSHOT"))) { 27 | Some("Cannot publish a release with snapshotted dependencies.") 28 | } else if (tags.contains(versionString) && !tags.contains("SNAPSHOT")) { 29 | Some("Cannot publish release version '" + 30 | versionString + 31 | "'. Tag for that release already exists.") 32 | } else { 33 | stripSnapshotExtraTask.run 34 | } 35 | } 36 | 37 | lazy val prepareForRelease = prepareForReleaseTask 38 | 39 | private def stripSnapshotExtraTask = task { 40 | projectVersion.update(projectVersion.value.stripSnapshot()) 41 | saveEnvironment() 42 | 43 | None 44 | } 45 | 46 | def finalizeReleaseTask = task { 47 | val version = projectVersion.value 48 | val newVersion = projectVersion.value.incMicro().addSnapshot() 49 | 50 | // commit and tag the release 51 | gitCommitSavedEnvironment(Some(versionToString(version))) 52 | 53 | val headRefs = "git show-ref refs/heads/master" lines_! 54 | 55 | require(headRefs.size == 1) 56 | val Array(sha1, _) = headRefs(0).split(" ") 57 | 58 | gitTag(versionToString(version)) 59 | 60 | // reset version to the new working version 61 | projectVersion.update(newVersion) 62 | saveEnvironment() 63 | // Save a properties file with our version number in it. 64 | 65 | val prop = new Properties 66 | prop.setProperty("version", version.toString) 67 | prop.setProperty("sha1", sha1) 68 | prop.store( 69 | new FileOutputStream(releasePropertiesPath.toString), 70 | "Automatically generated by ReleaseManagement") 71 | 72 | gitCommitSavedEnvironment(Some(versionToString(newVersion))) 73 | 74 | None 75 | } 76 | 77 | lazy val finalizeRelease = finalizeReleaseTask 78 | 79 | def publishReleaseTask = interactiveTask { 80 | // publishing local is required first with subprojects for hard to understand reasons 81 | val cmd = if (!subProjects.isEmpty) { 82 | "sbt +publish-local +publish" 83 | } else { 84 | "sbt +publish" 85 | } 86 | // set NO_PROJECT_DEPS so the nested publish doesn't depend on transient versions. 87 | val exitCode = Process(cmd, None, ("NO_PROJECT_DEPS", "1")).run().exitValue() 88 | if (exitCode == 0) { 89 | None 90 | } else { 91 | Some(cmd + " exit code " + exitCode) 92 | } 93 | } 94 | 95 | val PublishReleaseDescription = "Publish a release to maven. commits and tags version in git." 96 | 97 | lazy val publishRelease = { 98 | (info.parent match { 99 | case Some(_: ReleaseManagement) => 100 | // skip, let parent do the work 101 | task { None } 102 | 103 | case _ => 104 | task { log.info("Publishing new release: " + projectVersion.value.stripSnapshot()); None } && 105 | prepareForReleaseTask && 106 | publishReleaseTask && 107 | finalizeReleaseTask && 108 | task { log.info("Don't forget to push the version change to origin"); None } 109 | }) describedAs PublishReleaseDescription 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/SourceControlledProject.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | import Process._ 5 | 6 | 7 | trait ScmAdapter { 8 | def isARepository: Boolean 9 | def currentRevision: String 10 | 11 | /** 12 | * A string containing the last few commits to the repository 13 | */ 14 | def lastFewCommits: String 15 | 16 | /** 17 | * The name of the branch that the build came from 18 | */ 19 | def branchName: String 20 | } 21 | 22 | object ScmAdapters { 23 | val Git = new ScmAdapter { 24 | override def isARepository = ("git status" ! NullLogger) == 0 25 | override def currentRevision = ("git rev-parse HEAD" !! NullLogger).trim 26 | override def lastFewCommits = ("git log --oneline --decorate --max-count=10" !! NullLogger).trim 27 | override def branchName = ("git symbolic-ref HEAD" !! NullLogger).trim 28 | } 29 | } 30 | 31 | trait SourceControlledProject extends Project { 32 | val defaultAdapterOrdering = Seq(ScmAdapters.Git) 33 | def adapters = adapterOrdering 34 | def adapterOrdering = defaultAdapterOrdering 35 | lazy val foundAdapters = adapters.filter(_.isARepository) 36 | 37 | def currentRevision: Option[String] = foundAdapters.firstOption.map { adapter => 38 | adapter.currentRevision 39 | } 40 | 41 | /** 42 | * A string containing the last few commits to the repository 43 | */ 44 | def lastFewCommits: Option[String] = foundAdapters.firstOption.map { adapter => 45 | adapter.lastFewCommits 46 | } 47 | 48 | /** 49 | * The name of the branch that the build came from 50 | */ 51 | def branchName: Option[String] = foundAdapters.firstOption.map { adapter => 52 | adapter.branchName 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/StandardProject.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | import java.io.File 5 | 6 | trait StandardManagedProject extends BasicManagedProject 7 | with SourceControlledProject 8 | with ReleaseManagement 9 | with Versions 10 | with Environmentalist 11 | { 12 | override def disableCrossPaths = true 13 | override def managedStyle = ManagedStyle.Maven 14 | 15 | // resolvers that will be used even if we're going through a proxy resolver 16 | def localRepos: Set[Resolver] = Set() 17 | } 18 | 19 | trait StandardTestableProject extends DefaultProject with StandardManagedProject { 20 | override def dependencyPath = "libs" 21 | 22 | // override ivy cache 23 | override def ivyCacheDirectory = environment.get("SBT_CACHE").map { cacheDir => 24 | Path.fromFile(new File(cacheDir)) 25 | } 26 | 27 | // local repositories 28 | val localLibs = Resolver.file("local-libs", new File("libs"))(Patterns("[artifact]-[revision].[ext]")) transactional() 29 | override def localRepos = super.localRepos + localLibs 30 | 31 | override def compileOrder = CompileOrder.JavaThenScala 32 | 33 | // turn on more warnings. 34 | override def compileOptions = super.compileOptions ++ 35 | Seq(Unchecked) ++ 36 | compileOptions("-encoding", "utf8") ++ 37 | compileOptions("-deprecation") 38 | 39 | override def testOptions = { 40 | (environment.get("NO_TESTS") orElse environment.get("NO_TEST")).toList 41 | .map(_ => TestFilter(_ => false)) ++ super.testOptions 42 | } 43 | } 44 | 45 | class StandardProject(info: ProjectInfo) extends DefaultProject(info) 46 | with StandardTestableProject 47 | with Ramdiskable 48 | with DependencyChecking 49 | with PublishLocalWithMavenStyleBasePattern 50 | with PublishSourcesAndJavadocs 51 | with BuildProperties 52 | with IntransitiveCompiles 53 | { 54 | // need to add mainResourcesOutputPath so the build.properties file can be found. 55 | override def consoleAction = interactiveTask { 56 | val console = new Console(buildCompiler) 57 | val classpath = consoleClasspath +++ mainResourcesOutputPath 58 | console(classpath.get, compileOptions.map(_.asString), "", log) 59 | } dependsOn(writeBuildProperties) 60 | 61 | // need to add mainResourcesOutputPath so the build.properties file can be found. 62 | override def runAction = task { args => 63 | val classpath = runClasspath +++ mainResourcesOutputPath 64 | runTask(getMainClass(true), classpath, args) dependsOn(compile, writeBuildProperties) 65 | } 66 | 67 | lazy val runClass = runClassAction 68 | 69 | def runClassAction = task { args => 70 | if (args.isEmpty) { 71 | task { Some("class name expected as argument") } 72 | } else { 73 | val mainClass = args(0) 74 | val mainArgs = args.drop(1) 75 | val classpath = runClasspath +++ mainResourcesOutputPath 76 | runTask(Some(mainClass), classpath, mainArgs) 77 | .dependsOn(compile, writeBuildProperties) 78 | } 79 | } describedAs("Run the main method of a specified class.") 80 | 81 | override def packageAction = super.packageAction dependsOn(testAction) 82 | 83 | log.info("Standard project rules " + BuildInfo.version + " loaded (" + BuildInfo.date + ").") 84 | } 85 | 86 | class StandardParentProject(info: ProjectInfo) extends ParentProject(info) 87 | with StandardManagedProject 88 | with PublishLocalWithMavenStyleBasePattern 89 | { 90 | override def usesMavenStyleBasePatternInPublishLocalConfiguration = false 91 | } 92 | 93 | /** 94 | * Nothing special here yet, but this class could accumulate traits that 95 | * are specific to libraries 96 | */ 97 | class StandardLibraryProject(info: ProjectInfo) extends StandardProject(info) 98 | with PackageDist 99 | with PublishSourcesAndJavadocs 100 | 101 | /** 102 | * A standard project type for building services. 103 | */ 104 | class StandardServiceProject(info: ProjectInfo) extends StandardProject(info) 105 | with PackageDist 106 | with PublishSourcesAndJavadocs 107 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/StrictDependencies.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | import org.apache.ivy.plugins._ 5 | 6 | trait StrictDependencies extends BasicManagedProject { 7 | override def ivySbt: IvySbt = { 8 | val i = super.ivySbt 9 | i.withIvy { apacheIvy => 10 | val stricty = apacheIvy.getSettings().getConflictManager("strict") 11 | stricty.asInstanceOf[IvySettingsAware].setSettings(apacheIvy.getSettings()) 12 | apacheIvy.getSettings().setDefaultConflictManager(stricty) 13 | } 14 | i 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/SubversionPublisher.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import java.io.FileReader 4 | import java.util.Properties 5 | import fm.last.ivy.plugins.svnresolver.SvnResolver 6 | import _root_.sbt._ 7 | 8 | /** 9 | * Semi-hacky way to publish to a subversion-based maven repository, using ivy-svn. 10 | */ 11 | trait SubversionPublisher extends BasicManagedProject { 12 | private val prefs = new Properties() 13 | val prefsFilename = System.getProperty("user.home") + "/.svnrepo" 14 | 15 | // override me to publish to subversion. 16 | def subversionRepository: Option[String] = info.parent match { 17 | case Some(parent: SubversionPublisher) => parent.subversionRepository 18 | case _ => None 19 | } 20 | 21 | private val loaded = try { 22 | prefs.load(new FileReader(prefsFilename)) 23 | true 24 | } catch { 25 | case e: Exception => 26 | log.warn("No .svnrepo file; no svn repo will be configured.") 27 | false 28 | } 29 | 30 | lazy val subversionResolver = { 31 | if (loaded) { 32 | subversionRepository.map { repo => 33 | val resolver = new SvnResolver() 34 | resolver.setName("svn") 35 | resolver.setRepositoryRoot(repo) 36 | resolver.addArtifactPattern(prefs.getProperty("pattern", "[organisation]/[module]/[revision]/[artifact]-[revision](-[classifier]).[ext]")) 37 | resolver.setM2compatible(java.lang.Boolean.parseBoolean(prefs.getProperty("m2Compatible", "true"))) 38 | 39 | val username = prefs.getProperty("username") 40 | if (username ne null) { 41 | resolver.setUserName(username) 42 | } 43 | val password = prefs.getProperty("password") 44 | if (password eq null) { 45 | // Try to prompt the user for a password. 46 | val console = System.console 47 | if (console ne null) { 48 | // This is super janky -- it seems that sbt hoses the 49 | // console in a way so that it isn't line-buffered anymore, 50 | // or for some other reason causes Console.readPassword to 51 | // give us only one character at time. 52 | def readPassword: Stream[Char] = { 53 | val chars = console.readPassword("SVN repository password: ") 54 | if ((chars eq null) || chars.isEmpty) 55 | Stream.empty 56 | else 57 | Stream.concat(Stream.fromIterator(chars.elements), readPassword) 58 | } 59 | 60 | resolver.setUserPassword(new String(readPassword.toArray)) 61 | } 62 | } else { 63 | resolver.setUserPassword(password) 64 | } 65 | resolver.setBinaryDiff("true") 66 | resolver.setBinaryDiffFolderName(".upload") 67 | resolver.setCleanupPublishFolder("true") 68 | resolver 69 | } 70 | } else { 71 | None 72 | } 73 | } 74 | 75 | override def ivySbt: IvySbt = { 76 | val i = super.ivySbt 77 | subversionResolver.foreach { resolver => 78 | i.withIvy { _.getSettings().addResolver(resolver) } 79 | } 80 | i 81 | } 82 | 83 | override def publishConfiguration: DefaultPublishConfiguration = { 84 | subversionResolver match { 85 | case None => 86 | super.publishConfiguration 87 | case Some(resolver) => 88 | new DefaultPublishConfiguration(resolver.getName(), "release", true) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/Tartifactory.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import java.io.File 4 | import scala.collection.jcl 5 | import java.io.{BufferedReader, InputStreamReader} 6 | import _root_.sbt._ 7 | 8 | trait Tartifactory { 9 | def artifactoryRoot = "http://artifactory.local.twitter.com" 10 | def proxyRepo = "repo" 11 | def snapshotDeployRepo = "libs-snapshots-local" 12 | def releaseDeployRepo = "libs-releases-local" 13 | } 14 | 15 | trait TartifactoryPublisher extends BasicManagedProject with Tartifactory { 16 | override def managedStyle = ManagedStyle.Maven 17 | 18 | val publishTo = if (version.toString.endsWith("SNAPSHOT")) { 19 | "Twitter Artifactory" at (artifactoryRoot + "/" + snapshotDeployRepo) 20 | } else { 21 | "Twitter Artifactory" at (artifactoryRoot + "/" + releaseDeployRepo) 22 | } 23 | 24 | override def publishTask(module: => IvySbt#Module, publishConfiguration: => PublishConfiguration) = task { 25 | val stdinReader = new BufferedReader(new InputStreamReader(System.in)) 26 | System.out.print("enter your artifactory username: ") 27 | val username = stdinReader.readLine 28 | System.out.print("\nentire your artifactory password: ") 29 | val password = stdinReader.readLine 30 | Credentials.add("Artifactory Realm", "artifactory.local.twitter.com", username, password) 31 | super.publishTask(module, publishConfiguration).run 32 | } 33 | } 34 | 35 | @deprecated //("just use DefaultRepos") 36 | trait TartifactoryRepos extends DefaultRepos 37 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/TemplateProject.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | // from http://github.com/debasishg/sjson/blob/master/project/build/TemplateProject.scala 4 | 5 | import _root_.sbt._ 6 | import FileUtilities.{clean, createDirectory} 7 | 8 | trait TemplateProject extends DefaultProject with FileTasks 9 | { 10 | // declares fmpp as a managed dependency. By declaring it in the private 'fmpp' configuration, it doesn't get published 11 | val fmppDep = "net.sourceforge.fmpp" % "fmpp" % "0.9.13" % "fmpp" 12 | val fmppConf = config("fmpp") hide 13 | def fmppClasspath = configurationClasspath(fmppConf) 14 | 15 | // declare the directory structure for the processed sources 16 | def srcManaged: Path = "src_managed" 17 | override def mainScalaSourcePath = srcManaged / "main" 18 | override def testScalaSourcePath = srcManaged / "test" 19 | 20 | // declare the directory structure for the templates 21 | def srcRoot: Path = "src" / "main" / "scala" 22 | def testSrcRoot: Path = "src" / "test" / "scala" 23 | 24 | // global variables available within templates 25 | def fmppTemplateData = { 26 | val versionListWithDefaults = buildScalaVersion.split("\\.", 4).toList ::: List("_") 27 | val vMajor :: vMinor :: vPatch :: vExtra :: _ = versionListWithDefaults 28 | Map("scalaVersion" -> (vMajor +"."+ vMinor), 29 | "fullScalaVersion" -> buildScalaVersion, 30 | "scalaVersionMajor" -> vMajor, 31 | "scalaVersionMinor" -> vMinor, 32 | "scalaVersionPatch" -> vPatch, 33 | "scalaVersionExtra" -> vExtra) 34 | } 35 | def fmppTemplateDataString = fmppTemplateData.map { case (k,v) => k +": "+ v }.mkString(", ") 36 | 37 | // arguments to fmpp 38 | def fmppArgs = "--ignore-temporary-files" :: "-D" :: fmppTemplateDataString :: Nil 39 | 40 | // creates a task that invokes fmpp 41 | def fmppTask(args: => List[String], output: => Path, srcRoot: => Path, sources: PathFinder) = { 42 | runTask(Some("fmpp.tools.CommandLine"), fmppClasspath, 43 | "-U" :: "all" :: "-S" :: srcRoot.absolutePath :: "-O" :: output.absolutePath :: args ::: sources.getPaths.toList) 44 | } 45 | 46 | // Define template actions and make them run before compilation 47 | 48 | lazy val template = fmppTask(fmppArgs, mainScalaSourcePath, srcRoot, sources(srcRoot)) 49 | lazy val testTemplate = fmppTask(fmppArgs, testScalaSourcePath, testSrcRoot, sources(testSrcRoot)) 50 | 51 | override def compileAction = super.compileAction dependsOn(template) 52 | override def testCompileAction = super.testCompileAction dependsOn(testTemplate) 53 | } 54 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/UnpublishedProject.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt._ 4 | 5 | trait UnpublishedProject extends BasicManagedProject { 6 | override def publishTask(module: => IvySbt#Module, publishConfiguration: => PublishConfiguration) = task { 7 | log.info(name + ": skipping publish") 8 | None 9 | } 10 | 11 | override def deliverTask(module: => IvySbt#Module, deliverConfiguration: => PublishConfiguration, logging: => UpdateLogging.Value) = task { 12 | log.info(name + ": skipping deliver") 13 | None 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/sbt/Versions.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.sbt 2 | 3 | import _root_.sbt.{BasicManagedProject, BasicDependencyProject, Version} 4 | import pimpedversion._ 5 | 6 | trait Versions extends BasicManagedProject with GitHelpers { 7 | def versionBumpTask(newVersion: => Version) = task { 8 | info.parent match { 9 | case Some(_: Versions) => 10 | // this is a sub-project, don't change version here, let the parent do it 11 | None 12 | 13 | case _ => 14 | log.info("Current version: " + projectVersion.value) 15 | projectVersion.update(newVersion) 16 | log.info("New version: " + projectVersion.value) 17 | saveEnvironment() 18 | 19 | gitCommitSavedEnvironment(Some(projectVersion.value.toString)) 20 | 21 | None 22 | } 23 | } 24 | 25 | lazy val versionBump = versionBumpTask(projectVersion.value.incMicro()) 26 | .describedAs("bump patch version") 27 | 28 | lazy val versionBumpMinor = versionBumpTask(projectVersion.value.incMinor()) 29 | .describedAs("bump minor version") 30 | 31 | lazy val versionBumpMajor = versionBumpTask(projectVersion.value.incMajor()) 32 | .describedAs("bump major version") 33 | } 34 | --------------------------------------------------------------------------------