mirror of https://github.com/sbt/sbt.git
* Removed build scripts and manual dependencies
* useDefaultConfigurations supersedes useMavenConfigurations and is now true by default * Moved installer-plugin to its own independent project as an sbt plugin * bumped version for 0.5 release * Updated project excludes for plugins * Specifying the explicit URL for dependency now infers the extension and type from the URL * Can load credentials from a properties file instead of adding them inline * Added help for '+' * Added method configurationPath to get the path to the directory containing dependencies downloaded for a Configuration * managedStyle = ManagedStyle.Maven by default now git-svn-id: https://simple-build-tool.googlecode.com/svn/trunk@813 d89573ee-9141-11dd-94d4-bdf5e562f29c
This commit is contained in:
commit
7f92cc5f4c
|
|
@ -0,0 +1,25 @@
|
|||
Copyright (c) 2008, 2009 Steven Blundy, Mark Harrah, David MacIver, Mikko Peltonen
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. The name of the author may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
Simple Build Tool (sbt)
|
||||
Copyright 2008, 2009 Steven Blundy, Mark Harrah, David MacIver, Mikko Peltonen
|
||||
|
||||
|
||||
Portions based on code by Pete Kirkham in Nailgun
|
||||
Copyright 2004, Martian Software, Inc
|
||||
Licensed under the Apache License, Version 2.0
|
||||
(see licenses/LICENSE_Apache)
|
||||
|
||||
Portions based on code from the Scala compiler
|
||||
Copyright 2002-2008 EPFL, Lausanne
|
||||
Licensed under BSD-style license (see licenses/LICENSE_Scala)
|
||||
|
||||
Portions based on code from specs
|
||||
Copyright (c) 2007-2008 Eric Torreborre
|
||||
Licensed under MIT license (see licenses/LICENSE_specs)
|
||||
|
||||
The following test frameworks are distributed with sbt (in
|
||||
the subversion repository):
|
||||
specs (see licenses/LICENSE_specs)
|
||||
ScalaCheck (see licenses/LICENSE_ScalaCheck)
|
||||
ScalaTest (see licenses/LICENSE_Apache)
|
||||
|
||||
Jetty is distributed with sbt (in the subversion repository) and is
|
||||
licensed under the Apache License, Version 2.0 (see
|
||||
licenses/LICENSE_Apache).
|
||||
|
||||
ScalaTest is distributed with sbt (in the subversion repository)
|
||||
and requires the following notice:
|
||||
|
||||
This product includes software developed by
|
||||
Artima, Inc. (http://www.artima.com/).
|
||||
|
||||
|
||||
Apache Ivy, licensed under the Apache License, Version 2.0
|
||||
(see licenses/LICENSE_Apache) is distributed with sbt and
|
||||
requires the following notice:
|
||||
|
||||
This product includes software developed by
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
Portions of Ivy were originally developed by
|
||||
Jayasoft SARL (http://www.jayasoft.fr/)
|
||||
and are licensed to the Apache Software Foundation under the
|
||||
"Software Grant License Agreement"
|
||||
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
Copyright (c) 2009 Mark Harrah
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. The name of the author may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
Simple Build Tool (sbt)
|
||||
Copyright 2009 Mark Harrah
|
||||
|
||||
Parts of the Scala library are distributed with sbt:
|
||||
Copyright 2002-2008 EPFL, Lausanne
|
||||
Licensed under BSD-style license (see licenses/LICENSE_Scala)
|
||||
|
||||
JLine is distributed with sbt:
|
||||
Copyright (c) 2002-2006, Marc Prud'hommeaux <mwp1@cornell.edu>
|
||||
Licensed under BSD-style license (see licenses/LICENSE_JLine)
|
||||
|
||||
Apache Ivy, licensed under the Apache License, Version 2.0
|
||||
(see licenses/LICENSE_Apache) is distributed with sbt and
|
||||
requires the following notice:
|
||||
|
||||
This product includes software developed by
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
Portions of Ivy were originally developed by
|
||||
Jayasoft SARL (http://www.jayasoft.fr/)
|
||||
and are licensed to the Apache Software Foundation under the
|
||||
"Software Grant License Agreement"
|
||||
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Binary file not shown.
|
|
@ -0,0 +1,258 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
License for JCraft JSch package
|
||||
------------------------------------------------------------------------------
|
||||
Copyright (c) 2002,2003,2004,2005,2006,2007 Atsuhiko Yamanaka, JCraft,Inc.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. The names of the authors may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
|
||||
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
|
||||
INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
||||
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
License for jQuery
|
||||
------------------------------------------------------------------------------
|
||||
Copyright (c) 2007 John Resig, http://jquery.com/
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
Copyright (c) 2002-2006, Marc Prud'hommeaux <mwp1@cornell.edu>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or
|
||||
without modification, are permitted provided that the following
|
||||
conditions are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with
|
||||
the distribution.
|
||||
|
||||
Neither the name of JLine nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this
|
||||
software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
|
||||
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
||||
EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
|
||||
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
||||
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
||||
OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
SCALA LICENSE
|
||||
|
||||
Copyright (c) 2002-2008 EPFL, Lausanne, unless otherwise specified.
|
||||
All rights reserved.
|
||||
|
||||
This software was developed by the Programming Methods Laboratory of the
|
||||
Swiss Federal Institute of Technology (EPFL), Lausanne, Switzerland.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software in source
|
||||
or binary form for any purpose with or without fee is hereby granted,
|
||||
provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the EPFL nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this
|
||||
software without specific prior written permission.
|
||||
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGE.
|
||||
|
|
@ -0,0 +1,310 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt.boot
|
||||
|
||||
// This is the main class for the sbt launcher. Its purpose is to ensure the appropriate
|
||||
// versions of sbt and scala are downloaded to the projects 'project/boot' directory.
|
||||
// Then, the downloaded version of sbt is started as usual using the right version of
|
||||
// scala.
|
||||
|
||||
// Artifact names must be consistent between the main sbt build and this build.
|
||||
|
||||
import java.io.{File, FileFilter}
|
||||
import java.net.{MalformedURLException, URL, URLClassLoader}
|
||||
|
||||
// contains constants and paths
|
||||
import BootConfiguration._
|
||||
import UpdateTarget.{UpdateScala, UpdateSbt}
|
||||
|
||||
// The exception to use when an error occurs at the launcher level (and not a nested exception).
|
||||
// This indicates overrides toString because the exception class name is not needed to understand
|
||||
// the error message.
|
||||
private class BootException(override val toString: String) extends RuntimeException
|
||||
// The entry point to the launcher
|
||||
object Boot
|
||||
{
|
||||
def main(args: Array[String])
|
||||
{
|
||||
System.setProperty("sbt.boot", true.toString)
|
||||
checkProxy()
|
||||
try { boot(args) }
|
||||
catch
|
||||
{
|
||||
case b: BootException => errorAndExit(b)
|
||||
case e =>
|
||||
e.printStackTrace
|
||||
errorAndExit(e)
|
||||
}
|
||||
System.exit(0)
|
||||
}
|
||||
private def errorAndExit(e: Throwable)
|
||||
{
|
||||
System.out.println("Error during sbt execution: " + e.toString)
|
||||
System.exit(1)
|
||||
}
|
||||
def boot(args: Array[String])
|
||||
{
|
||||
// prompt to create project if it doesn't exist.
|
||||
// will not return if user declines
|
||||
(new Paths).checkProject()
|
||||
val loaderCache = new LoaderCache
|
||||
if(args.length == 0)
|
||||
load(args, loaderCache) // interactive mode, which can only use one version of scala for a run
|
||||
else
|
||||
runBatch(args.toList, Nil, loaderCache) // batch mode, which can reboot with a different scala version
|
||||
}
|
||||
private def runBatch(args: List[String], accumulateReversed: List[String], loaderCache: LoaderCache)
|
||||
{
|
||||
def doLoad() = if(!accumulateReversed.isEmpty) load(accumulateReversed.reverse.toArray, loaderCache)
|
||||
args match
|
||||
{
|
||||
case Nil => doLoad()
|
||||
case RebootCommand :: tail =>
|
||||
doLoad()
|
||||
runBatch(tail, Nil, loaderCache)
|
||||
case action :: tail if action.trim.startsWith(CrossBuildPrefix) =>
|
||||
doLoad()
|
||||
load(Array(action), loaderCache) // call main with the single cross-build argument, preserving the '+' prefix, with which it knows what to do
|
||||
runBatch(tail, Nil, loaderCache)
|
||||
case notReload :: tail => runBatch(tail, notReload :: accumulateReversed, loaderCache)
|
||||
}
|
||||
}
|
||||
/** Loads the project in the current working directory using the version of scala and sbt
|
||||
* declared in the build. The class loader used prevents the Scala and Ivy classes used by
|
||||
* this loader from being seen by the loaded sbt/project.*/
|
||||
private def load(args: Array[String], loaderCache: LoaderCache)
|
||||
{
|
||||
val loader = (new Setup(loaderCache)).loader()
|
||||
val sbtMain = Class.forName(SbtMainClass, true, loader)
|
||||
val exitCode = run(sbtMain, args)
|
||||
if(exitCode == NormalExitCode)
|
||||
()
|
||||
else if(exitCode == RebootExitCode)
|
||||
load(args, loaderCache)
|
||||
else
|
||||
System.exit(exitCode)
|
||||
}
|
||||
private def run(sbtMain: Class[_], args: Array[String]): Int =
|
||||
{
|
||||
try {
|
||||
// Versions newer than 0.3.8 enter through the run method, which does not call System.exit
|
||||
val runMethod = sbtMain.getMethod(MainMethodName, classOf[Array[String]])
|
||||
runMethod.invoke(null, Array(args) : _*).asInstanceOf[Int]
|
||||
} catch {
|
||||
case e: NoSuchMethodException => runOld(sbtMain, args)
|
||||
}
|
||||
}
|
||||
/** The entry point for version 0.3.8 was the main method. */
|
||||
private def runOld(sbtMain: Class[_], args: Array[String]): Int =
|
||||
{
|
||||
val runMethod = sbtMain.getMethod(OldMainMethodName, classOf[Array[String]])
|
||||
runMethod.invoke(null, Array(args) : _*)
|
||||
NormalExitCode
|
||||
}
|
||||
|
||||
private def checkProxy()
|
||||
{
|
||||
import ProxyProperties._
|
||||
val httpProxy = System.getenv(HttpProxyEnv)
|
||||
if(isDefined(httpProxy) && !isPropertyDefined(ProxyHost) && !isPropertyDefined(ProxyPort))
|
||||
{
|
||||
try
|
||||
{
|
||||
val proxy = new URL(httpProxy)
|
||||
setProperty(ProxyHost, proxy.getHost)
|
||||
val port = proxy.getPort
|
||||
if(port >= 0)
|
||||
System.setProperty(ProxyPort, port.toString)
|
||||
copyEnv(HttpProxyUser, ProxyUser)
|
||||
copyEnv(HttpProxyPassword, ProxyPassword)
|
||||
}
|
||||
catch
|
||||
{
|
||||
case e: MalformedURLException =>
|
||||
System.out.println("Warning: could not parse http_proxy setting: " + e.toString)
|
||||
}
|
||||
}
|
||||
}
|
||||
private def copyEnv(envKey: String, sysKey: String) { setProperty(sysKey, System.getenv(envKey)) }
|
||||
private def setProperty(key: String, value: String) { if(value != null) System.setProperty(key, value) }
|
||||
private def isPropertyDefined(k: String) = isDefined(System.getProperty(k))
|
||||
private def isDefined(s: String) = s != null && !s.isEmpty
|
||||
}
|
||||
|
||||
private class Paths extends NotNull
|
||||
{
|
||||
protected final val ProjectDirectory = new File(ProjectDirectoryName)
|
||||
protected final val BootDirectory = new File(ProjectDirectory, BootDirectoryName)
|
||||
protected final val PropertiesFile = new File(ProjectDirectory, BuildPropertiesName)
|
||||
|
||||
final def checkProject()
|
||||
{
|
||||
if(!ProjectDirectory.exists)
|
||||
{
|
||||
val line = SimpleReader.readLine("Project does not exist, create new project? (y/N/s) : ")
|
||||
if(Setup.isYes(line))
|
||||
ProjectProperties(PropertiesFile, true)
|
||||
else if(Setup.isScratch(line))
|
||||
ProjectProperties.scratch(PropertiesFile)
|
||||
else
|
||||
System.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
/** A class to handle setting up the properties and classpath of the project
|
||||
* before it is loaded. */
|
||||
private class Setup(loaderCache: LoaderCache) extends Paths
|
||||
{
|
||||
/** Checks that the requested version of sbt and scala have been downloaded.
|
||||
* It performs a simple check that the appropriate directories exist. It uses Ivy
|
||||
* to resolve and retrieve any necessary libraries. The classpath to use is returned.*/
|
||||
final def loader(): ClassLoader = loader(Nil)
|
||||
private final def loader(forcePrompt: Seq[String]): ClassLoader =
|
||||
{
|
||||
val (normalScalaVersion, sbtVersion) = ProjectProperties.forcePrompt(PropertiesFile, forcePrompt : _*)
|
||||
val scalaVersion = crossScalaVersion(normalScalaVersion)
|
||||
loaderCache( scalaVersion, sbtVersion ) match
|
||||
{
|
||||
case Some(existingLoader) =>
|
||||
{
|
||||
setScalaVersion(scalaVersion)
|
||||
existingLoader
|
||||
}
|
||||
case None =>
|
||||
{
|
||||
getLoader(scalaVersion, sbtVersion) match
|
||||
{
|
||||
case Left(retry) => loader(retry)
|
||||
case Right(classLoader) => classLoader
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private def crossScalaVersion(simpleScalaVersion: String): String =
|
||||
{
|
||||
val crossScalaVersion = System.getProperty(SbtScalaVersionKey)
|
||||
if(crossScalaVersion == null || crossScalaVersion.isEmpty)
|
||||
simpleScalaVersion
|
||||
else
|
||||
crossScalaVersion
|
||||
}
|
||||
private def getLoader(scalaVersion: String, sbtVersion: String): Either[Seq[String], ClassLoader] =
|
||||
{
|
||||
import Setup.{failIfMissing,isYes,needsUpdate}
|
||||
import ProjectProperties.{ScalaVersionKey, SbtVersionKey}
|
||||
|
||||
val baseDirectory = new File(BootDirectory, baseDirectoryName(scalaVersion))
|
||||
System.setProperty(ScalaHomeProperty, baseDirectory.getAbsolutePath)
|
||||
val scalaDirectory = new File(baseDirectory, ScalaDirectoryName)
|
||||
val sbtDirectory = new File(baseDirectory, sbtDirectoryName(sbtVersion))
|
||||
|
||||
val classLoader = createLoader(scalaDirectory, sbtDirectory)
|
||||
val updateTargets = needsUpdate("", classLoader, TestLoadScalaClasses, UpdateScala) ::: needsUpdate(sbtVersion, classLoader, TestLoadSbtClasses, UpdateSbt)
|
||||
if(updateTargets.isEmpty) // avoid loading Ivy related classes if there is nothing to update
|
||||
success(classLoader, scalaVersion, sbtVersion)
|
||||
else
|
||||
{
|
||||
Update(baseDirectory, sbtVersion, scalaVersion, updateTargets: _*)
|
||||
|
||||
val classLoader = createLoader(scalaDirectory, sbtDirectory)
|
||||
val sbtFailed = failIfMissing(classLoader, TestLoadSbtClasses, "sbt " + sbtVersion, SbtVersionKey)
|
||||
val scalaFailed = failIfMissing(classLoader, TestLoadScalaClasses, "Scala " + scalaVersion, ScalaVersionKey)
|
||||
|
||||
(scalaFailed +++ sbtFailed) match
|
||||
{
|
||||
case Success => success(classLoader, scalaVersion, sbtVersion)
|
||||
case f: Failure =>
|
||||
val noRetrieveMessage = "Could not retrieve " + f.label + "."
|
||||
val getNewVersions = SimpleReader.readLine(noRetrieveMessage + " Select different version? (y/N) : ")
|
||||
if(isYes(getNewVersions))
|
||||
Left(f.keys)
|
||||
else
|
||||
throw new BootException(noRetrieveMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
private def success(classLoader: ClassLoader, scalaVersion: String, sbtVersion: String) =
|
||||
{
|
||||
setScalaVersion(scalaVersion)
|
||||
loaderCache( scalaVersion, sbtVersion ) = classLoader
|
||||
Right(classLoader)
|
||||
}
|
||||
private def createLoader(dirs: File*) =
|
||||
{
|
||||
val classpath = Setup.getJars(dirs : _*)
|
||||
new URLClassLoader(classpath.toArray, new BootFilteredLoader)
|
||||
}
|
||||
private def setScalaVersion(scalaVersion: String) { System.setProperty(SbtScalaVersionKey, scalaVersion) }
|
||||
}
|
||||
private final class LoaderCache
|
||||
{
|
||||
private[this] var cachedSbtVersion: Option[String] = None
|
||||
private[this] val loaderMap = new scala.collection.mutable.HashMap[String, ClassLoader]
|
||||
def apply(scalaVersion: String, sbtVersion: String): Option[ClassLoader] =
|
||||
{
|
||||
cachedSbtVersion flatMap { currentSbtVersion =>
|
||||
if(sbtVersion == currentSbtVersion)
|
||||
loaderMap.get(scalaVersion)
|
||||
else
|
||||
None
|
||||
}
|
||||
}
|
||||
def update(scalaVersion: String, sbtVersion: String, loader: ClassLoader)
|
||||
{
|
||||
for(currentSbtVersion <- cachedSbtVersion)
|
||||
{
|
||||
if(sbtVersion != currentSbtVersion)
|
||||
loaderMap.clear()
|
||||
}
|
||||
cachedSbtVersion = Some(sbtVersion)
|
||||
loaderMap(scalaVersion) = loader
|
||||
}
|
||||
}
|
||||
private object Setup
|
||||
{
|
||||
private def failIfMissing(loader: ClassLoader, classes: Iterable[String], label: String, key: String) = checkTarget(loader, classes, Success, new Failure(label, List(key)))
|
||||
private def needsUpdate(version: String, loader: ClassLoader, classes: Iterable[String], target: UpdateTarget.Value) =
|
||||
if(version.endsWith("-SNAPSHOT"))
|
||||
target :: Nil
|
||||
else
|
||||
checkTarget(loader, classes, Nil, target :: Nil)
|
||||
private def checkTarget[T](loader: ClassLoader, classes: Iterable[String], ifSuccess: => T, ifFailure: => T): T =
|
||||
{
|
||||
try
|
||||
{
|
||||
for(c <- classes)
|
||||
Class.forName(c, false, loader)
|
||||
ifSuccess
|
||||
}
|
||||
catch { case e: ClassNotFoundException => ifFailure }
|
||||
}
|
||||
def isYes(so: Option[String]) = isValue("y", "yes")(so)
|
||||
def isScratch(so: Option[String]) = isValue("s", "scratch")(so)
|
||||
def isValue(values: String*)(so: Option[String]) =
|
||||
so match
|
||||
{
|
||||
case Some(s) => values.contains(s.toLowerCase)
|
||||
case None => false
|
||||
}
|
||||
private def getJars(directories: File*) = directories.flatMap(file => wrapNull(file.listFiles(JarFilter))).map(_.toURI.toURL)
|
||||
private def wrapNull(a: Array[File]): Array[File] = if(a == null) Array() else a
|
||||
}
|
||||
|
||||
|
||||
private object JarFilter extends FileFilter
|
||||
{
|
||||
def accept(file: File) = !file.isDirectory && file.getName.endsWith(".jar")
|
||||
}
|
||||
|
||||
private sealed trait Checked extends NotNull { def +++(o: Checked): Checked }
|
||||
private final object Success extends Checked { def +++(o: Checked) = o }
|
||||
private final class Failure(val label: String, val keys: List[String]) extends Checked
|
||||
{
|
||||
def +++(o: Checked) =
|
||||
o match
|
||||
{
|
||||
case Success => this
|
||||
case f: Failure => new Failure(label + " and " + f.label, keys ::: f.keys)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt.boot
|
||||
|
||||
// project/boot/ [BootDirectoryName]
|
||||
// scala-<version>/ [baseDirectoryName]
|
||||
// lib/ [ScalaDirectoryName]
|
||||
// sbt-<version>/ [sbtDirectoryName]
|
||||
//
|
||||
// see also ProjectProperties for the set of constants that apply to the build.properties file in a project
|
||||
private object BootConfiguration
|
||||
{
|
||||
val SbtMainClass = "sbt.Main"
|
||||
val MainMethodName = "run"
|
||||
val OldMainMethodName = "main"
|
||||
|
||||
// these are the module identifiers to resolve/retrieve
|
||||
val ScalaOrg = "org.scala-lang"
|
||||
val SbtOrg = "sbt"
|
||||
val CompilerModuleName = "scala-compiler"
|
||||
val LibraryModuleName = "scala-library"
|
||||
val SbtModuleName = "simple-build-tool"
|
||||
|
||||
/** The Ivy conflict manager to use for updating.*/
|
||||
val ConflictManagerName = "strict"
|
||||
/** The name of the local Ivy repository, which is used when compiling sbt from source.*/
|
||||
val LocalIvyName = "local"
|
||||
/** The pattern used for the local Ivy repository, which is used when compiling sbt from source.*/
|
||||
val LocalPattern = "[organisation]/[module]/[revision]/[type]s/[artifact].[ext]"
|
||||
/** The artifact pattern used for the local Ivy repository.*/
|
||||
def LocalArtifactPattern = LocalPattern
|
||||
/** The Ivy pattern used for the local Ivy repository.*/
|
||||
def LocalIvyPattern = LocalPattern
|
||||
|
||||
/** The name of the property declaring the version of scala to use to build the project when not cross-building.*/
|
||||
val ScalaVersion = "scala.version"
|
||||
/** The name of the property declaring the version of sbt to use to build the project.*/
|
||||
val SbtVersion = "sbt.version"
|
||||
/** The name of the system property containing the version of scala actually used to build a project.
|
||||
* This might be different from the ScalaVersion property when cross-building.*/
|
||||
val SbtScalaVersionKey = "sbt.scala.version"
|
||||
/** The class name prefix used to hide the Scala classes used by this loader from sbt
|
||||
* and the project definition*/
|
||||
val ScalaPackage = "scala."
|
||||
/** The class name prefix used to hide the Ivy classes used by this loader from sbt
|
||||
* and the project definition*/
|
||||
val IvyPackage = "org.apache.ivy."
|
||||
/** The loader will check that these classes can be loaded and will assume that their presence indicates
|
||||
* sbt and its dependencies have been downloaded.*/
|
||||
val TestLoadSbtClasses = "sbt.Main" :: "org.apache.ivy.Ivy" :: Nil
|
||||
/** The loader will check that these classes can be loaded and will assume that their presence indicates
|
||||
* the Scala compiler and library have been downloaded.*/
|
||||
val TestLoadScalaClasses = "scala.ScalaObject" :: "scala.tools.nsc.GenericRunnerCommand" :: Nil
|
||||
|
||||
val ProjectDirectoryName = "project"
|
||||
val BootDirectoryName = "boot"
|
||||
val BuildPropertiesName ="build.properties"
|
||||
val ScalaHomeProperty = "scala.home"
|
||||
val UpdateLogName = "update.log"
|
||||
|
||||
val CrossBuildPrefix = "+"
|
||||
val RebootCommand = "reboot"
|
||||
val RebootExitCode = -1
|
||||
val NormalExitCode = 0
|
||||
val DefaultIvyConfiguration = "default"
|
||||
|
||||
/** The base URL to use to resolve sbt for download. */
|
||||
val sbtRootBase = "http://simple-build-tool.googlecode.com/svn/artifacts/"
|
||||
/** The name of the directory within the boot directory to retrieve scala to. */
|
||||
val ScalaDirectoryName = "lib"
|
||||
/** The Ivy pattern to use for retrieving the scala compiler and library. It is relative to the directory
|
||||
* containing all jars for the requested version of scala. */
|
||||
val scalaRetrievePattern = ScalaDirectoryName + "/[artifact].[ext]"
|
||||
|
||||
/** The Ivy pattern to use for retrieving sbt and its dependencies. It is relative to the directory
|
||||
* containing all jars for the requested version of scala. */
|
||||
def sbtRetrievePattern(sbtVersion: String) = sbtDirectoryName(sbtVersion) + "/[artifact]-[revision].[ext]"
|
||||
/** The Ivy pattern to use for resolving sbt and its dependencies from the Google code project.*/
|
||||
def sbtResolverPattern(scalaVersion: String) = sbtRootBase + "[revision]/[type]s/[artifact].[ext]"
|
||||
/** The name of the directory to retrieve sbt and its dependencies to.*/
|
||||
def sbtDirectoryName(sbtVersion: String) = SbtOrg + "-" + sbtVersion
|
||||
/** The name of the directory in the boot directory to put all jars for the given version of scala in.*/
|
||||
def baseDirectoryName(scalaVersion: String) = "scala-" + scalaVersion
|
||||
}
|
||||
private object ProxyProperties
|
||||
{
|
||||
val HttpProxyEnv = "http_proxy"
|
||||
val HttpProxyUser = "http_proxy_user"
|
||||
val HttpProxyPassword = "http_proxy_pass"
|
||||
|
||||
val ProxyHost = "http.proxyHost"
|
||||
val ProxyPort = "http.proxyPort"
|
||||
val ProxyUser = "http.proxyUser"
|
||||
val ProxyPassword = "http.proxyPassword"
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt.boot
|
||||
|
||||
import BootConfiguration._
|
||||
|
||||
/** A custom class loader to ensure the main part of sbt doesn't load any Scala or
|
||||
* Ivy classes from the jar containing the loader. */
|
||||
private[boot] final class BootFilteredLoader extends ClassLoader with NotNull
|
||||
{
|
||||
@throws(classOf[ClassNotFoundException])
|
||||
override final def loadClass(className: String, resolve: Boolean): Class[_] =
|
||||
{
|
||||
if(className.startsWith(ScalaPackage) || className.startsWith(IvyPackage))
|
||||
throw new ClassNotFoundException(className)
|
||||
else
|
||||
super.loadClass(className, resolve)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt.boot
|
||||
|
||||
/*
|
||||
Project does not exist, create new project? [y/N] y
|
||||
Name:
|
||||
Organization []:
|
||||
Version [1.0]:
|
||||
Scala version [2.7.5]:
|
||||
sbt version [0.5]:
|
||||
*/
|
||||
import java.io.File
|
||||
/** Constants related to reading/writing the build.properties file in a project.
|
||||
* See BootConfiguration for general constants used by the loader. */
|
||||
private object ProjectProperties
|
||||
{
|
||||
/** The properties key for storing the name of the project.*/
|
||||
val NameKey = "project.name"
|
||||
/** The properties key for storing the organization of the project.*/
|
||||
val OrganizationKey = "project.organization"
|
||||
/** The properties key for storing the version of the project.*/
|
||||
val VersionKey = "project.version"
|
||||
/** The properties key for storing the version of Scala used with the project.*/
|
||||
val ScalaVersionKey = "scala.version"
|
||||
/** The properties key for storing the version of sbt used to build the project.*/
|
||||
val SbtVersionKey = "sbt.version"
|
||||
/** The properties key to communicate to the main component of sbt that the project
|
||||
* should be initialized after being loaded, typically by creating a default directory structure.*/
|
||||
val InitializeProjectKey = "project.initialize"
|
||||
/** The properties key that configures the project to be flattened a bit for use by quick throwaway projects.*/
|
||||
val ScratchKey = "project.scratch"
|
||||
|
||||
/** The label used when prompting for the name of the user's project.*/
|
||||
val NameLabel = "Name"
|
||||
/** The label used when prompting for the organization of the user's project.*/
|
||||
val OrganizationLabel = "Organization"
|
||||
/** The label used when prompting for the version of the user's project.*/
|
||||
val VersionLabel = "Version"
|
||||
/** The label used when prompting for the version of Scala to use for the user's project.*/
|
||||
val ScalaVersionLabel = "Scala version"
|
||||
/** The label used when prompting for the version of sbt to use for the user's project.*/
|
||||
val SbtVersionLabel = "sbt version"
|
||||
|
||||
/** The default organization of the new user project when the user doesn't explicitly specify one when prompted.*/
|
||||
val DefaultOrganization = ""
|
||||
/** The default version of the new user project when the user doesn't explicitly specify a version when prompted.*/
|
||||
val DefaultVersion = "1.0"
|
||||
/** The default version of sbt when the user doesn't explicitly specify a version when prompted.*/
|
||||
val DefaultSbtVersion = "0.5"
|
||||
/** The default version of Scala when the user doesn't explicitly specify a version when prompted.*/
|
||||
val DefaultScalaVersion = "2.7.5"
|
||||
|
||||
// sets up the project properties for a throwaway project (flattens src and lib to the root project directory)
|
||||
def scratch(file: File)
|
||||
{
|
||||
withProperties(file) { properties =>
|
||||
for( (key, _, default, _) <- propertyDefinitions(false))
|
||||
properties(key) = default.getOrElse("scratch")
|
||||
properties(ScratchKey) = true.toString
|
||||
}
|
||||
}
|
||||
// returns (scala version, sbt version)
|
||||
def apply(file: File, setInitializeProject: Boolean): (String, String) = applyImpl(file, setInitializeProject, Nil)
|
||||
def forcePrompt(file: File, propertyKeys: String*) = applyImpl(file, false, propertyKeys)
|
||||
private def applyImpl(file: File, setInitializeProject: Boolean, propertyKeys: Iterable[String]): (String, String) =
|
||||
{
|
||||
val organizationOptional = file.exists
|
||||
withProperties(file) { properties =>
|
||||
properties -= propertyKeys
|
||||
|
||||
prompt(properties, organizationOptional)
|
||||
if(setInitializeProject)
|
||||
properties(InitializeProjectKey) = true.toString
|
||||
}
|
||||
}
|
||||
// (key, label, defaultValue, promptRequired)
|
||||
private def propertyDefinitions(organizationOptional: Boolean) =
|
||||
(NameKey, NameLabel, None, true) ::
|
||||
(OrganizationKey, OrganizationLabel, Some(DefaultOrganization), !organizationOptional) ::
|
||||
(VersionKey, VersionLabel, Some(DefaultVersion), true) ::
|
||||
(ScalaVersionKey, ScalaVersionLabel, Some(DefaultScalaVersion), true) ::
|
||||
(SbtVersionKey, SbtVersionLabel, Some(DefaultSbtVersion), true) ::
|
||||
Nil
|
||||
private def prompt(fill: ProjectProperties, organizationOptional: Boolean)
|
||||
{
|
||||
for( (key, label, default, promptRequired) <- propertyDefinitions(organizationOptional))
|
||||
{
|
||||
val value = fill(key)
|
||||
if(value == null && promptRequired)
|
||||
fill(key) = readLine(label, default)
|
||||
}
|
||||
}
|
||||
private def withProperties(file: File)(f: ProjectProperties => Unit) =
|
||||
{
|
||||
val properties = new ProjectProperties(file)
|
||||
f(properties)
|
||||
properties.save
|
||||
(properties(ScalaVersionKey), properties(SbtVersionKey))
|
||||
}
|
||||
private def readLine(label: String, default: Option[String]): String =
|
||||
{
|
||||
val prompt =
|
||||
default match
|
||||
{
|
||||
case Some(d) => "%s [%s]: ".format(label, d)
|
||||
case None => "%s: ".format(label)
|
||||
}
|
||||
SimpleReader.readLine(prompt) orElse default match
|
||||
{
|
||||
case Some(line) => line
|
||||
case None => throw new BootException("Project not loaded: " + label + " not specified.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import java.io.{FileInputStream, FileOutputStream}
|
||||
import java.util.Properties
|
||||
private class ProjectProperties(file: File) extends NotNull
|
||||
{
|
||||
private[this] var modified = false
|
||||
private[this] val properties = new Properties
|
||||
if(file.exists)
|
||||
{
|
||||
val in = new FileInputStream(file)
|
||||
try { properties.load(in) } finally { in.close() }
|
||||
}
|
||||
|
||||
def update(key: String, value: String)
|
||||
{
|
||||
modified = true
|
||||
properties.setProperty(key, value)
|
||||
}
|
||||
def apply(key: String) = properties.getProperty(key)
|
||||
def save()
|
||||
{
|
||||
if(modified)
|
||||
{
|
||||
file.getParentFile.mkdirs()
|
||||
val out = new FileOutputStream(file)
|
||||
try { properties.store(out, "Project Properties") } finally { out.close() }
|
||||
modified = false
|
||||
}
|
||||
}
|
||||
def -= (keys: Iterable[String]) { for(key <- keys) properties.remove(key) }
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt.boot
|
||||
|
||||
import jline.ConsoleReader
|
||||
object SimpleReader extends NotNull
|
||||
{
|
||||
protected[this] val reader =
|
||||
{
|
||||
val cr = new ConsoleReader
|
||||
cr.setBellEnabled(false)
|
||||
cr
|
||||
}
|
||||
def readLine(prompt: String) =
|
||||
reader.readLine(prompt) match
|
||||
{
|
||||
case null => None
|
||||
case x =>
|
||||
val trimmed = x.trim
|
||||
if(trimmed.isEmpty)
|
||||
None
|
||||
else
|
||||
Some(trimmed)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt.boot
|
||||
|
||||
import java.io.{File, FileWriter, PrintWriter, Writer}
|
||||
|
||||
import org.apache.ivy.{core, plugins, util}
|
||||
import core.LogOptions
|
||||
import core.cache.DefaultRepositoryCacheManager
|
||||
import core.event.EventManager
|
||||
import core.module.id.ModuleRevisionId
|
||||
import core.module.descriptor.{Configuration, DefaultDependencyDescriptor, DefaultModuleDescriptor, ModuleDescriptor}
|
||||
import core.report.ResolveReport
|
||||
import core.resolve.{ResolveEngine, ResolveOptions}
|
||||
import core.retrieve.{RetrieveEngine, RetrieveOptions}
|
||||
import core.sort.SortEngine
|
||||
import core.settings.IvySettings
|
||||
import plugins.resolver.{ChainResolver, FileSystemResolver, IBiblioResolver, URLResolver}
|
||||
import util.{DefaultMessageLogger, Message}
|
||||
|
||||
import BootConfiguration._
|
||||
|
||||
private[boot] object UpdateTarget extends Enumeration
|
||||
{
|
||||
val UpdateScala, UpdateSbt = Value
|
||||
}
|
||||
import UpdateTarget.{UpdateSbt, UpdateScala}
|
||||
|
||||
object Update
|
||||
{
|
||||
/** Use Ivy to resolve and retrieve the specified 'targets' for the given versions.*/
|
||||
def apply(bootDirectory: File, sbtVersion: String, scalaVersion: String, targets: UpdateTarget.Value*) =
|
||||
synchronized // synchronized because Ivy is not thread-safe
|
||||
{
|
||||
val up = new Update(bootDirectory, sbtVersion, scalaVersion, targets : _*)
|
||||
up.update()
|
||||
}
|
||||
}
|
||||
/** Ensures that the Scala and sbt jars exist for the given versions or else downloads them.*/
|
||||
private final class Update(bootDirectory: File, sbtVersion: String, scalaVersion: String, targets: UpdateTarget.Value*)
|
||||
{
|
||||
private def logFile = new File(bootDirectory, UpdateLogName)
|
||||
/** A Writer to use to write the full logging information to a file for debugging. **/
|
||||
lazy val logWriter =
|
||||
{
|
||||
bootDirectory.mkdirs
|
||||
new PrintWriter(new FileWriter(logFile))
|
||||
}
|
||||
|
||||
/** The main entry point of this class for use by the Update module. It runs Ivy */
|
||||
private def update()
|
||||
{
|
||||
Message.setDefaultLogger(new SbtIvyLogger(logWriter))
|
||||
try { targets.foreach(update) } // runs update on each module separately
|
||||
catch
|
||||
{
|
||||
case e: Exception =>
|
||||
e.printStackTrace(logWriter)
|
||||
log(e.toString)
|
||||
println(" (see " + logFile + " for complete log)")
|
||||
}
|
||||
finally { logWriter.close() }
|
||||
}
|
||||
/** Runs update for the specified target (updates either the scala or sbt jars for building the project) */
|
||||
private def update(target: UpdateTarget.Value)
|
||||
{
|
||||
import Configuration.Visibility.PUBLIC
|
||||
// the actual module id here is not that important
|
||||
val moduleID = new DefaultModuleDescriptor(createID(SbtOrg, "boot", "1.0"), "release", null, false)
|
||||
moduleID.setLastModified(System.currentTimeMillis)
|
||||
moduleID.addConfiguration(new Configuration(DefaultIvyConfiguration, PUBLIC, "", Array(), true, null))
|
||||
// add dependencies based on which target needs updating
|
||||
target match
|
||||
{
|
||||
case UpdateScala =>
|
||||
addDependency(moduleID, ScalaOrg, CompilerModuleName, scalaVersion, "default")
|
||||
addDependency(moduleID, ScalaOrg, LibraryModuleName, scalaVersion, "default")
|
||||
update(moduleID, target, false)
|
||||
case UpdateSbt =>
|
||||
addDependency(moduleID, SbtOrg, SbtModuleName, sbtVersion, scalaVersion)
|
||||
try { update(moduleID, target, false) }
|
||||
catch
|
||||
{
|
||||
// unfortunately, there is not a more specific exception thrown when a configuration does not exist,
|
||||
// so we always retry after cleaning the ivy file for this version of sbt on in case it is a newer version
|
||||
// of Scala than when this version of sbt was initially published
|
||||
case e: RuntimeException =>
|
||||
update(moduleID, target, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Runs the resolve and retrieve for the given moduleID, which has had its dependencies added already. */
|
||||
private def update(moduleID: DefaultModuleDescriptor, target: UpdateTarget.Value, cleanExisting: Boolean)
|
||||
{
|
||||
val eventManager = new EventManager
|
||||
val settings = new IvySettings
|
||||
addResolvers(settings, scalaVersion, target)
|
||||
settings.setDefaultConflictManager(settings.getConflictManager(ConflictManagerName))
|
||||
settings.setBaseDir(bootDirectory)
|
||||
if(cleanExisting)
|
||||
{
|
||||
val sbtID = createID(SbtOrg, SbtModuleName, sbtVersion)
|
||||
onDefaultRepositoryCacheManager(settings) { cache =>
|
||||
val ivyFile = cache.getIvyFileInCache(sbtID)
|
||||
ivyFile.delete()
|
||||
val original = new File(ivyFile.getParentFile, ivyFile.getName + ".original")
|
||||
original.delete()
|
||||
}
|
||||
}
|
||||
resolve(settings, eventManager, moduleID)
|
||||
retrieve(settings, eventManager, moduleID, target)
|
||||
}
|
||||
private def createID(organization: String, name: String, revision: String) =
|
||||
ModuleRevisionId.newInstance(organization, name, revision)
|
||||
/** Adds the given dependency to the default configuration of 'moduleID'. */
|
||||
private def addDependency(moduleID: DefaultModuleDescriptor, organization: String, name: String, revision: String, conf: String)
|
||||
{
|
||||
val dep = new DefaultDependencyDescriptor(moduleID, createID(organization, name, revision), false, false, true)
|
||||
dep.addDependencyConfiguration(DefaultIvyConfiguration, conf)
|
||||
moduleID.addDependency(dep)
|
||||
}
|
||||
private def resolve(settings: IvySettings, eventManager: EventManager, module: ModuleDescriptor)
|
||||
{
|
||||
val resolveOptions = new ResolveOptions
|
||||
// this reduces the substantial logging done by Ivy, including the progress dots when downloading artifacts
|
||||
resolveOptions.setLog(LogOptions.LOG_DOWNLOAD_ONLY)
|
||||
val resolveEngine = new ResolveEngine(settings, eventManager, new SortEngine(settings))
|
||||
val resolveReport = resolveEngine.resolve(module, resolveOptions)
|
||||
if(resolveReport.hasError)
|
||||
{
|
||||
logExceptions(resolveReport)
|
||||
println(Set(resolveReport.getAllProblemMessages.toArray: _*).mkString(System.getProperty("line.separator")))
|
||||
throw new BootException("Error retrieving required libraries")
|
||||
}
|
||||
}
|
||||
/** Exceptions are logged to the update log file. */
|
||||
private def logExceptions(report: ResolveReport)
|
||||
{
|
||||
for(unresolved <- report.getUnresolvedDependencies)
|
||||
{
|
||||
val problem = unresolved.getProblem
|
||||
if(problem != null)
|
||||
problem.printStackTrace(logWriter)
|
||||
}
|
||||
}
|
||||
/** Retrieves resolved dependencies using the given target to determine the location to retrieve to. */
|
||||
private def retrieve(settings: IvySettings, eventManager: EventManager, module: ModuleDescriptor, target: UpdateTarget.Value)
|
||||
{
|
||||
val retrieveOptions = new RetrieveOptions
|
||||
val retrieveEngine = new RetrieveEngine(settings, eventManager)
|
||||
val pattern =
|
||||
target match
|
||||
{
|
||||
// see BuildConfiguration
|
||||
case UpdateSbt => sbtRetrievePattern(sbtVersion)
|
||||
case UpdateScala => scalaRetrievePattern
|
||||
}
|
||||
retrieveEngine.retrieve(module.getModuleRevisionId, pattern, retrieveOptions);
|
||||
}
|
||||
/** Add the scala tools repositories and a URL resolver to download sbt from the Google code project.*/
|
||||
private def addResolvers(settings: IvySettings, scalaVersion: String, target: UpdateTarget.Value)
|
||||
{
|
||||
val newDefault = new ChainResolver
|
||||
newDefault.setName("redefined-public")
|
||||
newDefault.add(localResolver(settings.getDefaultIvyUserDir.getAbsolutePath))
|
||||
newDefault.add(mavenLocal)
|
||||
target match
|
||||
{
|
||||
case UpdateSbt =>
|
||||
newDefault.add(sbtResolver(scalaVersion))
|
||||
newDefault.add(mavenMainResolver)
|
||||
case UpdateScala =>
|
||||
newDefault.add(mavenResolver("Scala-Tools Maven2 Repository", "http://scala-tools.org/repo-releases"))
|
||||
newDefault.add(mavenResolver("Scala-Tools Maven2 Snapshots Repository", "http://scala-tools.org/repo-snapshots"))
|
||||
}
|
||||
onDefaultRepositoryCacheManager(settings)(_.setUseOrigin(true))
|
||||
settings.addResolver(newDefault)
|
||||
settings.setDefaultResolver(newDefault.getName)
|
||||
}
|
||||
private def onDefaultRepositoryCacheManager(settings: IvySettings)(f: DefaultRepositoryCacheManager => Unit)
|
||||
{
|
||||
settings.getDefaultRepositoryCacheManager match
|
||||
{
|
||||
case manager: DefaultRepositoryCacheManager => f(manager)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
/** Uses the pattern defined in BuildConfiguration to download sbt from Google code.*/
|
||||
private def sbtResolver(scalaVersion: String) =
|
||||
{
|
||||
val pattern = sbtResolverPattern(scalaVersion)
|
||||
val resolver = new URLResolver
|
||||
resolver.setName("Sbt Repository")
|
||||
resolver.addIvyPattern(pattern)
|
||||
resolver.addArtifactPattern(pattern)
|
||||
resolver
|
||||
}
|
||||
private def mavenLocal = mavenResolver("Maven2 Local", "file://" + System.getProperty("user.home") + "/.m2/repository/")
|
||||
/** Creates a maven-style resolver.*/
|
||||
private def mavenResolver(name: String, root: String) =
|
||||
{
|
||||
val resolver = defaultMavenResolver(name)
|
||||
resolver.setRoot(root)
|
||||
resolver
|
||||
}
|
||||
/** Creates a resolver for Maven Central.*/
|
||||
private def mavenMainResolver = defaultMavenResolver("Maven Central")
|
||||
/** Creates a maven-style resolver with the default root.*/
|
||||
private def defaultMavenResolver(name: String) =
|
||||
{
|
||||
val resolver = new IBiblioResolver
|
||||
resolver.setName(name)
|
||||
resolver.setM2compatible(true)
|
||||
resolver
|
||||
}
|
||||
private def localResolver(ivyUserDirectory: String) =
|
||||
{
|
||||
val localIvyRoot = ivyUserDirectory + "/local"
|
||||
val artifactPattern = localIvyRoot + "/" + LocalArtifactPattern
|
||||
val ivyPattern = localIvyRoot + "/" + LocalIvyPattern
|
||||
val resolver = new FileSystemResolver
|
||||
resolver.setName(LocalIvyName)
|
||||
resolver.addIvyPattern(ivyPattern)
|
||||
resolver.addArtifactPattern(artifactPattern)
|
||||
resolver
|
||||
}
|
||||
/** Logs the given message to a file and to the console. */
|
||||
private def log(msg: String) =
|
||||
{
|
||||
try { logWriter.println(msg) }
|
||||
catch { case e: Exception => System.err.println("Error writing to update log file: " + e.toString) }
|
||||
println(msg)
|
||||
}
|
||||
}
|
||||
/** A custom logger for Ivy to ignore the messages about not finding classes
|
||||
* intentionally filtered using proguard. */
|
||||
private final class SbtIvyLogger(logWriter: PrintWriter) extends DefaultMessageLogger(Message.MSG_INFO) with NotNull
|
||||
{
|
||||
private val ignorePrefix = "impossible to define"
|
||||
override def log(msg: String, level: Int)
|
||||
{
|
||||
logWriter.println(msg)
|
||||
if(level <= getLevel && msg != null && !msg.startsWith(ignorePrefix))
|
||||
System.out.println(msg)
|
||||
}
|
||||
override def rawlog(msg: String, level: Int) { log(msg, level) }
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt.extract
|
||||
|
||||
import java.io.{File, InputStream}
|
||||
import java.util.zip.{ZipEntry, ZipFile}
|
||||
|
||||
object Main
|
||||
{
|
||||
lazy val log: Logger = new ConsoleLogger
|
||||
|
||||
def main(args: Array[String])
|
||||
{
|
||||
if(args.contains("debug"))
|
||||
log.setLevel(Level.Debug)
|
||||
val result = OpenResource.zipFile.ioOption(FileUtilities.classLocationFile[Install], "processing", log)(process)
|
||||
for(msg <- result)
|
||||
{
|
||||
log.error(msg)
|
||||
System.exit(1)
|
||||
}
|
||||
}
|
||||
private[this] val packedGzip = ".pack.gz"
|
||||
private def isArchive(name: String) = name.endsWith(".gz") || name.endsWith(".zip")
|
||||
private def process(zip: ZipFile) =
|
||||
{
|
||||
val installEntry = zip.getEntry("install")
|
||||
if(installEntry == null)
|
||||
Some("Install commands not found.")
|
||||
else
|
||||
{
|
||||
val jarAndZip = wrap.Wrappers.toList(zip.entries).filter(entry => isArchive(entry.getName)).partition(_.getName.endsWith(packedGzip))
|
||||
jarAndZip match
|
||||
{
|
||||
case (Nil, _)=> Some("sbt loader not found.")
|
||||
case (_, Nil) => Some("Project to extract and build not found.")
|
||||
case (loaderEntry :: _, projectEntry :: _) => extractAndRun(zip, loaderEntry, projectEntry, installEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
private def extractAndRun(zip: ZipFile, loaderEntry: ZipEntry, projectEntry: ZipEntry, installEntry: ZipEntry) =
|
||||
{
|
||||
val zipResource = OpenResource.zipEntry(zip)
|
||||
|
||||
import FileUtilities.{gunzip, readString, transfer, unzip, writeStream}
|
||||
val directory = new File(".", trimExtension(projectEntry.getName, ".zip"))
|
||||
assume(!directory.exists, "Could not extract project: directory " + projectEntry.getName + " exists.")
|
||||
|
||||
val loaderBaseName = trimExtension(loaderEntry.getName, packedGzip)
|
||||
val loaderFile = new File(directory, loaderBaseName + ".jar")
|
||||
val tempLoaderFile = new File(directory, loaderBaseName + ".pack")
|
||||
|
||||
def extractLoader() =
|
||||
{
|
||||
implicit def fileToPath(f: File) = Path.fromFile(f)
|
||||
val result =
|
||||
writeStream(tempLoaderFile, log) { out => zipResource.ioOption(loaderEntry, "reading", log)(gunzip(_, out, log)) } orElse
|
||||
Pack.unpack(tempLoaderFile, loaderFile, log)
|
||||
FileUtilities.clean(tempLoaderFile :: Nil, true, log)
|
||||
result.toLeft(loaderFile)
|
||||
}
|
||||
|
||||
Control.thread(zipResource.io(installEntry, "reading", log)(readString(_, log))) { installString =>
|
||||
Control.thread(parseInstall(installString)) { install =>
|
||||
zipResource.io(projectEntry, "reading", log)(unzip(_, Path.fromFile(directory), log)).left.toOption orElse
|
||||
Control.thread(extractLoader()) { loaderFile =>
|
||||
run(loaderFile, directory, install)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private def parseInstall(installString: String): Either[String, Install] =
|
||||
{
|
||||
installString.split(separator) match
|
||||
{
|
||||
case Array(allOptions, allActions) =>
|
||||
val options = allOptions.split("""\n""").toList
|
||||
val actions = allActions.split("""\n""").toList
|
||||
Right( Install(options, actions) )
|
||||
case _ => Left("Invalid install script (no separator found)")
|
||||
}
|
||||
}
|
||||
private def filterEmpty(list: List[String]) = list.filter(!_.isEmpty)
|
||||
private def run(loader: File, project: File, install: Install) =
|
||||
{
|
||||
val command = "java" :: "-cp" :: loader.getAbsolutePath :: filterEmpty(install.options) ::: "sbt.boot.Boot" :: filterEmpty(install.actions)
|
||||
val builder = new java.lang.ProcessBuilder(command.toArray : _*)
|
||||
builder.directory(project)
|
||||
//import BasicIO.{processFully, transferFully}
|
||||
//val standardIO = new ProcessIO(transferFully(System.in, _, 0), processFully(System.out.println), processFully(System.err.println))
|
||||
val exitCode = ( Process(builder) ! )//( Process(builder) run standardIO).exitValue()
|
||||
if(exitCode == 0)
|
||||
None
|
||||
else
|
||||
Some("sbt exited with nonzero exit code: " + exitCode)
|
||||
}
|
||||
private def trimExtension(name: String, ext: String) =
|
||||
{
|
||||
if(name.endsWith(ext))
|
||||
name.substring(0, name.length - ext.length)
|
||||
else
|
||||
name
|
||||
}
|
||||
// keep this in sync with sbt.extract.SelfExtractingProject
|
||||
private def separator = "===================="
|
||||
}
|
||||
private final case class Install(options: List[String], actions: List[String]) extends NotNull
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt.extract
|
||||
|
||||
import java.io.{ByteArrayOutputStream, File}
|
||||
import FileUtilities.{classLocationFile, clean, createTemporaryDirectory, download, transferAndClose, unzip, write, zip}
|
||||
import SelfExtractingProject.{flat, separator}
|
||||
|
||||
trait SelfExtractingProject extends Project
|
||||
{
|
||||
protected def createSelfExtractingJar(actions: List[String], jvmOptions: List[String], projectZip: Path, outputJar: Path): Option[String] =
|
||||
{
|
||||
def jarForClass(name: String) = Path.fromFile(classLocationFile(Class.forName(name)))
|
||||
val loaderJar = jarForClass("sbt.boot.Boot")
|
||||
val bytes = new ByteArrayOutputStream
|
||||
transferAndClose(this.getClass.getResourceAsStream("extract.location"), bytes, log) orElse
|
||||
{
|
||||
val extractorJarLocation = bytes.toString("UTF-8")
|
||||
createSelfExtractingJar(actions, jvmOptions, projectZip, loaderJar, extractorJarLocation, outputJar)
|
||||
}
|
||||
}
|
||||
private def createSelfExtractingJar(actions: List[String], jvmOptions: List[String], projectZip: Path, loaderJar: Path, extractorJarLocation: String, outputJar: Path): Option[String] =
|
||||
{
|
||||
val installContents = jvmOptions.mkString("\n") + separator + actions.mkString("\n")
|
||||
withTemporaryDirectory(log) { tmp =>
|
||||
val tmpPath = Path.fromFile(tmp)
|
||||
write(new File(tmp, "install"), installContents, log) orElse
|
||||
unzip(this.getClass.getResource(extractorJarLocation), tmpPath, log).left.toOption orElse
|
||||
Control.thread(compressLoader(loaderJar)) { compressedLoader =>
|
||||
zip( (tmpPath ##) :: flat(projectZip) :: compressedLoader :: Nil, outputJar, true, log)
|
||||
}
|
||||
}
|
||||
}
|
||||
private def withTemporaryDirectory(log: Logger)(f: File => Option[String]) =
|
||||
{
|
||||
Control.thread(createTemporaryDirectory(log)) { dir =>
|
||||
Control.trapUnitAndFinally("", log)
|
||||
{ f(dir) }
|
||||
{ clean(Path.fromFile(dir) :: Nil, true, log) }
|
||||
}
|
||||
}
|
||||
private def compressLoader(loaderJar: Path): Either[String, Path] =
|
||||
{
|
||||
val jarName = loaderJar.asFile.getName
|
||||
val dotIndex = jarName.lastIndexOf('.')
|
||||
val baseName =
|
||||
if(dotIndex > 0) jarName.substring(0, dotIndex)
|
||||
else jarName
|
||||
val packedName = baseName + ".pack"
|
||||
val packed = outputPath / packedName
|
||||
val packedAndGzip = (outputPath ##) / (packedName + ".gz")
|
||||
val result =
|
||||
Pack.pack(loaderJar, packed, log) orElse
|
||||
FileUtilities.gzip(packed, packedAndGzip, log)
|
||||
result.toLeft(packedAndGzip)
|
||||
}
|
||||
}
|
||||
trait BasicSelfExtractingProject extends BasicScalaProject with SelfExtractingProject
|
||||
{
|
||||
def installActions: List[String] = update.name :: `package`.name :: Nil
|
||||
def jvmOptions: List[String] = Nil
|
||||
def selfExtractingJar: Path = outputPath / (artifactBaseName + "-setup.jar")
|
||||
|
||||
lazy val installer = installerAction
|
||||
def installerAction = task { createSelfExtractingJar(installActions, jvmOptions, packageProjectZip, selfExtractingJar) } dependsOn packageProject
|
||||
}
|
||||
|
||||
object SelfExtractingProject
|
||||
{
|
||||
// keep this in sync with sbt.extract.Main.separator
|
||||
def separator = "===================="
|
||||
private def flat(p: Path) =
|
||||
p match
|
||||
{
|
||||
case rp: RelativePath => (rp.parentPath ##) / rp.component
|
||||
case _ => p
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#Project properties
|
||||
#Thu Jun 25 14:31:30 EDT 2009
|
||||
project.organization=org.scala-tools.sbt
|
||||
project.name=extract
|
||||
sbt.version=0.4.7-p13
|
||||
project.version=0.1
|
||||
scala.version=2.7.5
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
import sbt._
|
||||
|
||||
import java.io.File
|
||||
import java.nio.charset.Charset
|
||||
|
||||
class InstallerProject(info: ProjectInfo) extends ParentProject(info)
|
||||
{
|
||||
lazy val installExtractor: InstallExtractProject = project("extract", "Installer Extractor", new InstallExtractProject(_, installPlugin))
|
||||
lazy val installPlugin: InstallPluginProject = project("plugin", "Installer Plugin", new InstallPluginProject(_, installExtractor), installExtractor)
|
||||
}
|
||||
protected class InstallPluginProject(info: ProjectInfo, extract: => InstallExtractProject) extends PluginProject(info)
|
||||
{
|
||||
private lazy val extractProject = extract
|
||||
override def crossScalaVersions = Set("2.7.2", "2.7.3", "2.7.4", "2.7.5")
|
||||
override def mainResources = super.mainResources +++ extractProject.outputJar +++ extractLocation
|
||||
|
||||
def extractLocation = (outputPath ##) / "extract.location"
|
||||
lazy val writeProperties = task { FileUtilities.write(extractLocation.asFile, extractProject.outputJar.relativePath, Charset.forName("UTF-8"), log) }
|
||||
override def packageAction = super.packageAction dependsOn(extractProject.proguard, writeProperties)
|
||||
|
||||
val publishTo = "Scala Tools Nexus" at "http://nexus.scala-tools.org/content/repositories/releases/"
|
||||
Credentials(Path.fromFile(System.getProperty("user.home")) / ".ivy2" / ".credentials", log)
|
||||
}
|
||||
protected class InstallExtractProject(info: ProjectInfo, pluginProject: => InstallPluginProject) extends DefaultProject(info)
|
||||
{
|
||||
override def publishLocalAction = publishAction
|
||||
override def publishAction = task {None}
|
||||
override def unmanagedClasspath = super.unmanagedClasspath +++ Path.lazyPathFinder(Path.fromFile(FileUtilities.sbtJar) :: Nil)
|
||||
private lazy val plugin = pluginProject
|
||||
val mainClassName = "sbt.extract.Main"
|
||||
val proguardConfigurationPath: Path = outputPath / "proguard.pro"
|
||||
val toolsConfig = config("tools")
|
||||
val defaultConfig = Configurations.Default
|
||||
val proguardJar = "net.sf.proguard" % "proguard" % "4.3" % "tools->default"
|
||||
def rootProjectDirectory = rootProject.info.projectPath
|
||||
def outputJar = (plugin.outputPath ##) / defaultJarName
|
||||
|
||||
/******** Proguard *******/
|
||||
lazy val proguard = proguardTask dependsOn(`package`, writeProguardConfiguration)
|
||||
lazy val writeProguardConfiguration = writeProguardConfigurationTask dependsOn `package`
|
||||
|
||||
private def proguardTask =
|
||||
task
|
||||
{
|
||||
FileUtilities.clean(outputJar :: Nil, log)
|
||||
val proguardClasspathString = Path.makeString(managedClasspath(toolsConfig).get)
|
||||
val configFile = proguardConfigurationPath.asFile.getAbsolutePath
|
||||
val exitValue = Process("java", List("-Xmx256M", "-cp", proguardClasspathString, "proguard.ProGuard", "@" + configFile)) ! log
|
||||
if(exitValue == 0) None else Some("Proguard failed with nonzero exit code (" + exitValue + ")")
|
||||
}
|
||||
private def writeProguardConfigurationTask =
|
||||
task
|
||||
{
|
||||
// the template for the proguard configuration file
|
||||
val outTemplate = """
|
||||
|-dontoptimize
|
||||
|-dontobfuscate
|
||||
|-dontnote
|
||||
|-dontwarn
|
||||
|-libraryjars %s
|
||||
|%s
|
||||
|-outjars %s
|
||||
|-ignorewarnings
|
||||
|-keep public class %s {
|
||||
| public static void main(java.lang.String[]);
|
||||
|}"""
|
||||
|
||||
val defaultJar = jarPath.absolutePath
|
||||
log.debug("proguard configuration using main jar " + defaultJar)
|
||||
val externalDependencies = (mainCompileConditional.analysis.allExternals).map(_.getAbsoluteFile).filter(_.getName.endsWith(".jar"))
|
||||
debugJars("external dependencies", externalDependencies)
|
||||
// partition jars from the external jar dependencies of this project by whether they are located in the project directory
|
||||
// if they are, they are specified with -injars, otherwise they are specified with -libraryjars
|
||||
val (externalJars, libraryJars) = externalDependencies.toList.partition{jar => Path.relativize(rootProjectDirectory, jar).isDefined}
|
||||
debugJars("library jars", libraryJars)
|
||||
val sbtJarString = FileUtilities.sbtJar.getAbsolutePath + "(!META-INF/**,!licenses/**,LICENSE,NOTICE,!*.xml)"
|
||||
val externalJarStrings = externalJars.map( _ + "(!META-INF/**,!*.properties)")
|
||||
// exclude properties files and manifests from scala-library jar
|
||||
val inJars = (defaultJar :: externalJarStrings).map("-injars " + _).mkString("\n")
|
||||
|
||||
val proguardConfiguration = outTemplate.stripMargin.format(libraryJars.mkString(File.pathSeparator), inJars, outputJar.absolutePath, mainClassName)
|
||||
log.debug("Proguard configuration written to " + proguardConfigurationPath)
|
||||
FileUtilities.write(proguardConfigurationPath.asFile, proguardConfiguration, log)
|
||||
}
|
||||
private def debugJars[T](label: String, jars: Iterable[T]): Unit =
|
||||
log.debug("proguard configuration " + label + ": \n\t" + jars.mkString("\n\t"))
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
SCALA LICENSE
|
||||
|
||||
Copyright (c) 2002-2008 EPFL, Lausanne, unless otherwise specified.
|
||||
All rights reserved.
|
||||
|
||||
This software was developed by the Programming Methods Laboratory of the
|
||||
Swiss Federal Institute of Technology (EPFL), Lausanne, Switzerland.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software in source
|
||||
or binary form for any purpose with or without fee is hereby granted,
|
||||
provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the EPFL nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this
|
||||
software without specific prior written permission.
|
||||
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGE.
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
ScalaCheck LICENSE
|
||||
|
||||
Copyright (c) 2007, Rickard Nilsson
|
||||
All rights reserved.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software in source
|
||||
or binary form for any purpose with or without fee is hereby granted,
|
||||
provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the author nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this
|
||||
software without specific prior written permission.
|
||||
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGE.
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
Copyright (c) 2008 Mark Harrah, David MacIver
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. The name of the author may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
Copyright (c) 2007-2008 Eric Torreborre
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
Neither the name of specs nor the names of its contributors may be
|
||||
used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#Project properties
|
||||
#Thu Jun 25 20:59:30 EDT 2009
|
||||
project.organization=sbt
|
||||
project.name=Simple Build Tool Parent
|
||||
sbt.version=0.4.6
|
||||
project.version=0.5
|
||||
scala.version=2.7.2
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
import sbt._
|
||||
|
||||
import java.io.File
|
||||
import scala.xml.NodeSeq
|
||||
|
||||
/** Support for compiling sbt across multiple versions of Scala. The scala compiler is run in a
|
||||
* separate JVM and no partial compilation is done.*/
|
||||
abstract class CrossCompileProject extends BasicScalaProject
|
||||
{
|
||||
/** Used for 2.8.0-SNAPSHOT*/
|
||||
val scalaToolsSnapshots = "Scala Tools Snapshots" at "http://scala-tools.org/repo-snapshots"
|
||||
|
||||
/* The base configuration names for the versions of Scala*/
|
||||
private val version2_7_2 = "2.7.2"
|
||||
private val version2_7_3 = "2.7.3"
|
||||
private val version2_7_4 = "2.7.4"
|
||||
private val version2_7_5 = "2.7.5"
|
||||
private val version2_8_0 = "2.8.0.r18093-b20090623200909"
|
||||
private val base = "base"
|
||||
|
||||
/* The configurations for the versions of Scala.*/
|
||||
private val conf_2_7_2 = config(version2_7_2)
|
||||
private val conf_2_7_3 = config(version2_7_3)
|
||||
private val conf_2_7_4 = config(version2_7_4)
|
||||
private val conf_2_7_5 = config(version2_7_5)
|
||||
private val conf_2_8_0 = config(version2_8_0)
|
||||
private val conf_base = config(base)
|
||||
// the list of all configurations cross-compile supports
|
||||
private val allConfigurations = conf_2_7_2 :: conf_2_7_3 :: conf_2_7_4 :: conf_2_7_5 :: conf_2_8_0 :: Nil
|
||||
// the list of configurations to actually build against
|
||||
private val buildConfigurations = conf_2_7_2 :: conf_2_7_3 :: conf_2_7_4 :: conf_2_7_5 :: Nil//allConfigurations not currently used because of issues with 2.8.0
|
||||
// the configuration to use for normal development (when cross-building is not done)
|
||||
private def developmentVersion = buildConfigurations.first
|
||||
|
||||
/* Methods to derive the configuration name from the base name 'v'.*/
|
||||
private def optional(v: Configuration) = config("optional-" + v.toString)
|
||||
private def scalac(v: Configuration) = config("scalac-" + v.toString)
|
||||
private def sbt(v: Configuration) = config("sbt_" + v.toString)
|
||||
private def depConf(v: Configuration) = v.toString + "->default"
|
||||
|
||||
// =========== Cross-compilation across scala versions ===========
|
||||
|
||||
// The dependencies that should go in each configuration are:
|
||||
// base Required dependencies that are the same across all scala versions.
|
||||
// <version> Required dependencies to use with Scala <version>
|
||||
// optional-base Optional dependencies that are the same for all scala versions
|
||||
// optional-<version> Optional dependencies to use with Scala <version>
|
||||
// compile Used for normal development, it should extend a specific <version> and optional-<version>
|
||||
// scalac-<version> The scala compiler for Scala <version>
|
||||
// There should be a jar publication for each version of scala. The artifact should be named sbt_<version>.
|
||||
override def ivyXML =
|
||||
(<configurations>
|
||||
<conf name={conf_base.toString}/>
|
||||
<conf name={optional(conf_base).toString}/>
|
||||
{ variableConfigurations }
|
||||
<!-- The configuration used for normal development (actions other than cross-*) -->
|
||||
<conf name="default" extends={developmentVersion + "," + optional(developmentVersion).toString} visibility="private"/>
|
||||
</configurations>
|
||||
<publications>
|
||||
{ publications }
|
||||
</publications>
|
||||
<dependencies>
|
||||
<!-- Dependencies that are the same across all Scala versions -->
|
||||
<dependency org="org.apache.ivy" name="ivy" rev="2.0.0" transitive="false" conf={depConf(conf_base)}/>
|
||||
<dependency org="com.jcraft" name="jsch" rev="0.1.31" transitive="false" conf={depConf(conf_base)}/>
|
||||
<dependency org="org.mortbay.jetty" name="jetty" rev="6.1.14" transitive="true" conf={depConf(optional(conf_base))}/>
|
||||
|
||||
<!-- the dependencies that are different dependeding on the version of Scala -->
|
||||
{ variableDependencies(conf_2_7_2, /*ScalaTest*/"0.9.3", /*Specs*/"1.4.0", false) }
|
||||
{ variableDependencies(conf_2_7_3, /*ScalaTest*/"0.9.4", /*Specs*/"1.4.3", true) }
|
||||
{ variableDependencies(conf_2_7_4, /*ScalaTest*/"0.9.5", /*Specs*/"1.4.3", true) }
|
||||
{ variableDependencies(conf_2_7_5, /*ScalaTest*/"0.9.5", /*Specs*/"1.4.3", true) }
|
||||
{ variableDependencies(conf_2_8_0, /*ScalaTest*/"0.9.5", /*Specs*/"1.4.3", true) }
|
||||
</dependencies>)
|
||||
|
||||
/** Creates a publication (an 'artifact' element) for each Scala version */
|
||||
private def publications: NodeSeq =
|
||||
{
|
||||
for(conf <- buildConfigurations) yield
|
||||
<artifact name={sbt(conf).toString} conf={conf.toString}/>
|
||||
}
|
||||
/** Creates the main, optional, and scalac configurations for each Scala version*/
|
||||
private def variableConfigurations: NodeSeq =
|
||||
{
|
||||
buildConfigurations flatMap
|
||||
{ conf =>
|
||||
scalaComment(conf) ++
|
||||
(<conf name={conf.toString} extends={conf_base.toString}/>
|
||||
<conf name={optional(conf).toString} extends={optional(conf_base).toString}/>
|
||||
<conf name={scalac(conf).toString} visibility="private"/>)
|
||||
}
|
||||
}
|
||||
/** Defines the dependencies for the given version of Scala, ScalaTest, and Specs. If uniformTestOrg is true,
|
||||
* the 'org.scala-tools.testing' organization is used. Otherwise, 'org.' is prefixed to the module name. */
|
||||
private def variableDependencies(scalaVersion: Configuration, scalaTestVersion: String, specsVersion: String, uniformTestOrg: Boolean) =
|
||||
{
|
||||
if(buildConfigurations.contains(scalaVersion))
|
||||
{
|
||||
scalaComment(scalaVersion) ++
|
||||
{
|
||||
if(scalaVersion eq conf_2_8_0)
|
||||
Nil
|
||||
else
|
||||
{
|
||||
testDependency("scalatest", scalaTestVersion, uniformTestOrg, scalaVersion) ++
|
||||
testDependency("specs", specsVersion, uniformTestOrg, scalaVersion) ++
|
||||
testDependency("scalacheck", "1.5", false, scalaVersion)
|
||||
}
|
||||
} ++
|
||||
scalaDependency("scala-compiler", scalaVersion) ++ scalaDependency("scala-library", scalaVersion) ++
|
||||
{
|
||||
if(scalaVersion == conf_2_8_0)
|
||||
<dependency org="jline" name="jline" rev="0.9.94" transitive="false" conf={depConf(conf_2_8_0)}/>
|
||||
else
|
||||
NodeSeq.Empty
|
||||
}
|
||||
}
|
||||
else
|
||||
Nil
|
||||
}
|
||||
private def scalaDependency(name: String, scalaVersion: Configuration) =
|
||||
<dependency org="org.scala-lang" name={name} rev={scalaVersion.toString} conf={depConf(scalac(scalaVersion))}/>
|
||||
|
||||
/** Creates a comment containing the version of Scala*/
|
||||
private def scalaComment(scalaVersion: Configuration) = scala.xml.Comment("Scala " + scalaVersion)
|
||||
/** Creates a dependency element for a test. See 'testOrg' for a description of uniformTestOrg.*/
|
||||
|
||||
private def testDependency(name: String, version: String, uniformTestOrg: Boolean, baseConf: Configuration) =
|
||||
<dependency org={testOrg(name, uniformTestOrg)} name={name} rev={version} transitive="false" conf={depConf(optional(baseConf))}/>
|
||||
|
||||
/** Returns the organization for the given test library. If uniform is true,
|
||||
* the 'org.scala-tools.testing' organization is used. Otherwise, 'org.' is prefixed to the module name.*/
|
||||
private def testOrg(name: String, uniform: Boolean) =
|
||||
if(uniform) "org.scala-tools.testing"
|
||||
else "org." + name
|
||||
|
||||
/** Disable filtering Scala jars from dependency management, because we need them and are putting them
|
||||
* in custom configurations and are using them in a separate process than sbt runs in.*/
|
||||
override def filterScalaJars = false
|
||||
|
||||
/** The lib directory is now only for building using the 'build' script.*/
|
||||
override def unmanagedClasspath = path("ignore_lib_directory")
|
||||
/** When cross-compiling, replace mainCompilePath with the classes directory for the version being compiled.*/
|
||||
override def fullUnmanagedClasspath(config: Configuration) =
|
||||
if( (Configurations.Default :: Configurations.defaultMavenConfigurations) contains config)
|
||||
super.fullUnmanagedClasspath(config)
|
||||
else
|
||||
classesPath(config) +++ mainResourcesPath
|
||||
|
||||
// include the optional-<version> dependencies as well as the ones common across all scala versions
|
||||
def optionalClasspath(version: Configuration) = fullClasspath(optional(version)) +++ super.optionalClasspath
|
||||
|
||||
private val CompilerMainClass = "scala.tools.nsc.Main"
|
||||
// use a publish configuration that publishes the 'base' + all <version> configurations (base is required because
|
||||
// the <version> configurations extend it)
|
||||
private val conf = new DefaultPublishConfiguration("local", "release")
|
||||
{
|
||||
override def configurations: Option[Iterable[Configuration]] = Some(config(base) :: buildConfigurations)
|
||||
}
|
||||
// the actions for cross-version packaging and publishing
|
||||
lazy val crossPackage = buildConfigurations.map(packageForScala)
|
||||
lazy val crossDeliverLocal = deliverTask(conf, updateOptions) dependsOn(crossPackage : _*)
|
||||
lazy val crossPublishLocal = publishTask(conf, updateOptions) dependsOn(crossDeliverLocal)
|
||||
// Creates a task that produces a packaged sbt compiled against Scala scalaVersion.
|
||||
// The jar is named 'sbt_<scala-version>-<sbt-version>.jar'
|
||||
private def packageForScala(scalaVersion: Configuration) =
|
||||
{
|
||||
val classes = classesPath(scalaVersion) ** "*"
|
||||
val jarName = crossJarName(scalaVersion)
|
||||
val packageActionName = crossActionName("package", scalaVersion)
|
||||
val compileAction = compileForScala(scalaVersion) named(crossActionName("compile", scalaVersion))
|
||||
packageTask(classes +++ mainResources, outputPath, jarName, packageOptions) dependsOn(compileAction) named(packageActionName)
|
||||
}
|
||||
private def crossActionName(base: String, scalaVersion: Configuration) = base + " [ " + scalaVersion.toString + " ] "
|
||||
private def crossJarName(scalaVersion: Configuration) = sbt(scalaVersion) + "-" + version.toString + ".jar"
|
||||
// This creates a task that compiles sbt against the given version of scala. Classes are put in classes-<scalaVersion>.
|
||||
private def compileForScala(version: Configuration)=
|
||||
task
|
||||
{
|
||||
val classes = classesPath(version)
|
||||
val toClean = (outputPath / crossJarName(version)) +++ (classes ** "*")
|
||||
val setupResult =
|
||||
FileUtilities.clean(toClean.get, true, log) orElse
|
||||
FileUtilities.createDirectory(classes, log)
|
||||
for(err <- setupResult) log.error(err)
|
||||
// the classpath containing the scalac compiler
|
||||
val compilerClasspath = concatPaths(fullClasspath(scalac(version)))
|
||||
|
||||
// The libraries to compile sbt against
|
||||
val classpath = fullClasspath(version) +++ optionalClasspath(version)
|
||||
val sources: List[String] = pathListStrings(mainSources)
|
||||
val compilerOptions = List("-cp", concatPaths(classpath), "-d", classes.toString)
|
||||
val compilerArguments: List[String] = compilerOptions ::: sources
|
||||
|
||||
// the compiler classpath has to be appended to the boot classpath to work properly
|
||||
val allArguments = "-Xmx512M" :: ("-Xbootclasspath/a:" + compilerClasspath) :: CompilerMainClass :: compilerArguments
|
||||
log.debug("Running external compiler with command: java " + allArguments.mkString(" "))
|
||||
val exitValue = Process("java", allArguments) ! log
|
||||
if(exitValue == 0)
|
||||
None
|
||||
else
|
||||
Some("Nonzero exit value (" + exitValue + ") when calling scalac " + version + " with options: \n" + compilerOptions.mkString(" "))
|
||||
}
|
||||
private def concatPaths(p: PathFinder): String = Path.makeString(p.get)
|
||||
private def pathListStrings(p: PathFinder): List[String] = p.get.map(_.absolutePath).toList
|
||||
private def classesPath(scalaVersion: Configuration) = ("target" / ("classes-" + scalaVersion.toString)) ##
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
import sbt._
|
||||
|
||||
import LoaderProject._
|
||||
import java.io.File
|
||||
|
||||
// a project for the sbt launcher
|
||||
// the main content of this project definition is setting up and running proguard
|
||||
// to combine and compact all dependencies into a single jar
|
||||
protected/* removes the ambiguity as to which project is the entry point by making this class non-public*/
|
||||
class LoaderProject(info: ProjectInfo) extends DefaultProject(info)
|
||||
{
|
||||
val mainClassName = "sbt.boot.Boot"
|
||||
val baseName = "sbt-launcher"
|
||||
val proguardConfigurationPath: Path = outputPath / "proguard.pro"
|
||||
lazy val outputJar: Path = rootProject.outputPath / (baseName + "-" + version + ".jar")
|
||||
def rootProjectDirectory = rootProject.info.projectPath
|
||||
|
||||
override def mainClass = Some(mainClassName)
|
||||
override def defaultJarBaseName = baseName + "-" + version.toString
|
||||
|
||||
/****** Resources *****/
|
||||
def extraResources = descendents(info.projectPath / "licenses", "*") +++ "LICENSE" +++ "NOTICE"
|
||||
override def mainResources = super.mainResources +++ extraResources
|
||||
|
||||
/****** Dependencies *******/
|
||||
val defaultConfig = config("default")
|
||||
val toolsConfig = config("tools")
|
||||
val ivy = "org.apache.ivy" % "ivy" % "2.0.0"
|
||||
val proguardJar = "net.sf.proguard" % "proguard" % "4.3" % "tools->default"
|
||||
|
||||
/******** Proguard *******/
|
||||
lazy val proguard = proguardTask dependsOn(`package`, writeProguardConfiguration) describedAs(ProguardDescription)
|
||||
lazy val writeProguardConfiguration = writeProguardConfigurationTask dependsOn `package` describedAs WriteProguardDescription
|
||||
|
||||
private def proguardTask =
|
||||
task
|
||||
{
|
||||
FileUtilities.clean(outputJar :: Nil, log)
|
||||
val proguardClasspath = managedClasspath(toolsConfig)
|
||||
val proguardClasspathString = Path.makeString(proguardClasspath.get)
|
||||
val configFile = proguardConfigurationPath.asFile.getAbsolutePath
|
||||
val exitValue = Process("java", List("-Xmx128M", "-cp", proguardClasspathString, "proguard.ProGuard", "@" + configFile)) ! log
|
||||
if(exitValue == 0) None else Some("Proguard failed with nonzero exit code (" + exitValue + ")")
|
||||
}
|
||||
private def writeProguardConfigurationTask =
|
||||
task
|
||||
{
|
||||
// these are classes that need to be explicitly kept because they are loaded reflectively
|
||||
val ivyKeepResolvers =
|
||||
"org.apache.ivy.plugins.resolver.URLResolver" ::
|
||||
"org.apache.ivy.plugins.resolver.IBiblioResolver" ::
|
||||
Nil
|
||||
// the template for the proguard configuration file
|
||||
val outTemplate = """
|
||||
|-dontoptimize
|
||||
|-dontobfuscate
|
||||
|-dontnote
|
||||
|-dontwarn
|
||||
|-libraryjars %s
|
||||
|-injars %s(!META-INF/**,!fr/**,!**/antlib.xml,!**/*.png)
|
||||
|-injars %s(!META-INF/**)
|
||||
|%s
|
||||
|-outjars %s
|
||||
|-ignorewarnings
|
||||
|%s
|
||||
|%s
|
||||
|-keep public class %s {
|
||||
| public static void main(java.lang.String[]);
|
||||
|}"""
|
||||
|
||||
val defaultJar = (outputPath / defaultJarName).asFile.getAbsolutePath
|
||||
log.debug("proguard configuration using main jar " + defaultJar)
|
||||
val ivyKeepOptions = ivyKeepResolvers.map("-keep public class " + _ + allPublic).mkString("\n")
|
||||
val runtimeClasspath = runClasspath.get.map(_.asFile).toList
|
||||
val jlineJars = runtimeClasspath.filter(isJLineJar)
|
||||
val externalDependencies = (mainCompileConditional.analysis.allExternals).map(_.getAbsoluteFile).filter(_.getName.endsWith(".jar"))
|
||||
log.debug("proguard configuration external dependencies: \n\t" + externalDependencies.mkString("\n\t"))
|
||||
// partition jars from the external jar dependencies of this project by whether they are located in the project directory
|
||||
// if they are, they are specified with -injars, otherwise they are specified with -libraryjars
|
||||
val (externalJars, libraryJars) = externalDependencies.toList.partition(jar => Path.relativize(rootProjectDirectory, jar).isDefined)
|
||||
log.debug("proguard configuration library jars locations: " + libraryJars.mkString(", "))
|
||||
// pull out Ivy in order to exclude resources inside
|
||||
val (ivyJars, externalJarsNoIvy) = externalJars.partition(_.getName.startsWith("ivy"))
|
||||
log.debug("proguard configuration ivy jar location: " + ivyJars.mkString(", "))
|
||||
// the loader uses JLine, so there is a dependency on the compiler (because JLine is distributed with the compiler,
|
||||
// it finds the JLine classes from the compiler jar instead of the jline jar on the classpath), but we don't want to
|
||||
// include the version of JLine from the compiler.
|
||||
val includeExternalJars = externalJarsNoIvy.filter(jar => !isJarX(jar, "scala-compiler"))
|
||||
// exclude properties files and manifests from scala-library jar
|
||||
val inJars = (defaultJar :: includeExternalJars.map( _ + "(!META-INF/**,!*.properties)")).map("-injars " + _).mkString("\n")
|
||||
|
||||
withJar(ivyJars, "Ivy") { ivyJar =>
|
||||
withJar(jlineJars, "JLine") { jlineJar =>
|
||||
val proguardConfiguration =
|
||||
outTemplate.stripMargin.format(libraryJars.mkString(File.pathSeparator),
|
||||
ivyJar.getAbsolutePath, jlineJar.getAbsolutePath,
|
||||
inJars, outputJar.absolutePath, ivyKeepOptions, keepJLine, mainClassName)
|
||||
log.debug("Proguard configuration written to " + proguardConfigurationPath)
|
||||
FileUtilities.write(proguardConfigurationPath.asFile, proguardConfiguration, log)
|
||||
}
|
||||
}
|
||||
}
|
||||
private def withJar(files: List[File], name: String)(f: File => Option[String]): Option[String] =
|
||||
files match
|
||||
{
|
||||
case Nil => Some(name + " not present (try running update)")
|
||||
case jar :: _ => f(jar)
|
||||
}
|
||||
private def isJLineJar(file: File) = isJarX(file, "jline")
|
||||
private def isJarX(file: File, x: String) =
|
||||
{
|
||||
val name = file.getName
|
||||
name.startsWith(x) && name.endsWith(".jar")
|
||||
}
|
||||
// class body declaration for proguard that keeps all public members
|
||||
private val allPublic = " {\n public * ;\n}"
|
||||
|
||||
private val keepJLine =
|
||||
"""
|
||||
|-keep public class jline.** {
|
||||
| public protected *;
|
||||
|}
|
||||
""".stripMargin
|
||||
}
|
||||
object LoaderProject
|
||||
{
|
||||
val ProguardDescription = "Produces the final compacted jar that contains only the minimum classes needed using proguard."
|
||||
val WriteProguardDescription = "Creates the configuration file to use with proguard."
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
import sbt._
|
||||
|
||||
import java.io.File
|
||||
|
||||
class SbtProject(info: ProjectInfo) extends ParentProject(info)
|
||||
{
|
||||
// Launcher sub project.
|
||||
lazy val boot = project("boot", "Simple Build Tool Loader", new LoaderProject(_))
|
||||
// Main builder sub project
|
||||
lazy val main = project(info.projectPath, "Simple Build Tool", new MainProject(_))
|
||||
// One-shot build for users building from trunk
|
||||
lazy val fullBuild = task { None } dependsOn(boot.proguard, main.crossPublishLocal) describedAs
|
||||
"Builds the loader and builds main sbt against all supported versions of Scala and installs to the local repository."
|
||||
|
||||
override def shouldCheckOutputDirectories = false
|
||||
override def baseUpdateOptions = QuietUpdate :: Nil
|
||||
|
||||
override def parallelExecution = true
|
||||
override def deliverLocalAction = noAction
|
||||
private def noAction = task { None }
|
||||
override def publishLocalAction = noAction
|
||||
}
|
||||
|
||||
protected class MainProject(val info: ProjectInfo) extends CrossCompileProject
|
||||
{
|
||||
override def mainScalaSources =
|
||||
if(Project.currentScalaVersion.map(_.startsWith("2.8")).getOrElse(false)) // cannot compile against test libraries currently
|
||||
Path.lazyPathFinder { super.mainScalaSources.get.filter(_.asFile.getName.endsWith("TestFrameworkImpl.scala")) }
|
||||
else
|
||||
super.mainScalaSources
|
||||
override def defaultJarBaseName = "sbt_" + version.toString
|
||||
/** Additional resources to include in the produced jar.*/
|
||||
def extraResources = descendents(info.projectPath / "licenses", "*") +++ "LICENSE" +++ "NOTICE"
|
||||
override def mainResources = super.mainResources +++ extraResources
|
||||
override def mainClass = Some("sbt.Main")
|
||||
override def testOptions = ExcludeTests("sbt.ReflectiveSpecification" :: "sbt.ProcessSpecification" :: Nil) :: super.testOptions.toList
|
||||
|
||||
// ======== Scripted testing ==========
|
||||
|
||||
def sbtTestResources = testResourcesPath / "sbt-test-resources"
|
||||
|
||||
lazy val testNoScripted = super.testAction
|
||||
override def testAction = testNoScripted dependsOn(scripted)
|
||||
lazy val scripted = scriptedTask dependsOn(testCompile, `package`)
|
||||
def scriptedTask =
|
||||
task
|
||||
{
|
||||
log.info("Running scripted tests...")
|
||||
log.info("")
|
||||
// load ScriptedTests using a ClassLoader that loads from the project classpath so that the version
|
||||
// of sbt being built is tested, not the one doing the building.
|
||||
val loader = ScriptedLoader(scriptedClasspath.toArray)
|
||||
val scriptedClass = Class.forName(ScriptedClassName, true, loader)
|
||||
val scriptedConstructor = scriptedClass.getConstructor(classOf[File], classOf[Function2[String, String, Boolean]])
|
||||
val runner = scriptedConstructor.newInstance(sbtTestResources.asFile, filter)
|
||||
runner.asInstanceOf[{def scriptedTests(log: Logger): Option[String]}].scriptedTests(log)
|
||||
}
|
||||
/** The classpath to use for scripted tests. This ensures that the version of sbt being built it the one used for testing.*/
|
||||
private def scriptedClasspath =
|
||||
{
|
||||
val buildClasspath = classOf[SbtProject]. getProtectionDomain.getCodeSource.getLocation.toURI.toURL
|
||||
val scalacJar = FileUtilities.scalaCompilerJar.toURI.toURL
|
||||
val ivy = runClasspath.get.filter(_.asFile.getName.startsWith("ivy-")).map(_.asURL).toList
|
||||
val builtSbtJar = (outputPath / defaultJarName).asURL
|
||||
builtSbtJar :: buildClasspath :: scalacJar :: ivy
|
||||
}
|
||||
|
||||
val ScriptedClassName = "scripted.ScriptedTests"
|
||||
|
||||
val filter = (group: String, name: String) => true
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
import sbt._
|
||||
|
||||
import java.net.URL
|
||||
|
||||
package sbt { // need access to LoaderBase, which is private in package sbt
|
||||
object ScriptedLoader
|
||||
{
|
||||
def apply(paths: Array[URL]): ClassLoader = new ScriptedLoader(paths)
|
||||
}
|
||||
private class ScriptedLoader(paths: Array[URL]) extends LoaderBase(paths, classOf[ScriptedLoader].getClassLoader)
|
||||
{
|
||||
private val delegateFor = List("sbt.Logger", "sbt.LogEvent", "sbt.SetLevel", "sbt.Success", "sbt.Log", "sbt.SetTrace", "sbt.Trace", "sbt.ControlEvent")
|
||||
def doLoadClass(className: String): Class[_] =
|
||||
{
|
||||
// Logger needs to be loaded from the version of sbt building the project because we need to pass
|
||||
// a Logger from that loader into ScriptedTests.
|
||||
// All other sbt classes should be loaded from the project classpath so that we test those classes with 'scripted'
|
||||
if(!shouldDelegate(className) && (className.startsWith("sbt.") || className.startsWith("scripted.") || className.startsWith("scala.tools.")))
|
||||
findClass(className)
|
||||
else
|
||||
selfLoadClass(className)
|
||||
}
|
||||
|
||||
private def shouldDelegate(className: String) = delegateFor.exists(check => isNestedOrSelf(className, check))
|
||||
private def isNestedOrSelf(className: String, checkAgainst: String) =
|
||||
className == checkAgainst || className.startsWith(checkAgainst + "$")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
|
||||
package scripted
|
||||
|
||||
import sbt._
|
||||
import java.io.File
|
||||
|
||||
trait ScriptedTestFilter extends NotNull
|
||||
{
|
||||
def accept(group: String, name: String): Boolean
|
||||
}
|
||||
class BasicFilter(f: (String, String) => Boolean) extends ScriptedTestFilter
|
||||
{
|
||||
def accept(group: String, name: String) = f(group, name)
|
||||
}
|
||||
|
||||
object AcceptAllFilter extends ScriptedTestFilter
|
||||
{
|
||||
def accept(group: String, name: String): Boolean = true
|
||||
}
|
||||
class ScriptedTests(testResources: Resources, filter: ScriptedTestFilter) extends NotNull
|
||||
{
|
||||
def this(resourceBaseDirectory: File, filter: (String, String) => Boolean) = this(new Resources(resourceBaseDirectory), new BasicFilter(filter))
|
||||
def this(resourceBaseDirectory: File, filter: ScriptedTestFilter) = this(new Resources(resourceBaseDirectory), filter)
|
||||
def this(testResources: Resources) = this(testResources, AcceptAllFilter)
|
||||
def this(resourceBaseDirectory: File) = this(new Resources(resourceBaseDirectory))
|
||||
|
||||
val ScriptFilename = "test"
|
||||
import testResources._
|
||||
|
||||
private def includeDirectory(file: File) = file.getName != ".svn"
|
||||
def scriptedTests(log: Logger): Option[String] =
|
||||
{
|
||||
System.setProperty("sbt.scala.version", "")
|
||||
var success = true
|
||||
for(group <- baseDirectory.listFiles(DirectoryFilter) if includeDirectory(group))
|
||||
{
|
||||
log.info("Test group " + group.getName)
|
||||
for(test <- group.listFiles(DirectoryFilter) if includeDirectory(test))
|
||||
{
|
||||
val testName = test.getName
|
||||
if(!filter.accept(group.getName, testName))
|
||||
log.warn(" Test " + testName + " skipped.")
|
||||
else
|
||||
scriptedTest(test, log) match
|
||||
{
|
||||
case Some(err) =>
|
||||
log.error(" Test " + testName + " failed: " + err)
|
||||
success = false
|
||||
case None => log.info(" Test " + testName + " succeeded.")
|
||||
}
|
||||
}
|
||||
}
|
||||
if(success)
|
||||
None
|
||||
else
|
||||
Some("One or more tests failed.")
|
||||
}
|
||||
|
||||
def scriptedTest(group: String, name: String, log: Logger): Option[String] =
|
||||
readOnlyResourceDirectory(group, name).fold(err => Some(err), testDirectory => scriptedTest(testDirectory, log))
|
||||
def scriptedTest(testDirectory: File, log: Logger): Option[String] =
|
||||
{
|
||||
(for(script <- (new TestScriptParser(testDirectory, log)).parse(new File(testDirectory, ScriptFilename)).right;
|
||||
u <- withProject(testDirectory, log)(script).right )
|
||||
yield u).left.toOption
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
|
||||
package scripted
|
||||
|
||||
import sbt._
|
||||
import java.io.{BufferedReader, File, InputStreamReader}
|
||||
|
||||
/*
|
||||
statement*
|
||||
statement ::= ('$' | '>') word+ '[' word ']'
|
||||
word ::= [^ \[\]]+
|
||||
comment ::= '#' [^ \n\r]* ('\n' | '\r' | eof)
|
||||
*/
|
||||
import scala.util.parsing.combinator._
|
||||
import scala.util.parsing.input.Positional
|
||||
|
||||
import TestScriptParser._
|
||||
private class TestScriptParser(baseDirectory: File, log: Logger) extends RegexParsers with NotNull
|
||||
{
|
||||
type Statement = Project => Either[String, ReloadProject]
|
||||
type PStatement = Statement with Positional
|
||||
|
||||
private def evaluateList(list: List[PStatement])(p: Project): WithProjectResult[Unit] =
|
||||
list match
|
||||
{
|
||||
case Nil => ValueResult(())
|
||||
case head :: tail =>
|
||||
head(p) match
|
||||
{
|
||||
case Left(msg) => new ErrorResult(msg)
|
||||
case Right(reload) => ContinueResult(p =>evaluateList(tail)(p), reload)
|
||||
}
|
||||
}
|
||||
|
||||
def script: Parser[Project => WithProjectResult[Unit]] = rep1(space ~> statement <~ space) ^^ evaluateList
|
||||
def statement: Parser[PStatement] =
|
||||
positioned
|
||||
{
|
||||
(StartRegex ~! rep1(word) ~! "[" ~! word ~! "]") ^^
|
||||
{
|
||||
case start ~ command ~ open ~ result ~ close =>
|
||||
val successExpected = result.toLowerCase == SuccessLiteral.toLowerCase
|
||||
new Statement with Positional
|
||||
{ selfPositional =>
|
||||
def apply(p: Project) =
|
||||
{
|
||||
val result =
|
||||
try
|
||||
{
|
||||
start match
|
||||
{
|
||||
case CommandStart => evaluateCommand(command, successExpected, selfPositional)(p)
|
||||
case ActionStart => evaluateAction(command, successExpected)(p).toLeft(NoReload)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
case e: Exception =>
|
||||
log.trace(e)
|
||||
Left(e.toString)
|
||||
}
|
||||
result.left.map(message => linePrefix(this) + message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private def linePrefix(p: Positional) = "{line " + p.pos.line + "} "
|
||||
def space = """(\s+|(\#[^\n\r]*))*""".r
|
||||
def word: Parser[String] = ("\'" ~> "[^'\n\r]*".r <~ "\'") | ("\"" ~> "[^\"\n\r]*".r <~ "\"") | WordRegex
|
||||
def parse(scriptFile: File): Either[String, Project => WithProjectResult[Unit]] =
|
||||
{
|
||||
def parseReader(reader: java.io.Reader) =
|
||||
parseAll(script, reader) match
|
||||
{
|
||||
case Success(result, next) => Right(result)
|
||||
case err: NoSuccess =>
|
||||
{
|
||||
val pos = err.next.pos
|
||||
Left("Could not parse test script '" + scriptFile.getCanonicalPath +
|
||||
"' (" + pos.line + "," + pos.column + "): " + err.msg)
|
||||
}
|
||||
}
|
||||
FileUtilities.readValue(scriptFile, log)(parseReader)
|
||||
}
|
||||
|
||||
private def scriptError(message: String): Some[String] = Some("Test script error: " + message)
|
||||
private def wrongArguments(commandName: String, args: List[String]): Some[String] =
|
||||
scriptError("Command '" + commandName + "' does not accept arguments (found '" + spacedString(args) + "').")
|
||||
private def wrongArguments(commandName: String, requiredArgs: String, args: List[String]): Some[String] =
|
||||
scriptError("Wrong number of arguments to " + commandName + " command. " + requiredArgs + " required, found: '" + spacedString(args) + "'.")
|
||||
private def evaluateCommand(command: List[String], successExpected: Boolean, position: Positional)(project: Project): Either[String, ReloadProject] =
|
||||
{
|
||||
command match
|
||||
{
|
||||
case "reload" :: Nil => Right(if(successExpected) new ReloadSuccessExpected(linePrefix(position)) else ReloadErrorExpected)
|
||||
case x => evaluateCommandNoReload(x, successExpected)(project).toLeft(NoReload)
|
||||
}
|
||||
}
|
||||
private def evaluateCommandNoReload(command: List[String], successExpected: Boolean)(project: Project): Option[String] =
|
||||
{
|
||||
evaluate(successExpected, "Command '" + command.firstOption.getOrElse("") + "'", project)
|
||||
{
|
||||
command match
|
||||
{
|
||||
case Nil => scriptError("No command specified.")
|
||||
case "touch" :: paths => touch(paths, project)
|
||||
case "delete" :: paths => delete(paths, project)
|
||||
case "mkdir" :: paths => makeDirectories(paths, project)
|
||||
case "copy-file" :: from :: to :: Nil => copyFile(from, to, project)
|
||||
case "copy-file" :: args => wrongArguments("copy-file", "Two paths", args)
|
||||
case "sync" :: from :: to :: Nil => sync(from, to, project)
|
||||
case "sync" :: args => wrongArguments("sync", "Two directory paths", args)
|
||||
case "copy" :: paths => copy(paths, project)
|
||||
case "exists" :: paths => exists(paths, project)
|
||||
case "absent" :: paths => absent(paths, project)
|
||||
case "pause" :: Nil => readLine("Press enter to continue. "); println(); None
|
||||
case "pause" :: args => wrongArguments("pause", args)
|
||||
case "newer" :: a :: b :: Nil => newer(a, b, project)
|
||||
case "newer" :: args => wrongArguments("newer", "Two paths", args)
|
||||
case "sleep" :: time :: Nil => trap("Error while sleeping:") { Thread.sleep(time.toLong) }
|
||||
case "sleep" :: args => wrongArguments("sleep", "Time in milliseconds", args)
|
||||
case "exec" :: command :: args => execute(command, args, project)
|
||||
case "exec" :: other => wrongArguments("exec", "Command and arguments", other)
|
||||
case "reload" :: args => wrongArguments("reload", args)
|
||||
case unknown :: arguments => scriptError("Unknown command " + unknown)
|
||||
}
|
||||
}
|
||||
}
|
||||
private def foreachBufferedLogger(project: Project)(f: BufferedLogger => Unit)
|
||||
{
|
||||
project.topologicalSort.foreach(p => p.log match { case buffered: BufferedLogger => f(buffered); case _ => () })
|
||||
}
|
||||
private def evaluate(successExpected: Boolean, label: String, project: Project)(body: => Option[String]): Option[String] =
|
||||
{
|
||||
def startRecordingLog() { foreachBufferedLogger(project)(_.startRecording()) }
|
||||
def playLog() { foreachBufferedLogger(project)(_.playAll()) }
|
||||
def stopLog() { foreachBufferedLogger(project)(_.stop()) }
|
||||
|
||||
startRecordingLog()
|
||||
val result =
|
||||
body match
|
||||
{
|
||||
case None =>
|
||||
if(successExpected) None
|
||||
else
|
||||
{
|
||||
playLog()
|
||||
Some(label + " succeeded (expected failure).")
|
||||
}
|
||||
case Some(failure) =>
|
||||
if(successExpected)
|
||||
{
|
||||
playLog()
|
||||
Some(label + " failed (expected success): " + failure)
|
||||
}
|
||||
else None
|
||||
}
|
||||
stopLog()
|
||||
result
|
||||
}
|
||||
private def evaluateAction(action: List[String], successExpected: Boolean)(project: Project): Option[String] =
|
||||
{
|
||||
def actionToString = action.mkString(" ")
|
||||
action match
|
||||
{
|
||||
case Nil => scriptError("No action specified.")
|
||||
case head :: Nil if project.taskNames.toSeq.contains(head)=>
|
||||
evaluate(successExpected, "Action '" + actionToString + "'", project)(project.act(head))
|
||||
case head :: tail =>
|
||||
evaluate(successExpected, "Method '" + actionToString + "'", project)(project.call(head, tail.toArray))
|
||||
}
|
||||
}
|
||||
private def spacedString[T](l: Seq[T]) = l.mkString(" ")
|
||||
private def wrap(result: Option[String]) = result.flatMap(scriptError)
|
||||
private def trap(errorPrefix: String)(action: => Unit) = wrap( Control.trapUnit(errorPrefix, log) { action; None } )
|
||||
|
||||
private def fromStrings(paths: List[String], project: Project) = paths.map(path => fromString(path, project))
|
||||
private def fromString(path: String, project: Project) = Path.fromString(project.info.projectPath, path)
|
||||
private def touch(paths: List[String], project: Project) =
|
||||
if(paths.isEmpty)
|
||||
scriptError("No paths specified for touch command.")
|
||||
else
|
||||
wrap(lazyFold(paths) { path => FileUtilities.touch(fromString(path, project), log) })
|
||||
|
||||
private def delete(paths: List[String], project: Project) =
|
||||
if(paths.isEmpty)
|
||||
scriptError("No paths specified for delete command.")
|
||||
else
|
||||
wrap(FileUtilities.clean(fromStrings(paths, project), true, log))
|
||||
private def sync(from: String, to: String, project: Project) =
|
||||
wrap(FileUtilities.sync(fromString(from, project), fromString(to, project), log))
|
||||
private def copyFile(from: String, to: String, project: Project) =
|
||||
wrap(FileUtilities.copyFile(fromString(from, project), fromString(to, project), log))
|
||||
private def copy(paths: List[String], project: Project) =
|
||||
paths match
|
||||
{
|
||||
case Nil => scriptError("No paths specified for copy command.")
|
||||
case path :: Nil => scriptError("No destination specified for copy command.")
|
||||
case _ =>
|
||||
val mapped = fromStrings(paths, project).toArray
|
||||
val last = mapped.length - 1
|
||||
wrap(FileUtilities.copy(mapped.take(last), mapped(last), log).left.toOption)
|
||||
}
|
||||
private def makeDirectories(paths: List[String], project: Project) =
|
||||
fromStrings(paths, project) match
|
||||
{
|
||||
case Nil => scriptError("No paths specified for mkdir command.")
|
||||
case p => FileUtilities.createDirectories(p, project.log)
|
||||
}
|
||||
private def newer(a: String, b: String, project: Project) =
|
||||
trap("Error testing if '" + a + "' is newer than '" + b + "'")
|
||||
{
|
||||
val pathA = fromString(a, project)
|
||||
val pathB = fromString(b, project)
|
||||
pathA.exists && (!pathB.exists || pathA.lastModified > pathB.lastModified)
|
||||
}
|
||||
private def exists(paths: List[String], project: Project) =
|
||||
fromStrings(paths, project).filter(!_.exists) match
|
||||
{
|
||||
case Nil => None
|
||||
case x => Some("File(s) did not exist: " + x.mkString("[ ", " , ", " ]"))
|
||||
}
|
||||
private def absent(paths: List[String], project: Project) =
|
||||
fromStrings(paths, project).filter(_.exists) match
|
||||
{
|
||||
case Nil => None
|
||||
case x => Some("File(s) existed: " + x.mkString("[ ", " , ", " ]"))
|
||||
}
|
||||
private def execute(command: String, args: List[String], project: Project) =
|
||||
{
|
||||
if(command.trim.isEmpty)
|
||||
Some("Command was empty.")
|
||||
else
|
||||
{
|
||||
Control.trapUnit("Error running command: ", project.log)
|
||||
{
|
||||
val builder = new java.lang.ProcessBuilder((command :: args).toArray : _*).directory(project.info.projectDirectory)
|
||||
val exitValue = Process(builder) ! log
|
||||
if(exitValue == 0)
|
||||
None
|
||||
else
|
||||
Some("Nonzero exit value (" + exitValue + ")")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private object TestScriptParser
|
||||
{
|
||||
val SuccessLiteral = "success"
|
||||
val Failure = "error"
|
||||
val CommandStart = "$"
|
||||
val ActionStart = ">"
|
||||
val WordRegex = """[^ \[\]\s'\"][^ \[\]\s]*""".r
|
||||
val StartRegex = ("[" + CommandStart + ActionStart + "]").r
|
||||
|
||||
final def lazyFold[T](list: List[T])(f: T => Option[String]): Option[String] =
|
||||
list match
|
||||
{
|
||||
case Nil => None
|
||||
case head :: tail =>
|
||||
f(head) match
|
||||
{
|
||||
case None => lazyFold(tail)(f)
|
||||
case x => x
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<plugin>
|
||||
<name>sbt-analyze</name>
|
||||
<classname>sbt.Analyzer</classname>
|
||||
</plugin>
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
trait TaskAnalysis[Source, Product, External] extends NotNull
|
||||
{
|
||||
import scala.collection.Set
|
||||
def save(): Option[String]
|
||||
def revert(): Option[String]
|
||||
def clear(): Unit
|
||||
|
||||
def allSources: Set[Source]
|
||||
def allProducts: Set[Product]
|
||||
def allExternals: Set[External]
|
||||
|
||||
def sourceDependencies(source: Source): Option[Set[Source]]
|
||||
def products(source: Source): Option[Set[Product]]
|
||||
def externalDependencies(external: External): Option[Set[Source]]
|
||||
|
||||
def addSource(source: Source): Unit
|
||||
def addExternalDependency(dependsOn: External, source: Source): Unit
|
||||
def addSourceDependency(dependsOn: Source, source: Source): Unit
|
||||
def addProduct(source: Source, product: Product): Unit
|
||||
|
||||
def removeSource(source: Source): Unit
|
||||
def removeDependent(source: Source): Unit
|
||||
def removeDependencies(source: Source): Option[Set[Source]]
|
||||
def removeExternalDependency(external: External): Unit
|
||||
}
|
||||
|
||||
import java.io.File
|
||||
import BasicAnalysis._
|
||||
import impl.MapUtilities.{add, all, read, mark, readOnlyIterable, write}
|
||||
import scala.collection.mutable.{HashMap, HashSet, ListBuffer, Map, Set}
|
||||
|
||||
sealed class BasicAnalysis(analysisPath: Path, projectPath: Path, log: Logger) extends TaskAnalysis[Path, Path, File]
|
||||
{
|
||||
private val sourceDependencyMap: Map[Path, Set[Path]] = new HashMap
|
||||
private val productMap: Map[Path, Set[Path]] = new HashMap
|
||||
private val externalDependencyMap: Map[File, Set[Path]] = new HashMap
|
||||
|
||||
final type AnyMapToSource = Map[K, Set[Path]] forSome {type K}
|
||||
final type AnySourceMap = Map[Path, T] forSome {type T}
|
||||
final type AnySourceSetMap = Map[Path, Set[T]] forSome {type T}
|
||||
final type AnyMap = Map[K, V] forSome { type K; type V }
|
||||
|
||||
protected def mapsToClear = List[AnyMap](sourceDependencyMap, productMap, externalDependencyMap)
|
||||
protected def mapsToRemoveSource = List[AnySourceMap](sourceDependencyMap, productMap)
|
||||
protected def mapsToRemoveDependent = List[AnyMapToSource](sourceDependencyMap, externalDependencyMap)
|
||||
protected def mapsToMark = List[AnySourceSetMap](sourceDependencyMap, productMap)
|
||||
|
||||
def clear()
|
||||
{
|
||||
for(map <- mapsToClear)
|
||||
map.clear()
|
||||
}
|
||||
def removeSource(source: Path)
|
||||
{
|
||||
for(sourceProducts <- productMap.get(source))
|
||||
FileUtilities.clean(sourceProducts, true, log)
|
||||
for(map <- mapsToRemoveSource)
|
||||
map -= source
|
||||
}
|
||||
def removeSelfDependency(source: Path)
|
||||
{
|
||||
for(deps <- sourceDependencyMap.get(source))
|
||||
deps -= source
|
||||
}
|
||||
def removeDependent(source: Path)
|
||||
{
|
||||
for(map <- mapsToRemoveDependent; deps <- map.values)
|
||||
deps -= source
|
||||
}
|
||||
def removeDependencies(source: Path) = sourceDependencyMap.removeKey(source)
|
||||
def removeExternalDependency(dep: File) = externalDependencyMap.removeKey(dep.getAbsoluteFile)
|
||||
|
||||
def externalDependencies(external: File) = externalDependencyMap.get(external.getAbsoluteFile)
|
||||
def sourceDependencies(source: Path) = sourceDependencyMap.get(source)
|
||||
def products(sources: Iterable[Path]): Iterable[Path] =
|
||||
{
|
||||
val buffer = new ListBuffer[Path]
|
||||
for(source <- sources; sourceProducts <- productMap.get(source))
|
||||
buffer ++= sourceProducts
|
||||
buffer.readOnly
|
||||
}
|
||||
def products(source: Path) = productMap.get(source)
|
||||
|
||||
def allSources = sourceDependencyMap.keySet
|
||||
def allProducts: Set[Path] = HashSet(flatten(productMap.values.toList) : _*)
|
||||
def allExternals = externalDependencyMap.keySet
|
||||
|
||||
def allExternalDependencies = readOnlyIterable(externalDependencyMap)
|
||||
def allDependencies = readOnlyIterable(sourceDependencyMap)
|
||||
|
||||
def addSourceDependency(on: Path, from: Path) = add(on, from, sourceDependencyMap)
|
||||
def addExternalDependency(on: File, from: Path) = add(on.getAbsoluteFile, from, externalDependencyMap)
|
||||
def addProduct(source: Path, file: Path) = add(source, file, productMap)
|
||||
def addSource(source: Path) =
|
||||
{
|
||||
for(map <- mapsToMark)
|
||||
mark(source, map)
|
||||
}
|
||||
|
||||
import Format._ // get implicits for data types
|
||||
implicit val path: Format[Path] = Format.path(projectPath)
|
||||
implicit val pathSet: Format[Set[Path]] = Format.set
|
||||
|
||||
protected def backedMaps: Iterable[Backed[_,_]] =
|
||||
Backed(sourceDependencyMap, DependenciesLabel, DependenciesFileName) ::
|
||||
Backed(productMap, GeneratedLabel, GeneratedFileName) ::
|
||||
Backed(externalDependencyMap, ExternalDependenciesLabel, ExternalDependenciesFileName) ::
|
||||
Nil
|
||||
|
||||
def revert() = load()
|
||||
private def loadBacked[Key,Value](b: Backed[Key,Value]) = read(b.map, analysisPath / b.name, log)(b.keyFormat, b.valueFormat)
|
||||
private def storeBacked[Key,Value](b: Backed[Key,Value]) = write(b.map, b.label, analysisPath / b.name, log)(b.keyFormat, b.valueFormat)
|
||||
final def load(): Option[String] = Control.lazyFold(backedMaps.toList)(backed =>loadBacked(backed))
|
||||
final def save(): Option[String] = Control.lazyFold(backedMaps.toList)(backed => storeBacked(backed))
|
||||
}
|
||||
object BasicAnalysis
|
||||
{
|
||||
private def flatten(s: Iterable[Set[Path]]): Seq[Path] = s.flatMap(x => x.toSeq).toSeq
|
||||
|
||||
val GeneratedFileName = "generated_files"
|
||||
val DependenciesFileName = "dependencies"
|
||||
val ExternalDependenciesFileName = "external"
|
||||
|
||||
val GeneratedLabel = "Generated Classes"
|
||||
val DependenciesLabel = "Source Dependencies"
|
||||
val ExternalDependenciesLabel = "External Dependencies"
|
||||
|
||||
def load(analysisPath: Path, projectPath: Path, log: Logger): Either[String, BasicAnalysis] =
|
||||
{
|
||||
val analysis = new BasicAnalysis(analysisPath, projectPath, log)
|
||||
analysis.load().toLeft(analysis)
|
||||
}
|
||||
}
|
||||
object CompileAnalysis
|
||||
{
|
||||
val HashesFileName = "hashes"
|
||||
val TestsFileName = "tests"
|
||||
val ApplicationsFileName = "applications"
|
||||
val ProjectDefinitionsName = "projects"
|
||||
|
||||
val HashesLabel = "Source Hashes"
|
||||
val TestsLabel = "Tests"
|
||||
val ApplicationsLabel = "Classes with main methods"
|
||||
val ProjectDefinitionsLabel = "Project Definitions"
|
||||
|
||||
def load(analysisPath: Path, projectPath: Path, log: Logger): Either[String, CompileAnalysis] =
|
||||
{
|
||||
val analysis = new CompileAnalysis(analysisPath, projectPath, log)
|
||||
analysis.load().toLeft(analysis)
|
||||
}
|
||||
}
|
||||
import CompileAnalysis._
|
||||
import Format._ // get implicits for data types
|
||||
sealed class BasicCompileAnalysis protected (analysisPath: Path, projectPath: Path, log: Logger) extends BasicAnalysis(analysisPath, projectPath, log)
|
||||
{
|
||||
/*private */val hashesMap = new HashMap[Path, Array[Byte]]
|
||||
|
||||
override protected def mapsToClear = hashesMap :: super.mapsToClear
|
||||
override protected def mapsToRemoveSource = hashesMap :: super.mapsToRemoveSource
|
||||
|
||||
def setHash(source: Path, hash: Array[Byte]) { hashesMap(source) = hash }
|
||||
def clearHash(source: Path) { hashesMap.removeKey(source) }
|
||||
def hash(source: Path) = hashesMap.get(source)
|
||||
def clearHashes() { hashesMap.clear() }
|
||||
|
||||
def getClasses(sources: PathFinder, outputDirectory: Path): PathFinder =
|
||||
Path.lazyPathFinder
|
||||
{
|
||||
val basePath = (outputDirectory ##)
|
||||
for(c <- products(sources.get)) yield
|
||||
Path.relativize(basePath, c).getOrElse(c)
|
||||
}
|
||||
|
||||
implicit val stringSet: Format[Set[String]] = Format.set
|
||||
override protected def backedMaps = Backed(hashesMap, HashesLabel, HashesFileName) :: super.backedMaps.toList
|
||||
}
|
||||
private[sbt] final class BuilderCompileAnalysis(analysisPath: Path, projectPath: Path, log: Logger) extends BasicCompileAnalysis(analysisPath, projectPath, log)
|
||||
{
|
||||
private val projectDefinitionMap = new HashMap[Path, Set[String]]
|
||||
override protected def mapsToClear = projectDefinitionMap :: super.mapsToClear
|
||||
override protected def mapsToRemoveSource = projectDefinitionMap :: super.mapsToRemoveSource
|
||||
def allProjects = all(projectDefinitionMap)
|
||||
def addProjectDefinition(source: Path, className: String) = add(source, className, projectDefinitionMap)
|
||||
|
||||
override protected def backedMaps =
|
||||
Backed(projectDefinitionMap, ProjectDefinitionsLabel, ProjectDefinitionsName) ::
|
||||
super.backedMaps
|
||||
}
|
||||
final class CompileAnalysis(analysisPath: Path, projectPath: Path, log: Logger) extends BasicCompileAnalysis(analysisPath, projectPath, log)
|
||||
{
|
||||
private val testMap = new HashMap[Path, Set[TestDefinition]]
|
||||
private val applicationsMap = new HashMap[Path, Set[String]]
|
||||
def allTests = all(testMap)
|
||||
def allApplications = all(applicationsMap)
|
||||
def addTest(source: Path, test: TestDefinition) = add(source, test, testMap)
|
||||
def addApplication(source: Path, className: String) = add(source, className, applicationsMap)
|
||||
|
||||
def testSourceMap: Map[String, Path] =
|
||||
{
|
||||
val map = new HashMap[String, Path]
|
||||
for( (source, tests) <- testMap; test <- tests) map(test.testClassName) = source
|
||||
map
|
||||
}
|
||||
|
||||
override protected def mapsToClear = applicationsMap :: testMap :: super.mapsToClear
|
||||
override protected def mapsToRemoveSource = applicationsMap :: testMap :: super.mapsToRemoveSource
|
||||
|
||||
implicit val testSet: Format[Set[TestDefinition]] = Format.set
|
||||
override protected def backedMaps =
|
||||
Backed(testMap, TestsLabel, TestsFileName) ::
|
||||
Backed(applicationsMap, ApplicationsLabel, ApplicationsFileName) ::
|
||||
super.backedMaps
|
||||
}
|
||||
/** A map that is persisted in a properties file named 'name' and with 'label'. 'keyFormat' and 'valueFormat' are used to (de)serialize. */
|
||||
final case class Backed[Key, Value](map: Map[Key, Value], label: String, name: String)(implicit val keyFormat: Format[Key], val valueFormat: Format[Value]) extends NotNull
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import java.io.File
|
||||
|
||||
object AnalysisCallback
|
||||
{
|
||||
private val map = new scala.collection.mutable.HashMap[Int, AnalysisCallback]
|
||||
private var nextID: Int = 0
|
||||
def register(callback: AnalysisCallback): Int =
|
||||
{
|
||||
val id = nextID
|
||||
nextID += 1
|
||||
map(id) = callback
|
||||
id
|
||||
}
|
||||
def apply(id: Int): Option[AnalysisCallback] = map.get(id)
|
||||
def unregister(id: Int)
|
||||
{
|
||||
map -= id
|
||||
}
|
||||
}
|
||||
|
||||
trait AnalysisCallback extends NotNull
|
||||
{
|
||||
/** The names of classes that the analyzer should find subclasses of.*/
|
||||
def superclassNames: Iterable[String]
|
||||
/** The base path for the project.*/
|
||||
def basePath: Path
|
||||
/** Called when the the given superclass could not be found on the classpath by the compiler.*/
|
||||
def superclassNotFound(superclassName: String): Unit
|
||||
/** Called before the source at the given location is processed. */
|
||||
def beginSource(sourcePath: Path): Unit
|
||||
/** Called when the a subclass of one of the classes given in <code>superclassNames</code> is
|
||||
* discovered.*/
|
||||
def foundSubclass(sourcePath: Path, subclassName: String, superclassName: String, isModule: Boolean): Unit
|
||||
/** Called to indicate that the source file <code>sourcePath</code> depends on the source file
|
||||
* <code>dependsOnPath</code>.*/
|
||||
def sourceDependency(dependsOnPath: Path, sourcePath: Path): Unit
|
||||
/** Called to indicate that the source file <code>sourcePath</code> depends on the jar
|
||||
* <code>jarPath</code>.*/
|
||||
def jarDependency(jarPath: File, sourcePath: Path): Unit
|
||||
/** Called to indicate that the source file <code>sourcePath</code> depends on the class file
|
||||
* <code>classFile</code>.*/
|
||||
def classDependency(classFile: File, sourcePath: Path): Unit
|
||||
/** Called to indicate that the source file <code>sourcePath</code> produces a class file at
|
||||
* <code>modulePath</code>.*/
|
||||
def generatedClass(sourcePath: Path, modulePath: Path): Unit
|
||||
/** Called after the source at the given location has been processed. */
|
||||
def endSource(sourcePath: Path): Unit
|
||||
/** Called when a module with a public 'main' method with the right signature is found.*/
|
||||
def foundApplication(sourcePath: Path, className: String): Unit
|
||||
}
|
||||
abstract class BasicAnalysisCallback[A <: BasicCompileAnalysis](val basePath: Path, val superclassNames: Iterable[String],
|
||||
protected val analysis: A) extends AnalysisCallback
|
||||
{
|
||||
def superclassNotFound(superclassName: String) {}
|
||||
|
||||
def beginSource(sourcePath: Path)
|
||||
{
|
||||
analysis.addSource(sourcePath)
|
||||
}
|
||||
def sourceDependency(dependsOnPath: Path, sourcePath: Path)
|
||||
{
|
||||
analysis.addSourceDependency(dependsOnPath, sourcePath)
|
||||
}
|
||||
def jarDependency(jarFile: File, sourcePath: Path)
|
||||
{
|
||||
analysis.addExternalDependency(jarFile, sourcePath)
|
||||
}
|
||||
def classDependency(classFile: File, sourcePath: Path)
|
||||
{
|
||||
analysis.addExternalDependency(classFile, sourcePath)
|
||||
}
|
||||
def generatedClass(sourcePath: Path, modulePath: Path)
|
||||
{
|
||||
analysis.addProduct(sourcePath, modulePath)
|
||||
}
|
||||
def endSource(sourcePath: Path)
|
||||
{
|
||||
analysis.removeSelfDependency(sourcePath)
|
||||
}
|
||||
}
|
||||
abstract class BasicCompileAnalysisCallback(basePath: Path, superclassNames: Iterable[String], analysis: CompileAnalysis)
|
||||
extends BasicAnalysisCallback(basePath, superclassNames, analysis)
|
||||
{
|
||||
def foundApplication(sourcePath: Path, className: String)
|
||||
{
|
||||
analysis.addApplication(sourcePath, className)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import scala.tools.nsc.{io, plugins, symtab, Global, Phase}
|
||||
import io.{AbstractFile, PlainFile, ZipArchive}
|
||||
import plugins.{Plugin, PluginComponent}
|
||||
import symtab.Flags
|
||||
import scala.collection.mutable.{HashMap, HashSet, Map, Set}
|
||||
|
||||
import java.io.File
|
||||
|
||||
object Analyzer
|
||||
{
|
||||
val PluginName = "sbt-analyzer"
|
||||
val CallbackIDOptionName = "callback:"
|
||||
}
|
||||
class Analyzer(val global: Global) extends Plugin
|
||||
{
|
||||
import global._
|
||||
import Analyzer._
|
||||
|
||||
val name = PluginName
|
||||
val description = "A plugin to find all concrete instances of a given class and extract dependency information."
|
||||
val components = List[PluginComponent](Component)
|
||||
|
||||
private var callbackOption: Option[AnalysisCallback] = None
|
||||
|
||||
override def processOptions(options: List[String], error: String => Unit)
|
||||
{
|
||||
for(option <- options)
|
||||
{
|
||||
if(option.startsWith(CallbackIDOptionName))
|
||||
callbackOption = AnalysisCallback(option.substring(CallbackIDOptionName.length).toInt)
|
||||
else
|
||||
error("Option for sbt analyzer plugin not understood: " + option)
|
||||
}
|
||||
if(callbackOption.isEmpty)
|
||||
error("Callback ID not specified for sbt analyzer plugin.")
|
||||
}
|
||||
|
||||
override val optionsHelp: Option[String] =
|
||||
{
|
||||
val prefix = " -P:" + name + ":"
|
||||
Some(prefix + CallbackIDOptionName + "<callback-id> Set the callback id.\n")
|
||||
}
|
||||
|
||||
/* ================================================== */
|
||||
// These two templates abuse scope for source compatibility between Scala 2.7.x and 2.8.x so that a single
|
||||
// sbt codebase compiles with both series of versions.
|
||||
// In 2.8.x, PluginComponent.runsAfter has type List[String] and the method runsBefore is defined on
|
||||
// PluginComponent with default value Nil.
|
||||
// In 2.7.x, runsBefore does not exist on PluginComponent and PluginComponent.runsAfter has type String.
|
||||
//
|
||||
// Therefore, in 2.8.x, object runsBefore is shadowed by PluginComponent.runsBefore (which is Nil) and so
|
||||
// afterPhase :: runsBefore
|
||||
// is equivalent to List[String](afterPhase)
|
||||
// In 2.7.x, object runsBefore is not shadowed and so runsAfter has type String.
|
||||
private object runsBefore { def :: (s: String) = s }
|
||||
private abstract class CompatiblePluginComponent(afterPhase: String) extends PluginComponent
|
||||
{
|
||||
override val runsAfter = afterPhase :: runsBefore
|
||||
}
|
||||
/* ================================================== */
|
||||
|
||||
private object Component extends CompatiblePluginComponent("jvm")
|
||||
{
|
||||
val global = Analyzer.this.global
|
||||
val phaseName = Analyzer.this.name
|
||||
def newPhase(prev: Phase) = new AnalyzerPhase(prev)
|
||||
}
|
||||
|
||||
private class AnalyzerPhase(prev: Phase) extends Phase(prev)
|
||||
{
|
||||
def name = Analyzer.this.name
|
||||
def run
|
||||
{
|
||||
val callback = callbackOption.get
|
||||
val projectPath = callback.basePath
|
||||
val projectPathString = Path.basePathString(projectPath).getOrElse({error("Could not determine base path for " + projectPath); ""})
|
||||
def relativize(file: File) = Path.relativize(projectPath, projectPathString, file)
|
||||
|
||||
val outputDir = new File(global.settings.outdir.value)
|
||||
val outputPathOption = relativize(outputDir)
|
||||
if(outputPathOption.isEmpty)
|
||||
error("Output directory " + outputDir.getAbsolutePath + " must be in the project directory.")
|
||||
val outputPath = outputPathOption.get
|
||||
|
||||
val superclassNames = callback.superclassNames.map(newTermName)
|
||||
val superclassesAll =
|
||||
for(name <- superclassNames) yield
|
||||
{
|
||||
try { Some(global.definitions.getClass(name)) }
|
||||
catch { case fe: scala.tools.nsc.FatalError => callback.superclassNotFound(name.toString); None }
|
||||
}
|
||||
val superclasses = superclassesAll.filter(_.isDefined).map(_.get)
|
||||
|
||||
for(unit <- currentRun.units)
|
||||
{
|
||||
// build dependencies structure
|
||||
val sourceFile = unit.source.file.file
|
||||
val sourcePathOption = relativize(sourceFile)
|
||||
if(sourcePathOption.isEmpty)
|
||||
error("Source file " + sourceFile.getAbsolutePath + " must be in the project directory.")
|
||||
val sourcePath = sourcePathOption.get
|
||||
callback.beginSource(sourcePath)
|
||||
for(on <- unit.depends)
|
||||
{
|
||||
val onSource = on.sourceFile
|
||||
if(onSource == null)
|
||||
{
|
||||
classFile(on) match
|
||||
{
|
||||
case Some(f) =>
|
||||
{
|
||||
f match
|
||||
{
|
||||
case ze: ZipArchive#Entry => callback.jarDependency(new File(ze.getArchive.getName), sourcePath)
|
||||
case pf: PlainFile =>
|
||||
{
|
||||
// ignore dependencies in the output directory: these are handled by source dependencies
|
||||
if(Path.relativize(outputPath, pf.file).isEmpty)
|
||||
callback.classDependency(pf.file, sourcePath)
|
||||
}
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
case None => ()
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for(depPath <- relativize(onSource.file))
|
||||
callback.sourceDependency(depPath, sourcePath)
|
||||
}
|
||||
}
|
||||
|
||||
// find subclasses and modules with main methods
|
||||
for(clazz @ ClassDef(mods, n, _, _) <- unit.body)
|
||||
{
|
||||
val sym = clazz.symbol
|
||||
if(sym != NoSymbol && mods.isPublic && !mods.isAbstract && !mods.isTrait &&
|
||||
!sym.isImplClass && sym.isStatic && !sym.isNestedClass)
|
||||
{
|
||||
val isModule = sym.isModuleClass
|
||||
for(superclass <- superclasses.filter(sym.isSubClass))
|
||||
callback.foundSubclass(sourcePath, sym.fullNameString, superclass.fullNameString, isModule)
|
||||
if(isModule && hasMainMethod(sym))
|
||||
callback.foundApplication(sourcePath, sym.fullNameString)
|
||||
}
|
||||
}
|
||||
|
||||
// build list of generated classes
|
||||
for(iclass <- unit.icode)
|
||||
{
|
||||
val sym = iclass.symbol
|
||||
def addGenerated(separatorRequired: Boolean)
|
||||
{
|
||||
val classPath = pathOfClass(outputPath, sym, separatorRequired)
|
||||
if(classPath.asFile.exists)
|
||||
callback.generatedClass(sourcePath, classPath)
|
||||
}
|
||||
if(sym.isModuleClass && !sym.isImplClass)
|
||||
{
|
||||
if(isTopLevelModule(sym) && sym.linkedClassOfModule == NoSymbol)
|
||||
addGenerated(false)
|
||||
addGenerated(true)
|
||||
}
|
||||
else
|
||||
addGenerated(false)
|
||||
}
|
||||
callback.endSource(sourcePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def classFile(sym: Symbol): Option[AbstractFile] =
|
||||
{
|
||||
import scala.tools.nsc.symtab.Flags
|
||||
val name = sym.fullNameString(java.io.File.separatorChar) + (if (sym.hasFlag(Flags.MODULE)) "$" else "")
|
||||
val entry = classPath.root.find(name, false)
|
||||
if (entry ne null)
|
||||
Some(entry.classFile)
|
||||
else
|
||||
None
|
||||
}
|
||||
|
||||
private def isTopLevelModule(sym: Symbol): Boolean =
|
||||
atPhase (currentRun.picklerPhase.next) {
|
||||
sym.isModuleClass && !sym.isImplClass && !sym.isNestedClass
|
||||
}
|
||||
private def pathOfClass(outputPath: Path, s: Symbol, separatorRequired: Boolean): Path =
|
||||
pathOfClass(outputPath, s, separatorRequired, ".class")
|
||||
private def pathOfClass(outputPath: Path, s: Symbol, separatorRequired: Boolean, postfix: String): Path =
|
||||
{
|
||||
if(s.owner.isPackageClass && s.isPackageClass)
|
||||
packagePath(outputPath, s) / postfix
|
||||
else
|
||||
pathOfClass(outputPath, s.owner.enclClass, true, s.simpleName + (if(separatorRequired) "$" else "") + postfix)
|
||||
}
|
||||
private def packagePath(outputPath: Path, s: Symbol): Path =
|
||||
{
|
||||
if(s.isEmptyPackageClass || s.isRoot)
|
||||
outputPath
|
||||
else
|
||||
packagePath(outputPath, s.owner.enclClass) / s.simpleName.toString
|
||||
}
|
||||
|
||||
private def hasMainMethod(sym: Symbol): Boolean =
|
||||
{
|
||||
val main = sym.info.nonPrivateMember(newTermName("main"))//nme.main)
|
||||
main.tpe match
|
||||
{
|
||||
case OverloadedType(pre, alternatives) => alternatives.exists(alt => isVisible(alt) && isMainType(pre.memberType(alt)))
|
||||
case tpe => isVisible(main) && isMainType(main.owner.thisType.memberType(main))
|
||||
}
|
||||
}
|
||||
private def isVisible(sym: Symbol) = sym != NoSymbol && sym.isPublic && !sym.isDeferred
|
||||
private def isMainType(tpe: Type) =
|
||||
{
|
||||
tpe match
|
||||
{
|
||||
// singleArgument is of type Symbol in 2.8.0 and type Type in 2.7.x
|
||||
case MethodType(List(singleArgument), result) => isUnitType(result) && isStringArray(singleArgument)
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
private lazy val StringArrayType = appliedType(definitions.ArrayClass.typeConstructor, definitions.StringClass.tpe :: Nil)
|
||||
// isStringArray is overloaded to handle the incompatibility between 2.7.x and 2.8.0
|
||||
private def isStringArray(tpe: Type): Boolean = tpe.typeSymbol == StringArrayType.typeSymbol
|
||||
private def isStringArray(sym: Symbol): Boolean = isStringArray(sym.tpe)
|
||||
private def isUnitType(tpe: Type) = tpe.typeSymbol == definitions.UnitClass
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
trait AutoCompilerPlugins extends BasicScalaProject
|
||||
{
|
||||
import Configurations.CompilerPlugin
|
||||
abstract override def ivyConfigurations =
|
||||
{
|
||||
val superConfigurations = super.ivyConfigurations.toList
|
||||
val newConfigurations =
|
||||
if(superConfigurations.isEmpty)
|
||||
{
|
||||
if(useDefaultConfigurations)
|
||||
CompilerPlugin :: Configurations.defaultMavenConfigurations
|
||||
else
|
||||
Configurations.Default :: CompilerPlugin :: Nil
|
||||
}
|
||||
else
|
||||
CompilerPlugin :: superConfigurations
|
||||
log.debug("Auto configurations: " + newConfigurations.toList.mkString(", "))
|
||||
Configurations.removeDuplicates(newConfigurations)
|
||||
}
|
||||
abstract override def compileOptions = compilerPlugins ++ super.compileOptions
|
||||
|
||||
/** A PathFinder that provides the classpath to search for compiler plugins. */
|
||||
def pluginClasspath = fullClasspath(CompilerPlugin)
|
||||
protected def compilerPlugins: List[CompileOption] =
|
||||
ClasspathUtilities.compilerPlugins(pluginClasspath.get).map(plugin => new CompileOption("-Xplugin:" + plugin.getAbsolutePath)).toList
|
||||
|
||||
def compilerPlugin(dependency: ModuleID) = dependency % "plugin->default(compile)"
|
||||
}
|
||||
|
|
@ -0,0 +1,578 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import StringUtilities.{appendable,nonEmpty}
|
||||
|
||||
/** A project that provides a classpath. */
|
||||
trait ClasspathProject extends Project
|
||||
{
|
||||
/** The local classpath for this project.*/
|
||||
def projectClasspath(config: Configuration): PathFinder
|
||||
|
||||
/** Returns the classpath of this project and the classpaths of all dependencies for the
|
||||
* given configuration. Specifically, this concatentates projectClasspath(config) for all
|
||||
* projects of type ClasspathProject in topologicalSort. */
|
||||
def fullClasspath(config: Configuration): PathFinder =
|
||||
Path.lazyPathFinder
|
||||
{
|
||||
val set = new wrap.MutableSetWrapper(new java.util.LinkedHashSet[Path])
|
||||
for(project <- topologicalSort)
|
||||
{
|
||||
project match
|
||||
{
|
||||
case sp: ClasspathProject => set ++= sp.projectClasspath(config).get
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
set.toList
|
||||
}
|
||||
}
|
||||
trait BasicDependencyProject extends BasicManagedProject with UnmanagedClasspathProject
|
||||
{
|
||||
/** This returns the classpath for only this project for the given configuration.*/
|
||||
def projectClasspath(config: Configuration) = fullUnmanagedClasspath(config) +++ managedClasspath(config)
|
||||
}
|
||||
/** A project that provides a directory in which jars can be manually managed.*/
|
||||
trait UnmanagedClasspathProject extends ClasspathProject
|
||||
{
|
||||
/** The location of the manually managed (unmanaged) dependency directory.*/
|
||||
def dependencyPath: Path
|
||||
/** The classpath containing all jars in the unmanaged directory. */
|
||||
def unmanagedClasspath: PathFinder =
|
||||
{
|
||||
val base = descendents(dependencyPath, "*.jar")
|
||||
if(scratch)
|
||||
base +++ (info.projectPath * "*.jar")
|
||||
else
|
||||
base
|
||||
}
|
||||
/** The classpath containing all unmanaged classpath elements for the given configuration. This typically includes
|
||||
* at least 'unmanagedClasspath'.*/
|
||||
def fullUnmanagedClasspath(config: Configuration): PathFinder
|
||||
}
|
||||
|
||||
/** A project that provides automatic dependency management.*/
|
||||
trait ManagedProject extends ClasspathProject
|
||||
{
|
||||
trait ManagedOption extends ActionOption
|
||||
final class ManagedFlagOption extends ManagedOption
|
||||
/** An update option that specifies that unneeded files should be pruned from the managed library directory
|
||||
* after updating. */
|
||||
final val Synchronize = new ManagedFlagOption
|
||||
/** An update option that specifies that Ivy should validate configurations.*/
|
||||
final val Validate = new ManagedFlagOption
|
||||
/** An update option that puts Ivy into a quieter logging mode.*/
|
||||
final val QuietUpdate = new ManagedFlagOption
|
||||
/** An update option that adds the scala-tools.org releases repository to the set of resolvers, unless
|
||||
* no inline repositories are present and an ivysettings.xml file is present.*/
|
||||
final val AddScalaToolsReleases = new ManagedFlagOption
|
||||
/** An update option that specifies that an error should be generated if no inline dependencies, resolvers,
|
||||
* XML file, or Ivy or Maven configuration files are present.*/
|
||||
final val ErrorIfNoConfiguration = new ManagedFlagOption
|
||||
/** An update option that explicitly specifies the dependency manager to use. This can be used to
|
||||
* override the default precendence. */
|
||||
final case class LibraryManager(m: Manager) extends ManagedOption
|
||||
/** An update option that overrides the default Ivy cache location. */
|
||||
final case class CacheDirectory(dir: Path) extends ManagedOption
|
||||
final case class CheckScalaVersion(configs: Iterable[Configuration], checkExplicit: Boolean, filterImplicit: Boolean) extends ManagedOption
|
||||
|
||||
private def withConfigurations(outputPattern: String, managedDependencyPath: Path, options: Seq[ManagedOption])
|
||||
(doWith: (IvyConfiguration, UpdateConfiguration) => Option[String]) =
|
||||
{
|
||||
var synchronize = false
|
||||
var validate = false
|
||||
var quiet = false
|
||||
var addScalaTools = false
|
||||
var errorIfNoConfiguration = false
|
||||
var manager: Manager = new AutoDetectManager(projectID)
|
||||
var cacheDirectory: Option[Path] = None
|
||||
var checkScalaVersion: Option[IvyScala] = None
|
||||
for(option <- options)
|
||||
{
|
||||
option match
|
||||
{
|
||||
case Synchronize => synchronize = true
|
||||
case Validate => validate = true
|
||||
case LibraryManager(m) => manager = m
|
||||
case QuietUpdate => quiet = true
|
||||
case AddScalaToolsReleases => addScalaTools = true
|
||||
case ErrorIfNoConfiguration => errorIfNoConfiguration = true
|
||||
case CacheDirectory(dir) => cacheDirectory = Some(dir)
|
||||
case CheckScalaVersion(configs, checkExplicit, filterImplicit) =>
|
||||
checkScalaVersion = getScalaVersion.map(version => new IvyScala(version, configs, checkExplicit, filterImplicit))
|
||||
case _ => log.warn("Ignored unknown managed option " + option)
|
||||
}
|
||||
}
|
||||
val ivyPaths = new IvyPaths(info.projectPath, managedDependencyPath, cacheDirectory)
|
||||
val ivyFlags = new IvyFlags(validate, addScalaTools, errorIfNoConfiguration)
|
||||
val ivyConfiguration = new IvyConfiguration(ivyPaths, manager, ivyFlags, checkScalaVersion, log)
|
||||
val updateConfiguration = new UpdateConfiguration(outputPattern, synchronize, quiet)
|
||||
doWith(ivyConfiguration, updateConfiguration)
|
||||
}
|
||||
private def getScalaVersion =
|
||||
{
|
||||
val v = scalaVersion.value
|
||||
if(v.isEmpty) None
|
||||
else Some(v)
|
||||
}
|
||||
private def withIvyTask(doTask: => Option[String]) =
|
||||
task
|
||||
{
|
||||
try { doTask }
|
||||
catch
|
||||
{
|
||||
case e: NoClassDefFoundError =>
|
||||
log.trace(e)
|
||||
Some("Apache Ivy is required for dependency management (" + e.toString + ")")
|
||||
}
|
||||
}
|
||||
def updateTask(outputPattern: String, managedDependencyPath: Path, options: ManagedOption*): Task =
|
||||
updateTask(outputPattern, managedDependencyPath, options)
|
||||
def updateTask(outputPattern: String, managedDependencyPath: Path, options: => Seq[ManagedOption]) =
|
||||
withIvyTask(withConfigurations(outputPattern, managedDependencyPath, options)(ManageDependencies.update))
|
||||
|
||||
def publishTask(publishConfiguration: => PublishConfiguration, options: => Seq[ManagedOption]) =
|
||||
withIvyTask
|
||||
{
|
||||
val publishConfig = publishConfiguration
|
||||
import publishConfig._
|
||||
withConfigurations("", managedDependencyPath, options) { (ivyConf, ignore) =>
|
||||
val delivered = if(publishIvy) Some(deliveredPattern) else None
|
||||
ManageDependencies.publish(ivyConf, resolverName, srcArtifactPatterns, delivered, configurations) }
|
||||
}
|
||||
def deliverTask(deliverConfiguration: => PublishConfiguration, options: => Seq[ManagedOption]) =
|
||||
withIvyTask
|
||||
{
|
||||
val deliverConfig = deliverConfiguration
|
||||
import deliverConfig._
|
||||
withConfigurations("", managedDependencyPath, options) { (ivyConf, updateConf) =>
|
||||
ManageDependencies.deliver(ivyConf, updateConf, status, deliveredPattern, extraDependencies, configurations)
|
||||
}
|
||||
}
|
||||
def makePomTask(output: => Path, extraDependencies: => Iterable[ModuleID], configurations: => Option[Iterable[Configuration]], options: => Seq[ManagedOption]) =
|
||||
withIvyTask(withConfigurations("", managedDependencyPath, options) { (ivyConf, ignore) =>
|
||||
ManageDependencies.makePom(ivyConf, extraDependencies, configurations, output.asFile) })
|
||||
|
||||
def cleanCacheTask(managedDependencyPath: Path, options: => Seq[ManagedOption]) =
|
||||
withIvyTask(withConfigurations("", managedDependencyPath, options) { (ivyConf, ignore) => ManageDependencies.cleanCache(ivyConf) })
|
||||
|
||||
def cleanLibTask(managedDependencyPath: Path) = task { FileUtilities.clean(managedDependencyPath.get, log) }
|
||||
|
||||
/** This is the public ID of the project (used for publishing, for example) */
|
||||
def moduleID: String = normalizedName + appendable(crossScalaVersionString)
|
||||
/** This is the full public ID of the project (used for publishing, for example) */
|
||||
def projectID: ModuleID = ModuleID(organization, moduleID, version.toString)
|
||||
|
||||
/** This is the default name for artifacts (such as jars) without any version string.*/
|
||||
def artifactID = moduleID
|
||||
/** This is the default name for artifacts (such as jars) including the version string.*/
|
||||
def artifactBaseName = artifactID + "-" + version.toString
|
||||
def artifacts: Iterable[Artifact]
|
||||
|
||||
def managedDependencyPath: Path
|
||||
/** The managed classpath for the given configuration. This can be overridden to add jars from other configurations
|
||||
* so that the Ivy 'extends' mechanism is not required. That way, the jars are only copied to one configuration.*/
|
||||
def managedClasspath(config: Configuration): PathFinder = configurationClasspath(config)
|
||||
/** All dependencies in the given configuration. */
|
||||
final def configurationClasspath(config: Configuration): PathFinder = descendents(configurationPath(config), "*.jar")
|
||||
/** The base path to which dependencies in configuration 'config' are downloaded.*/
|
||||
def configurationPath(config: Configuration): Path = managedDependencyPath / config.toString
|
||||
|
||||
import StringUtilities.nonEmpty
|
||||
implicit def toGroupID(groupID: String): GroupID =
|
||||
{
|
||||
nonEmpty(groupID, "Group ID")
|
||||
new GroupID(groupID, ScalaVersion.currentString)
|
||||
}
|
||||
implicit def toRepositoryName(name: String): RepositoryName =
|
||||
{
|
||||
nonEmpty(name, "Repository name")
|
||||
new RepositoryName(name)
|
||||
}
|
||||
implicit def moduleIDConfigurable(m: ModuleID): ModuleIDConfigurable =
|
||||
{
|
||||
require(m.configurations.isEmpty, "Configurations already specified for module " + m)
|
||||
new ModuleIDConfigurable(m)
|
||||
}
|
||||
|
||||
/** Creates a new configuration with the given name.*/
|
||||
def config(name: String) = new Configuration(name)
|
||||
}
|
||||
/** This class groups required configuration for the deliver and publish tasks. */
|
||||
trait PublishConfiguration extends NotNull
|
||||
{
|
||||
/** The name of the resolver to which publishing should be done.*/
|
||||
def resolverName: String
|
||||
/** The Ivy pattern used to determine the delivered Ivy file location. An example is
|
||||
* (outputPath / "[artifact]-[revision].[ext]").relativePath. */
|
||||
def deliveredPattern: String
|
||||
/** Ivy patterns used to find artifacts for publishing. An example pattern is
|
||||
* (outputPath / "[artifact]-[revision].[ext]").relativePath */
|
||||
def srcArtifactPatterns: Iterable[String]
|
||||
/** Additional dependencies to include for delivering/publishing only. These are typically dependencies on
|
||||
* subprojects. */
|
||||
def extraDependencies: Iterable[ModuleID]
|
||||
/** The status to use when delivering or publishing. This might be "release" or "integration" or another valid Ivy status. */
|
||||
def status: String
|
||||
/** The configurations to include in the publish/deliver action: specify none for all configurations. */
|
||||
def configurations: Option[Iterable[Configuration]]
|
||||
/** True if the Ivy file should be published. */
|
||||
def publishIvy: Boolean
|
||||
}
|
||||
object ManagedStyle extends Enumeration
|
||||
{
|
||||
val Maven, Ivy = Value
|
||||
}
|
||||
import ManagedStyle.{Ivy, Maven, Value => ManagedType}
|
||||
trait BasicManagedProject extends ManagedProject with ReflectiveManagedProject with BasicDependencyPaths
|
||||
{
|
||||
import BasicManagedProject._
|
||||
/** The dependency manager that represents inline declarations. The default manager packages the information
|
||||
* from 'ivyXML', 'projectID', 'repositories', and 'libraryDependencies' and does not typically need to be
|
||||
* be overridden. */
|
||||
def manager = new SimpleManager(ivyXML, true, projectID, repositories, ivyConfigurations, defaultConfiguration, artifacts, libraryDependencies.toList: _*)
|
||||
|
||||
/** The pattern for Ivy to use when retrieving dependencies into the local project. Classpath management
|
||||
* depends on the first directory being [conf] and the extension being [ext].*/
|
||||
def outputPattern = "[conf]/[artifact](-[revision]).[ext]"
|
||||
/** Override this to specify the publications, configurations, and/or dependencies sections of an Ivy file.
|
||||
* See http://code.google.com/p/simple-build-tool/wiki/LibraryManagement for details.*/
|
||||
def ivyXML: scala.xml.NodeSeq = scala.xml.NodeSeq.Empty
|
||||
/** The base options passed to the 'update' action. */
|
||||
def baseUpdateOptions = checkScalaVersion :: Validate :: Synchronize :: QuietUpdate :: AddScalaToolsReleases :: Nil
|
||||
override def ivyConfigurations: Iterable[Configuration] =
|
||||
{
|
||||
val reflective = super.ivyConfigurations
|
||||
if(useDefaultConfigurations)
|
||||
{
|
||||
if(reflective.isEmpty && !useIntegrationTestConfiguration)
|
||||
Nil
|
||||
else
|
||||
{
|
||||
val base = Configurations.defaultMavenConfigurations ++ reflective
|
||||
val allConfigurations =
|
||||
if(useIntegrationTestConfiguration)
|
||||
base ++ List(Configurations.IntegrationTest)
|
||||
else
|
||||
base
|
||||
Configurations.removeDuplicates(allConfigurations)
|
||||
}
|
||||
}
|
||||
else
|
||||
reflective
|
||||
}
|
||||
def useIntegrationTestConfiguration = false
|
||||
def defaultConfiguration: Option[Configuration] = Some(Configurations.DefaultConfiguration(useDefaultConfigurations))
|
||||
def useMavenConfigurations = true // TBD: set to true and deprecate
|
||||
def useDefaultConfigurations = useMavenConfigurations
|
||||
def managedStyle: ManagedType = Maven
|
||||
protected implicit final val defaultPatterns: RepositoryHelpers.Patterns =
|
||||
{
|
||||
managedStyle match
|
||||
{
|
||||
case Maven => Resolver.mavenStylePatterns
|
||||
case Ivy => Resolver.ivyStylePatterns
|
||||
}
|
||||
}
|
||||
/** The options provided to the 'update' action. This is by default the options in 'baseUpdateOptions'.
|
||||
* If 'manager' has any dependencies, resolvers, or inline Ivy XML (which by default happens when inline
|
||||
* dependency management is used), it is passed as the dependency manager.*/
|
||||
def updateOptions: Seq[ManagedOption] =
|
||||
{
|
||||
val m = manager
|
||||
if(m.dependencies.isEmpty && m.resolvers.isEmpty && ivyXML.isEmpty && m.artifacts.isEmpty && m.configurations.isEmpty)
|
||||
baseUpdateOptions
|
||||
else
|
||||
LibraryManager(m) :: baseUpdateOptions
|
||||
}
|
||||
def deliverOptions: Seq[ManagedOption] = updateOptions.filter { case _: CheckScalaVersion => false; case _ => true }
|
||||
def publishOptions: Seq[ManagedOption] = deliverOptions
|
||||
/** True if the 'provided' configuration should be included on the 'compile' classpath. The default value is true.*/
|
||||
def includeProvidedWithCompile = true
|
||||
/** True if the default implicit extensions should be used when determining classpaths. The default value is true. */
|
||||
def defaultConfigurationExtensions = true
|
||||
/** If true, verify that explicit dependencies on Scala libraries use the same version as scala.version. */
|
||||
def checkExplicitScalaDependencies = true
|
||||
/** If true, filter dependencies on scala-library and scala-compiler. This is true by default to avoid conflicts with
|
||||
* the jars provided by sbt. You can set this to false to download these jars. Overriding checkScalaInConfigurations might
|
||||
* be more appropriate, however.*/
|
||||
def filterScalaJars = true
|
||||
/** The configurations to check/filter.*/
|
||||
def checkScalaInConfigurations: Iterable[Configuration] =
|
||||
{
|
||||
val all = ivyConfigurations
|
||||
if(all.isEmpty)
|
||||
Configurations.defaultMavenConfigurations
|
||||
else
|
||||
all
|
||||
}
|
||||
def checkScalaVersion = CheckScalaVersion(checkScalaInConfigurations, checkExplicitScalaDependencies, filterScalaJars)
|
||||
def defaultPublishRepository: Option[Resolver] =
|
||||
{
|
||||
reflectiveRepositories.get("publish-to") orElse
|
||||
info.parent.flatMap
|
||||
{
|
||||
case managed: BasicManagedProject => managed.defaultPublishRepository
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
/** Includes the Provided configuration on the Compile classpath, the Compile configuration on the Runtime classpath,
|
||||
* and Compile and Runtime on the Test classpath. Including Provided can be disabled by setting
|
||||
* includeProvidedWithCompile to false. Including Compile and Runtime can be disabled by setting
|
||||
* defaultConfigurationExtensions to false.*/
|
||||
override def managedClasspath(config: Configuration) =
|
||||
{
|
||||
import Configurations.{Compile, CompilerPlugin, Default, Provided, Runtime, Test}
|
||||
val baseClasspath = configurationClasspath(config)
|
||||
config match
|
||||
{
|
||||
case Compile =>
|
||||
val baseCompileClasspath = baseClasspath +++ managedClasspath(Default)
|
||||
if(includeProvidedWithCompile)
|
||||
baseCompileClasspath +++ managedClasspath(Provided)
|
||||
else
|
||||
baseCompileClasspath
|
||||
case Runtime if defaultConfigurationExtensions => baseClasspath +++ managedClasspath(Compile)
|
||||
case Test if defaultConfigurationExtensions => baseClasspath +++ managedClasspath(Runtime)
|
||||
case _ => baseClasspath
|
||||
}
|
||||
}
|
||||
|
||||
protected def updateAction = updateTask(outputPattern, managedDependencyPath, updateOptions) describedAs UpdateDescription
|
||||
protected def cleanLibAction = cleanLibTask(managedDependencyPath) describedAs CleanLibDescription
|
||||
protected def cleanCacheAction = cleanCacheTask(managedDependencyPath, updateOptions) describedAs CleanCacheDescription
|
||||
|
||||
protected def deliverProjectDependencies: Iterable[ModuleID] =
|
||||
{
|
||||
val interDependencies = new scala.collection.mutable.ListBuffer[ModuleID]
|
||||
dependencies.foreach(dep => dep match { case mp: ManagedProject => interDependencies += mp.projectID; case _ => () })
|
||||
if(filterScalaJars)
|
||||
interDependencies ++= deliverScalaDependencies
|
||||
interDependencies.readOnly
|
||||
}
|
||||
protected def deliverScalaDependencies: Iterable[ModuleID] = Nil
|
||||
protected def makePomAction = makePomTask(pomPath, deliverProjectDependencies, None, updateOptions)
|
||||
protected def deliverLocalAction = deliverTask(publishLocalConfiguration, deliverOptions)
|
||||
protected def publishLocalAction =
|
||||
{
|
||||
val dependencies = deliverLocal :: publishPomDepends
|
||||
publishTask(publishLocalConfiguration, publishOptions) dependsOn(dependencies : _*)
|
||||
}
|
||||
protected def publishLocalConfiguration = new DefaultPublishConfiguration("local", "release", true)
|
||||
protected def deliverAction = deliverTask(publishConfiguration, deliverOptions)
|
||||
protected def publishAction =
|
||||
{
|
||||
val dependencies = deliver :: publishPomDepends
|
||||
publishTask(publishConfiguration, publishOptions) dependsOn(dependencies : _*)
|
||||
}
|
||||
private def publishPomDepends = if(managedStyle == Maven) makePom :: Nil else Nil
|
||||
protected def publishConfiguration =
|
||||
{
|
||||
val repository = defaultPublishRepository.getOrElse(error("Repository to publish to not specified."))
|
||||
val publishIvy = managedStyle != Maven
|
||||
new DefaultPublishConfiguration(repository, "release", publishIvy)
|
||||
}
|
||||
protected class DefaultPublishConfiguration(val resolverName: String, val status: String, val publishIvy: Boolean) extends PublishConfiguration
|
||||
{
|
||||
def this(resolver: Resolver, status: String, publishIvy: Boolean) = this(resolver.name, status, publishIvy)
|
||||
def this(resolverName: String, status: String) = this(resolverName, status, true)
|
||||
def this(resolver: Resolver, status: String) = this(resolver.name, status)
|
||||
|
||||
protected def deliveredPathPattern = outputPath / "[artifact]-[revision].[ext]"
|
||||
def deliveredPattern = deliveredPathPattern.relativePath
|
||||
def srcArtifactPatterns: Iterable[String] =
|
||||
{
|
||||
val pathPatterns =
|
||||
(outputPath / "[artifact]-[revision]-[type].[ext]") ::
|
||||
(outputPath / "[artifact]-[revision].[ext]") ::
|
||||
Nil
|
||||
pathPatterns.map(_.relativePath)
|
||||
}
|
||||
def extraDependencies: Iterable[ModuleID] = Nil//deliverProjectDependencies
|
||||
/** The configurations to include in the publish/deliver action: specify none for all public configurations. */
|
||||
def configurations: Option[Iterable[Configuration]] = None
|
||||
}
|
||||
|
||||
lazy val update = updateAction
|
||||
lazy val makePom = makePomAction
|
||||
lazy val deliverLocal = deliverLocalAction
|
||||
lazy val publishLocal = publishLocalAction
|
||||
lazy val deliver = deliverAction
|
||||
lazy val publish = publishAction
|
||||
lazy val cleanLib = cleanLibAction
|
||||
lazy val cleanCache = cleanCacheAction
|
||||
}
|
||||
|
||||
object BasicManagedProject
|
||||
{
|
||||
val UpdateDescription =
|
||||
"Resolves and retrieves automatically managed dependencies."
|
||||
val CleanLibDescription =
|
||||
"Deletes the managed library directory."
|
||||
val CleanCacheDescription =
|
||||
"Deletes the cache of artifacts downloaded for automatically managed dependencies."
|
||||
}
|
||||
|
||||
trait BasicDependencyPaths extends ManagedProject
|
||||
{
|
||||
import BasicDependencyPaths._
|
||||
def dependencyDirectoryName = DefaultDependencyDirectoryName
|
||||
def managedDirectoryName = DefaultManagedDirectoryName
|
||||
def pomName = artifactBaseName + PomExtension
|
||||
def dependencyPath = path(dependencyDirectoryName)
|
||||
def managedDependencyPath = crossPath(managedDependencyRootPath)
|
||||
def managedDependencyRootPath: Path = managedDirectoryName
|
||||
def pomPath = outputPath / pomName
|
||||
}
|
||||
object BasicDependencyPaths
|
||||
{
|
||||
val DefaultManagedDirectoryName = "lib_managed"
|
||||
val DefaultManagedSourceDirectoryName = "src_managed"
|
||||
val DefaultDependencyDirectoryName = "lib"
|
||||
val PomExtension = ".pom"
|
||||
}
|
||||
|
||||
object StringUtilities
|
||||
{
|
||||
def normalize(s: String) = s.toLowerCase.replaceAll("""\s+""", "-")
|
||||
def nonEmpty(s: String, label: String)
|
||||
{
|
||||
require(s.trim.length > 0, label + " cannot be empty.")
|
||||
}
|
||||
def appendable(s: String) = if(s.isEmpty) "" else "_" + s
|
||||
}
|
||||
final class GroupID private[sbt] (groupID: String, scalaVersion: String) extends NotNull
|
||||
{
|
||||
def % (artifactID: String) = groupArtifact(artifactID)
|
||||
def %% (artifactID: String) =
|
||||
{
|
||||
require(!scalaVersion.isEmpty, "Cannot use %% when the sbt launcher is not used.")
|
||||
groupArtifact(artifactID + appendable(scalaVersion))
|
||||
}
|
||||
private def groupArtifact(artifactID: String) =
|
||||
{
|
||||
nonEmpty(artifactID, "Artifact ID")
|
||||
new GroupArtifactID(groupID, artifactID)
|
||||
}
|
||||
}
|
||||
final class GroupArtifactID private[sbt] (groupID: String, artifactID: String) extends NotNull
|
||||
{
|
||||
def % (revision: String): ModuleID =
|
||||
{
|
||||
nonEmpty(revision, "Revision")
|
||||
ModuleID(groupID, artifactID, revision, None)
|
||||
}
|
||||
}
|
||||
final class ModuleIDConfigurable private[sbt] (moduleID: ModuleID) extends NotNull
|
||||
{
|
||||
def % (configurations: String): ModuleID =
|
||||
{
|
||||
nonEmpty(configurations, "Configurations")
|
||||
import moduleID._
|
||||
ModuleID(organization, name, revision, Some(configurations))
|
||||
}
|
||||
}
|
||||
final class RepositoryName private[sbt] (name: String) extends NotNull
|
||||
{
|
||||
def at (location: String) =
|
||||
{
|
||||
nonEmpty(location, "Repository location")
|
||||
new MavenRepository(name, location)
|
||||
}
|
||||
}
|
||||
|
||||
import scala.collection.{Map, mutable}
|
||||
/** A Project that determines its tasks by reflectively finding all vals with a type
|
||||
* that conforms to Task.*/
|
||||
trait ReflectiveTasks extends Project
|
||||
{
|
||||
def tasks: Map[String, Task] = reflectiveTaskMappings
|
||||
def reflectiveTaskMappings : Map[String, Task] = Reflective.reflectiveMappings[Task](this)
|
||||
}
|
||||
/** A Project that determines its method tasks by reflectively finding all vals with a type
|
||||
* that conforms to MethodTask.*/
|
||||
trait ReflectiveMethods extends Project
|
||||
{
|
||||
def methods: Map[String, MethodTask] = reflectiveMethodMappings
|
||||
def reflectiveMethodMappings : Map[String, MethodTask] = Reflective.reflectiveMappings[MethodTask](this)
|
||||
}
|
||||
/** A Project that determines its dependencies on other projects by reflectively
|
||||
* finding all vals with a type that conforms to Project.*/
|
||||
trait ReflectiveModules extends Project
|
||||
{
|
||||
override def subProjects: Map[String, Project] = reflectiveModuleMappings
|
||||
def reflectiveModuleMappings : Map[String, Project] = Reflective.reflectiveMappings[Project](this)
|
||||
}
|
||||
/** A Project that determines its dependencies on other projects by reflectively
|
||||
* finding all vals with a type that conforms to Project and determines its tasks
|
||||
* by reflectively finding all vals with a type that conforms to Task.*/
|
||||
trait ReflectiveProject extends ReflectiveModules with ReflectiveTasks with ReflectiveMethods
|
||||
|
||||
/** This Project subclass is used to contain other projects as dependencies.*/
|
||||
class ParentProject(val info: ProjectInfo) extends BasicDependencyProject
|
||||
{
|
||||
def dependencies = info.dependencies ++ subProjects.values.toList
|
||||
/** The directories to which a project writes are listed here and is used
|
||||
* to check a project and its dependencies for collisions.*/
|
||||
override def outputDirectories = managedDependencyPath :: outputPath :: Nil
|
||||
def fullUnmanagedClasspath(config: Configuration) = unmanagedClasspath
|
||||
}
|
||||
|
||||
object Reflective
|
||||
{
|
||||
def reflectiveMappings[T](obj: AnyRef)(implicit m: scala.reflect.Manifest[T]): Map[String, T] =
|
||||
{
|
||||
val mappings = new mutable.OpenHashMap[String, T]
|
||||
for ((name, value) <- ReflectUtilities.allVals[T](obj))
|
||||
mappings(ReflectUtilities.transformCamelCase(name, '-')) = value
|
||||
mappings
|
||||
}
|
||||
}
|
||||
|
||||
/** A Project that determines its library dependencies by reflectively finding all vals with a type
|
||||
* that conforms to ModuleID.*/
|
||||
trait ReflectiveLibraryDependencies extends ManagedProject
|
||||
{
|
||||
def excludeIDs: Iterable[ModuleID] = projectID :: Nil
|
||||
def libraryDependencies: Set[ModuleID] = reflectiveLibraryDependencies
|
||||
def reflectiveLibraryDependencies : Set[ModuleID] = Set[ModuleID](Reflective.reflectiveMappings[ModuleID](this).values.toList: _*) -- excludeIDs
|
||||
}
|
||||
|
||||
trait ReflectiveConfigurations extends Project
|
||||
{
|
||||
def ivyConfigurations: Iterable[Configuration] = reflectiveIvyConfigurations
|
||||
def reflectiveIvyConfigurations: Set[Configuration] = Configurations.removeDuplicates(Reflective.reflectiveMappings[Configuration](this).values.toList)
|
||||
}
|
||||
trait ReflectiveArtifacts extends ManagedProject
|
||||
{
|
||||
def managedStyle: ManagedType
|
||||
def artifacts: Set[Artifact] =
|
||||
{
|
||||
val reflective = reflectiveArtifacts
|
||||
managedStyle match
|
||||
{
|
||||
case Maven =>reflective ++ List(Artifact(artifactID, "pom", "pom"))
|
||||
case Ivy => reflective
|
||||
}
|
||||
}
|
||||
def reflectiveArtifacts: Set[Artifact] = Set(Reflective.reflectiveMappings[Artifact](this).values.toList: _*)
|
||||
}
|
||||
/** A Project that determines its library dependencies by reflectively finding all vals with a type
|
||||
* that conforms to ModuleID.*/
|
||||
trait ReflectiveRepositories extends Project
|
||||
{
|
||||
def repositories: Set[Resolver] =
|
||||
{
|
||||
val reflective = Set[Resolver](reflectiveRepositories.values.toList: _*)
|
||||
info.parent match
|
||||
{
|
||||
case Some(p: ReflectiveRepositories) => p.repositories ++ reflective
|
||||
case None => reflective
|
||||
}
|
||||
}
|
||||
def reflectiveRepositories: Map[String, Resolver] = Reflective.reflectiveMappings[Resolver](this)
|
||||
}
|
||||
|
||||
trait ReflectiveManagedProject extends ReflectiveProject with ReflectiveArtifacts with ReflectiveRepositories with ReflectiveLibraryDependencies with ReflectiveConfigurations
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah, David MacIver
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import BasicProjectPaths._
|
||||
|
||||
sealed abstract class InternalProject extends Project
|
||||
{
|
||||
override final def historyPath = None
|
||||
override def tasks: Map[String, Task] = Map.empty
|
||||
override final protected def disableCrossPaths = false
|
||||
override final def shouldCheckOutputDirectories = false
|
||||
}
|
||||
private sealed abstract class BasicBuilderProject extends InternalProject with SimpleScalaProject
|
||||
{
|
||||
def sourceFilter = "*.scala" | "*.java"
|
||||
def jarFilter: NameFilter = "*.jar"
|
||||
def compilePath = outputPath / DefaultMainCompileDirectoryName
|
||||
def mainResourcesPath = path(DefaultResourcesDirectoryName)
|
||||
def dependencyPath = path(DefaultDependencyDirectoryName)
|
||||
def libraries = descendents(dependencyPath, jarFilter)
|
||||
override final def dependencies = Nil
|
||||
|
||||
protected final def logInfo(messages: String*): Unit = atInfo { messages.foreach(message => log.info(message)) }
|
||||
protected final def atInfo(action: => Unit)
|
||||
{
|
||||
val oldLevel = log.getLevel
|
||||
log.setLevel(Level.Info)
|
||||
action
|
||||
log.setLevel(oldLevel)
|
||||
}
|
||||
|
||||
def projectClasspath = compilePath +++ libraries +++ sbtJarPath
|
||||
def sbtJarPath = Path.lazyPathFinder { Path.fromFile(FileUtilities.sbtJar) :: Nil }
|
||||
|
||||
abstract class BuilderCompileConfiguration extends AbstractCompileConfiguration
|
||||
{
|
||||
def projectPath = info.projectPath
|
||||
def log = BasicBuilderProject.this.log
|
||||
def options = (Deprecation :: Unchecked :: Nil).map(_.asString)
|
||||
def javaOptions = Nil
|
||||
def maxErrors = ScalaProject.DefaultMaximumCompileErrors
|
||||
def compileOrder = CompileOrder.Mixed
|
||||
}
|
||||
def definitionCompileConfiguration =
|
||||
new BuilderCompileConfiguration
|
||||
{
|
||||
def label = "builder"
|
||||
def sources = (info.projectPath * sourceFilter) +++ path(DefaultSourceDirectoryName).descendentsExcept(sourceFilter, defaultExcludes)
|
||||
def outputDirectory = compilePath
|
||||
def classpath = projectClasspath
|
||||
def analysisPath = outputPath / DefaultMainAnalysisDirectoryName
|
||||
}
|
||||
|
||||
def tpe: String
|
||||
|
||||
val definitionCompileConditional = new BuilderCompileConditional(definitionCompileConfiguration, tpe)
|
||||
final class BuilderCompileConditional(config: BuilderCompileConfiguration, tpe: String) extends AbstractCompileConditional(config)
|
||||
{
|
||||
type AnalysisType = BuilderCompileAnalysis
|
||||
override protected def constructAnalysis(analysisPath: Path, projectPath: Path, log: Logger) =
|
||||
new BuilderCompileAnalysis(analysisPath, projectPath, log)
|
||||
override protected def execute(cAnalysis: ConditionalAnalysis): Option[String] =
|
||||
{
|
||||
if(cAnalysis.dirtySources.isEmpty)
|
||||
None
|
||||
else
|
||||
{
|
||||
definitionChanged()
|
||||
logInfo(
|
||||
"Recompiling " + tpe + "...",
|
||||
"\t" + cAnalysis.toString)
|
||||
super.execute(cAnalysis)
|
||||
}
|
||||
}
|
||||
protected def analysisCallback: AnalysisCallback =
|
||||
new BasicAnalysisCallback(info.projectPath, List(Project.ProjectClassName), analysis)
|
||||
{
|
||||
def foundApplication(sourcePath: Path, className: String) {}
|
||||
def foundSubclass(sourcePath: Path, subclassName: String, superclassName: String, isModule: Boolean)
|
||||
{
|
||||
if(superclassName == Project.ProjectClassName && !isModule)
|
||||
{
|
||||
log.debug("Found " + tpe + " " + subclassName)
|
||||
analysis.addProjectDefinition(sourcePath, subclassName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
protected def definitionChanged() {}
|
||||
lazy val compile = compileTask
|
||||
def compileTask = task { definitionCompileConditional.run }
|
||||
|
||||
def projectDefinition: Either[String, Option[String]] =
|
||||
{
|
||||
definitionCompileConditional.analysis.allProjects.toList match
|
||||
{
|
||||
case Nil =>
|
||||
log.debug("No " + tpe + "s detected using default project.")
|
||||
Right(None)
|
||||
case singleDefinition :: Nil => Right(Some(singleDefinition))
|
||||
case multipleDefinitions =>Left(multipleDefinitions.mkString("Multiple " + tpe + "s detected: \n\t","\n\t","\n"))
|
||||
}
|
||||
}
|
||||
override final def methods = Map.empty
|
||||
}
|
||||
/** The project definition used to build project definitions. */
|
||||
private final class BuilderProject(val info: ProjectInfo, val pluginPath: Path, override protected val logImpl: Logger) extends BasicBuilderProject
|
||||
{
|
||||
private lazy val pluginProject =
|
||||
{
|
||||
if(pluginPath.exists)
|
||||
Some(new PluginBuilderProject(ProjectInfo(pluginPath.asFile, Nil, None)))
|
||||
else
|
||||
None
|
||||
}
|
||||
override def projectClasspath = super.projectClasspath +++ pluginProject.map(_.pluginClasspath).getOrElse(Path.emptyPathFinder)
|
||||
def tpe = "project definition"
|
||||
|
||||
override def compileTask = super.compileTask dependsOn(pluginProject.map(_.syncPlugins).toList : _*)
|
||||
|
||||
final class PluginBuilderProject(val info: ProjectInfo) extends BasicBuilderProject
|
||||
{
|
||||
override protected def logImpl = BuilderProject.this.log
|
||||
val pluginUptodate = propertyOptional[Boolean](false)
|
||||
def tpe = "plugin definition"
|
||||
def managedSourcePath = path(BasicDependencyPaths.DefaultManagedSourceDirectoryName)
|
||||
def managedDependencyPath = crossPath(BasicDependencyPaths.DefaultManagedDirectoryName)
|
||||
override protected def definitionChanged() { setUptodate(false) }
|
||||
private def setUptodate(flag: Boolean)
|
||||
{
|
||||
pluginUptodate() = flag
|
||||
saveEnvironment()
|
||||
}
|
||||
|
||||
private def pluginTask(f: => Option[String]) = task { if(!pluginUptodate.value) f else None }
|
||||
|
||||
lazy val syncPlugins = pluginTask(sync()) dependsOn(extractSources)
|
||||
lazy val extractSources = pluginTask(extract()) dependsOn(update)
|
||||
lazy val update = pluginTask(loadAndUpdate()) dependsOn(compile)
|
||||
|
||||
private def sync() = pluginCompileConditional.run orElse { setUptodate(true); None }
|
||||
private def extract() =
|
||||
{
|
||||
FileUtilities.clean(managedSourcePath, log) orElse
|
||||
Control.lazyFold(plugins.get.toList) { jar =>
|
||||
Control.thread(FileUtilities.unzip(jar, extractTo(jar), sourceFilter, log)) { extracted =>
|
||||
if(!extracted.isEmpty)
|
||||
logInfo("\tExtracted source plugin " + jar + " ...")
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
private def loadAndUpdate() =
|
||||
{
|
||||
Control.thread(projectDefinition) {
|
||||
case Some(definition) =>
|
||||
logInfo("\nUpdating plugins")
|
||||
val pluginInfo = ProjectInfo(info.projectPath.asFile, Nil, None)
|
||||
val pluginBuilder = Project.constructProject(pluginInfo, Project.getProjectClass[PluginDefinition](definition, projectClasspath))
|
||||
pluginBuilder.projectName() = "Plugin builder"
|
||||
pluginBuilder.projectVersion() = OpaqueVersion("1.0")
|
||||
val result = pluginBuilder.update.run
|
||||
if(result.isEmpty)
|
||||
{
|
||||
atInfo {
|
||||
log.success("Plugins updated successfully.")
|
||||
log.info("")
|
||||
}
|
||||
}
|
||||
result
|
||||
case None => None
|
||||
}
|
||||
}
|
||||
def extractTo(jar: Path) =
|
||||
{
|
||||
val name = jar.asFile.getName
|
||||
managedSourcePath / name.substring(0, name.length - ".jar".length)
|
||||
}
|
||||
def plugins = descendents(managedDependencyPath, jarFilter)
|
||||
def pluginClasspath = plugins +++ pluginCompileConfiguration.outputDirectory
|
||||
|
||||
lazy val pluginCompileConditional = new BuilderCompileConditional(pluginCompileConfiguration, "plugin")
|
||||
lazy val pluginCompileConfiguration =
|
||||
new BuilderCompileConfiguration
|
||||
{
|
||||
def label = "plugin builder"
|
||||
def sources = descendents(managedSourcePath, sourceFilter)
|
||||
def outputDirectory = outputPath / "plugin-classes"
|
||||
def classpath = sbtJarPath
|
||||
def analysisPath = outputPath / "plugin-analysis"
|
||||
}
|
||||
}
|
||||
}
|
||||
class PluginDefinition(val info: ProjectInfo) extends InternalProject with BasicManagedProject
|
||||
{
|
||||
override final def outputPattern = "[artifact](-[revision]).[ext]"
|
||||
override final val tasks = Map("update" -> update)
|
||||
override def projectClasspath(config: Configuration) = Path.emptyPathFinder
|
||||
override def dependencies = info.dependencies
|
||||
}
|
||||
class PluginProject(info: ProjectInfo) extends DefaultProject(info)
|
||||
{
|
||||
override def unmanagedClasspath = super.unmanagedClasspath +++ Path.lazyPathFinder(Path.fromFile(FileUtilities.sbtJar) :: Nil)
|
||||
override def packageAction = packageSrc
|
||||
override def packageSrcJar = jarPath
|
||||
override def useMavenConfigurations = true
|
||||
override def managedStyle = ManagedStyle.Maven
|
||||
}
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import java.io.File
|
||||
import java.net.{URI, URL, URLClassLoader}
|
||||
import java.util.Collections
|
||||
import scala.collection.Set
|
||||
import scala.collection.mutable.{HashSet, ListBuffer}
|
||||
|
||||
private[sbt] object ClasspathUtilities
|
||||
{
|
||||
def toClasspath(finder: PathFinder): Array[URL] = toClasspath(finder.get)
|
||||
def toClasspath(paths: Iterable[Path]): Array[URL] = paths.map(_.asURL).toSeq.toArray
|
||||
def toLoader(finder: PathFinder): ClassLoader = toLoader(finder.get)
|
||||
def toLoader(paths: Iterable[Path]): ClassLoader = new URLClassLoader(toClasspath(paths), getClass.getClassLoader)
|
||||
|
||||
def isArchive(path: Path): Boolean = isArchive(path.asFile)
|
||||
def isArchive(file: File): Boolean = isArchiveName(file.getName)
|
||||
def isArchiveName(fileName: String) = fileName.endsWith(".jar") || fileName.endsWith(".zip")
|
||||
// Partitions the given classpath into (jars, directories)
|
||||
def separate(paths: Iterable[File]): (Iterable[File], Iterable[File]) = paths.partition(isArchive)
|
||||
// Partitions the given classpath into (jars, directories)
|
||||
def separatePaths(paths: Iterable[Path]) = separate(paths.map(_.asFile.getCanonicalFile))
|
||||
private[sbt] def buildSearchPaths(classpath: Iterable[Path]): (wrap.Set[File], wrap.Set[File]) =
|
||||
{
|
||||
val (jars, dirs) = separatePaths(classpath)
|
||||
(linkedSet(jars ++ extraJars.toList), linkedSet(dirs ++ extraDirs.toList))
|
||||
}
|
||||
private[sbt] def onClasspath(classpathJars: wrap.Set[File], classpathDirectories: wrap.Set[File], file: File): Boolean =
|
||||
{
|
||||
val f = file.getCanonicalFile
|
||||
if(ClasspathUtilities.isArchive(f))
|
||||
classpathJars.contains(f)
|
||||
else
|
||||
classpathDirectories.toList.find(Path.relativize(_, f).isDefined).isDefined
|
||||
}
|
||||
|
||||
/** Returns all entries in 'classpath' that correspond to a compiler plugin.*/
|
||||
def compilerPlugins(classpath: Iterable[Path]): Iterable[File] =
|
||||
{
|
||||
val loader = new URLClassLoader(classpath.map(_.asURL).toList.toArray)
|
||||
wrap.Wrappers.toList(loader.getResources("scalac-plugin.xml")).flatMap(asFile)
|
||||
}
|
||||
/** Converts the given URL to a File. If the URL is for an entry in a jar, the File for the jar is returned. */
|
||||
private[sbt] def asFile(url: URL) =
|
||||
{
|
||||
try
|
||||
{
|
||||
url.getProtocol match
|
||||
{
|
||||
case "file" => new File(url.toURI) :: Nil
|
||||
case "jar" =>
|
||||
val path = url.getPath
|
||||
val end = path.indexOf('!')
|
||||
new File(new URI(if(end == -1) path else path.substring(0, end))) :: Nil
|
||||
case _ => Nil
|
||||
}
|
||||
}
|
||||
catch { case e: Exception => Nil }
|
||||
}
|
||||
|
||||
private lazy val (extraJars, extraDirs) =
|
||||
{
|
||||
import scala.tools.nsc.GenericRunnerCommand
|
||||
val settings = (new GenericRunnerCommand(Nil, message => error(message))).settings
|
||||
val bootPaths = FileUtilities.pathSplit(settings.bootclasspath.value).map(p => new File(p)).toList
|
||||
val (bootJars, bootDirs) = separate(bootPaths)
|
||||
val extJars =
|
||||
{
|
||||
val buffer = new ListBuffer[File]
|
||||
def findJars(dir: File)
|
||||
{
|
||||
buffer ++= dir.listFiles(new SimpleFileFilter(isArchive))
|
||||
for(dir <- dir.listFiles(DirectoryFilter))
|
||||
findJars(dir)
|
||||
}
|
||||
for(path <- FileUtilities.pathSplit(settings.extdirs.value); val dir = new File(path) if dir.isDirectory)
|
||||
findJars(dir)
|
||||
buffer.readOnly.map(_.getCanonicalFile)
|
||||
}
|
||||
(linkedSet(extJars ++ bootJars), linkedSet(bootDirs))
|
||||
}
|
||||
private def linkedSet[T](s: Iterable[T]): wrap.Set[T] =
|
||||
{
|
||||
val set = new wrap.MutableSetWrapper(new java.util.LinkedHashSet[T])
|
||||
set ++= s
|
||||
set.readOnly
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class LoaderBase(urls: Array[URL], parent: ClassLoader) extends URLClassLoader(urls, parent) with NotNull
|
||||
{
|
||||
require(parent != null) // included because a null parent is legitimate in Java
|
||||
@throws(classOf[ClassNotFoundException])
|
||||
override final def loadClass(className: String, resolve: Boolean): Class[_] =
|
||||
{
|
||||
val loaded = findLoadedClass(className)
|
||||
val found =
|
||||
if(loaded == null)
|
||||
doLoadClass(className)
|
||||
else
|
||||
loaded
|
||||
|
||||
if(resolve)
|
||||
resolveClass(found)
|
||||
found
|
||||
}
|
||||
protected def doLoadClass(className: String): Class[_]
|
||||
protected final def selfLoadClass(className: String): Class[_] = super.loadClass(className, false)
|
||||
}
|
||||
private class IntermediateLoader(urls: Array[URL], parent: ClassLoader) extends LoaderBase(urls, parent) with NotNull
|
||||
{
|
||||
def doLoadClass(className: String): Class[_] =
|
||||
{
|
||||
// if this loader is asked to load an sbt class, it must be because the project we are building is sbt itself,
|
||||
// so we want to load the version of classes on the project classpath, not the parent
|
||||
if(className.startsWith(Loaders.SbtPackage))
|
||||
findClass(className)
|
||||
else
|
||||
selfLoadClass(className)
|
||||
}
|
||||
}
|
||||
/** Delegates class loading to `parent` for all classes included by `filter`. An attempt to load classes excluded by `filter`
|
||||
* results in a `ClassNotFoundException`.*/
|
||||
private class FilteredLoader(parent: ClassLoader, filter: ClassFilter) extends ClassLoader(parent) with NotNull
|
||||
{
|
||||
require(parent != null) // included because a null parent is legitimate in Java
|
||||
def this(parent: ClassLoader, excludePackages: Iterable[String]) = this(parent, new ExcludePackagesFilter(excludePackages))
|
||||
|
||||
@throws(classOf[ClassNotFoundException])
|
||||
override final def loadClass(className: String, resolve: Boolean): Class[_] =
|
||||
{
|
||||
if(filter.include(className))
|
||||
super.loadClass(className, resolve)
|
||||
else
|
||||
throw new ClassNotFoundException(className)
|
||||
}
|
||||
}
|
||||
private class SelectiveLoader(urls: Array[URL], parent: ClassLoader, filter: ClassFilter) extends URLClassLoader(urls, parent) with NotNull
|
||||
{
|
||||
require(parent != null) // included because a null parent is legitimate in Java
|
||||
def this(urls: Array[URL], parent: ClassLoader, includePackages: Iterable[String]) = this(urls, parent, new IncludePackagesFilter(includePackages))
|
||||
|
||||
@throws(classOf[ClassNotFoundException])
|
||||
override final def loadClass(className: String, resolve: Boolean): Class[_] =
|
||||
{
|
||||
if(filter.include(className))
|
||||
super.loadClass(className, resolve)
|
||||
else
|
||||
{
|
||||
val loaded = parent.loadClass(className)
|
||||
if(resolve)
|
||||
resolveClass(loaded)
|
||||
loaded
|
||||
}
|
||||
}
|
||||
}
|
||||
private trait ClassFilter
|
||||
{
|
||||
def include(className: String): Boolean
|
||||
}
|
||||
private abstract class PackageFilter(packages: Iterable[String]) extends ClassFilter
|
||||
{
|
||||
require(packages.forall(_.endsWith(".")))
|
||||
protected final def matches(className: String): Boolean = packages.exists(className.startsWith)
|
||||
}
|
||||
private class ExcludePackagesFilter(exclude: Iterable[String]) extends PackageFilter(exclude)
|
||||
{
|
||||
def include(className: String): Boolean = !matches(className)
|
||||
}
|
||||
private class IncludePackagesFilter(include: Iterable[String]) extends PackageFilter(include)
|
||||
{
|
||||
def include(className: String): Boolean = matches(className)
|
||||
}
|
||||
|
||||
private class LazyFrameworkLoader(runnerClassName: String, urls: Array[URL], parent: ClassLoader, grandparent: ClassLoader)
|
||||
extends LoaderBase(urls, parent) with NotNull
|
||||
{
|
||||
def doLoadClass(className: String): Class[_] =
|
||||
{
|
||||
if(Loaders.isNestedOrSelf(className, runnerClassName))
|
||||
findClass(className)
|
||||
else if(Loaders.isSbtClass(className)) // we circumvent the parent loader because we know that we want the
|
||||
grandparent.loadClass(className) // version of sbt that is currently the builder (not the project being built)
|
||||
else
|
||||
parent.loadClass(className)
|
||||
}
|
||||
}
|
||||
private object Loaders
|
||||
{
|
||||
val SbtPackage = "sbt."
|
||||
def isNestedOrSelf(className: String, checkAgainst: String) =
|
||||
className == checkAgainst || className.startsWith(checkAgainst + "$")
|
||||
def isSbtClass(className: String) = className.startsWith(Loaders.SbtPackage)
|
||||
}
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
object CompileOrder extends Enumeration
|
||||
{
|
||||
val Mixed, JavaThenScala, ScalaThenJava = Value
|
||||
}
|
||||
sealed abstract class CompilerCore
|
||||
{
|
||||
val ClasspathOptionString = "-classpath"
|
||||
val OutputOptionString = "-d"
|
||||
|
||||
// Returns false if there were errors, true if there were not.
|
||||
protected def process(args: List[String], log: Logger): Boolean
|
||||
// Returns false if there were errors, true if there were not.
|
||||
protected def processJava(args: List[String], log: Logger): Boolean = true
|
||||
def actionStartMessage(label: String): String
|
||||
def actionNothingToDoMessage: String
|
||||
def actionSuccessfulMessage: String
|
||||
def actionUnsuccessfulMessage: String
|
||||
|
||||
final def apply(label: String, sources: Iterable[Path], classpathString: String, outputDirectory: Path, options: Seq[String], log: Logger): Option[String] =
|
||||
apply(label, sources, classpathString, outputDirectory, options, Nil, CompileOrder.Mixed, log)
|
||||
final def apply(label: String, sources: Iterable[Path], classpathString: String, outputDirectory: Path, options: Seq[String], javaOptions: Seq[String], order: CompileOrder.Value, log: Logger): Option[String] =
|
||||
{
|
||||
log.info(actionStartMessage(label))
|
||||
val classpathOption: List[String] =
|
||||
if(classpathString.isEmpty)
|
||||
Nil
|
||||
else
|
||||
List(ClasspathOptionString, classpathString)
|
||||
val outputDir = outputDirectory.asFile
|
||||
FileUtilities.createDirectory(outputDir, log) orElse
|
||||
{
|
||||
val classpathAndOut: List[String] = OutputOptionString :: outputDir.getAbsolutePath :: classpathOption
|
||||
|
||||
Control.trapUnit("Compiler error: ", log)
|
||||
{
|
||||
val sourceList = sources.map(_.asFile.getAbsolutePath).toList
|
||||
if(sourceList.isEmpty)
|
||||
{
|
||||
log.info(actionNothingToDoMessage)
|
||||
None
|
||||
}
|
||||
else
|
||||
{
|
||||
def filteredSources(extension: String) = sourceList.filter(_.endsWith(extension))
|
||||
def compile(label: String, sources: List[String], options: Seq[String])(process: (List[String], Logger) => Boolean) =
|
||||
{
|
||||
if(sources.isEmpty)
|
||||
{
|
||||
log.debug("No "+label+" sources to compile.")
|
||||
true
|
||||
}
|
||||
else
|
||||
{
|
||||
val arguments = (options ++ classpathAndOut ++ sources).toList
|
||||
log.debug(label + " arguments: " + arguments.mkString(" "))
|
||||
process(arguments, log)
|
||||
}
|
||||
}
|
||||
def scalaCompile = () =>
|
||||
{
|
||||
val scalaSourceList = if(order == CompileOrder.Mixed) sourceList else filteredSources(".scala")
|
||||
compile("Scala", scalaSourceList, options)(process)
|
||||
}
|
||||
def javaCompile = () =>
|
||||
{
|
||||
val javaSourceList = filteredSources(".java")
|
||||
compile("Java", javaSourceList, javaOptions)(processJava)
|
||||
}
|
||||
|
||||
val (first, second) = if(order == CompileOrder.JavaThenScala) (javaCompile, scalaCompile) else (scalaCompile, javaCompile)
|
||||
if(first() && second())
|
||||
{
|
||||
log.info(actionSuccessfulMessage)
|
||||
None
|
||||
}
|
||||
else
|
||||
Some(actionUnsuccessfulMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed abstract class CompilerBase extends CompilerCore
|
||||
{
|
||||
def actionStartMessage(label: String) = "Compiling " + label + " sources..."
|
||||
val actionNothingToDoMessage = "Nothing to compile."
|
||||
val actionSuccessfulMessage = "Compilation successful."
|
||||
val actionUnsuccessfulMessage = "Compilation unsuccessful."
|
||||
}
|
||||
final class ForkCompile(config: ForkScalaCompiler) extends CompilerBase
|
||||
{
|
||||
import java.io.File
|
||||
protected def process(arguments: List[String], log: Logger) =
|
||||
Fork.scalac(config.javaHome, config.compileJVMOptions, config.scalaJars, arguments, log) == 0
|
||||
override protected def processJava(args: List[String], log: Logger) =
|
||||
Fork.javac(config.javaHome, args, log) == 0
|
||||
}
|
||||
object ForkCompile
|
||||
{
|
||||
def apply(config: ForkScalaCompiler, conditional: CompileConditional) =
|
||||
{
|
||||
import conditional.config.{compileOrder, classpath, javaOptions, label, log, options, outputDirectory, sources}
|
||||
// recompile only if any sources were modified after any classes or no classes exist
|
||||
val sourcePaths = sources.get
|
||||
val newestSource = (0L /: sourcePaths)(_ max _.lastModified)
|
||||
val products = (outputDirectory ** GlobFilter("*.class")).get
|
||||
val oldestClass = (java.lang.Long.MAX_VALUE /: products)(_ min _.lastModified)
|
||||
if(products.isEmpty || newestSource > oldestClass)
|
||||
{
|
||||
// full recompile, since we are not doing proper dependency tracking
|
||||
FileUtilities.clean(outputDirectory :: Nil, log)
|
||||
val compiler = new ForkCompile(config)
|
||||
FileUtilities.createDirectory(outputDirectory.asFile, log)
|
||||
compiler(label, sourcePaths, Path.makeString(classpath.get), outputDirectory, options, javaOptions, compileOrder, log)
|
||||
}
|
||||
else
|
||||
{
|
||||
log.info("Compilation up to date.")
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The following code is based on scala.tools.nsc.Main and scala.tools.nsc.ScalaDoc
|
||||
// Copyright 2005-2008 LAMP/EPFL
|
||||
// Original author: Martin Odersky
|
||||
|
||||
final class Compile(maximumErrors: Int) extends CompilerBase
|
||||
{
|
||||
protected def process(arguments: List[String], log: Logger) =
|
||||
{
|
||||
import scala.tools.nsc.{CompilerCommand, FatalError, Global, Settings, reporters, util}
|
||||
import util.FakePos
|
||||
var reporter = new LoggerReporter(maximumErrors, log)
|
||||
val settings = new Settings(reporter.error)
|
||||
val command = new CompilerCommand(arguments, settings, error, false)
|
||||
|
||||
object compiler extends Global(command.settings, reporter)
|
||||
if(!reporter.hasErrors)
|
||||
{
|
||||
val run = new compiler.Run
|
||||
run compile command.files
|
||||
reporter.printSummary()
|
||||
}
|
||||
!reporter.hasErrors
|
||||
}
|
||||
override protected def processJava(args: List[String], log: Logger) =
|
||||
(Process("javac", args) ! log) == 0
|
||||
}
|
||||
final class Scaladoc(maximumErrors: Int) extends CompilerCore
|
||||
{
|
||||
protected def process(arguments: List[String], log: Logger) =
|
||||
{
|
||||
import scala.tools.nsc.{doc, CompilerCommand, FatalError, Global, reporters, util}
|
||||
import util.FakePos
|
||||
val reporter = new LoggerReporter(maximumErrors, log)
|
||||
val docSettings: doc.Settings = new doc.Settings(reporter.error)
|
||||
val command = new CompilerCommand(arguments, docSettings, error, false)
|
||||
object compiler extends Global(command.settings, reporter)
|
||||
{
|
||||
override val onlyPresentation = true
|
||||
}
|
||||
if(!reporter.hasErrors)
|
||||
{
|
||||
val run = new compiler.Run
|
||||
run compile command.files
|
||||
val generator = new doc.DefaultDocDriver
|
||||
{
|
||||
lazy val global: compiler.type = compiler
|
||||
lazy val settings = docSettings
|
||||
}
|
||||
generator.process(run.units)
|
||||
reporter.printSummary()
|
||||
}
|
||||
!reporter.hasErrors
|
||||
}
|
||||
def actionStartMessage(label: String) = "Generating API documentation for " + label + " sources..."
|
||||
val actionNothingToDoMessage = "No sources specified."
|
||||
val actionSuccessfulMessage = "API documentation generation successful."
|
||||
def actionUnsuccessfulMessage = "API documentation generation unsuccessful."
|
||||
}
|
||||
|
||||
// The following code is based on scala.tools.nsc.reporters.{AbstractReporter, ConsoleReporter}
|
||||
// Copyright 2002-2008 LAMP/EPFL
|
||||
// Original author: Martin Odersky
|
||||
final class LoggerReporter(maximumErrors: Int, log: Logger) extends scala.tools.nsc.reporters.Reporter
|
||||
{
|
||||
import scala.tools.nsc.util.{FakePos,Position}
|
||||
private val positions = new scala.collection.mutable.HashMap[Position, Severity]
|
||||
|
||||
def error(msg: String) { error(FakePos("scalac"), msg) }
|
||||
|
||||
def printSummary()
|
||||
{
|
||||
if(WARNING.count > 0)
|
||||
log.warn(countElementsAsString(WARNING.count, "warning") + " found")
|
||||
if(ERROR.count > 0)
|
||||
log.error(countElementsAsString(ERROR.count, "error") + " found")
|
||||
}
|
||||
|
||||
def display(pos: Position, msg: String, severity: Severity)
|
||||
{
|
||||
severity.count += 1
|
||||
if(severity != ERROR || maximumErrors < 0 || severity.count <= maximumErrors)
|
||||
print(severityToLevel(severity), pos, msg)
|
||||
}
|
||||
private def severityToLevel(severity: Severity): Level.Value =
|
||||
severity match
|
||||
{
|
||||
case ERROR => Level.Error
|
||||
case WARNING => Level.Warn
|
||||
case INFO => Level.Info
|
||||
}
|
||||
|
||||
private def print(level: Level.Value, posIn: Position, msg: String)
|
||||
{
|
||||
if(posIn == null)
|
||||
log.log(level, msg)
|
||||
else
|
||||
{
|
||||
val pos = posIn.inUltimateSource(posIn.source.getOrElse(null))
|
||||
def message =
|
||||
{
|
||||
val sourcePrefix =
|
||||
pos match
|
||||
{
|
||||
case FakePos(msg) => msg + " "
|
||||
case _ => pos.source.map(_.file.path).getOrElse("")
|
||||
}
|
||||
val lineNumberString = pos.line.map(line => ":" + line + ":").getOrElse(":") + " "
|
||||
sourcePrefix + lineNumberString + msg
|
||||
}
|
||||
log.log(level, message)
|
||||
if (!pos.line.isEmpty)
|
||||
{
|
||||
log.log(level, pos.lineContent.stripLineEnd) // source line with error/warning
|
||||
for(column <- pos.column if column > 0) // pointer to the column position of the error/warning
|
||||
log.log(level, (" " * (column-1)) + '^')
|
||||
}
|
||||
}
|
||||
}
|
||||
override def reset =
|
||||
{
|
||||
super.reset
|
||||
positions.clear
|
||||
}
|
||||
|
||||
protected def info0(pos: Position, msg: String, severity: Severity, force: Boolean)
|
||||
{
|
||||
severity match
|
||||
{
|
||||
case WARNING | ERROR =>
|
||||
{
|
||||
if(!testAndLog(pos, severity))
|
||||
display(pos, msg, severity)
|
||||
}
|
||||
case _ => display(pos, msg, severity)
|
||||
}
|
||||
}
|
||||
|
||||
private def testAndLog(pos: Position, severity: Severity): Boolean =
|
||||
{
|
||||
if(pos == null || pos.offset.isEmpty)
|
||||
false
|
||||
else if(positions.get(pos).map(_ >= severity).getOrElse(false))
|
||||
true
|
||||
else
|
||||
{
|
||||
positions(pos) = severity
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,381 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
trait Conditional[Source, Product, External] extends NotNull
|
||||
{
|
||||
type AnalysisType <: TaskAnalysis[Source, Product, External]
|
||||
val analysis: AnalysisType = loadAnalysis
|
||||
|
||||
protected def loadAnalysis: AnalysisType
|
||||
protected def log: Logger
|
||||
|
||||
protected def productType: String
|
||||
protected def productTypePlural: String
|
||||
|
||||
protected def sourcesToProcess: Iterable[Source]
|
||||
|
||||
protected def sourceExists(source: Source): Boolean
|
||||
protected def sourceLastModified(source: Source): Long
|
||||
|
||||
protected def productExists(product: Product): Boolean
|
||||
protected def productLastModified(product: Product): Long
|
||||
|
||||
protected def externalInfo(externals: Iterable[External]): Iterable[(External, ExternalInfo)]
|
||||
|
||||
protected def execute(cAnalysis: ConditionalAnalysis): Option[String]
|
||||
|
||||
final case class ExternalInfo(available: Boolean, lastModified: Long) extends NotNull
|
||||
trait ConditionalAnalysis extends NotNull
|
||||
{
|
||||
def dirtySources: Iterable[Source]
|
||||
def cleanSources: Iterable[Source]
|
||||
def directlyModifiedSourcesCount: Int
|
||||
def invalidatedSourcesCount: Int
|
||||
def removedSourcesCount: Int
|
||||
}
|
||||
|
||||
final def run =
|
||||
{
|
||||
val result = execute(analyze)
|
||||
processingComplete(result.isEmpty)
|
||||
result
|
||||
}
|
||||
private def analyze =
|
||||
{
|
||||
import scala.collection.mutable.HashSet
|
||||
|
||||
val sourcesSnapshot = sourcesToProcess
|
||||
val removedSources = new HashSet[Source]
|
||||
removedSources ++= analysis.allSources
|
||||
removedSources --= sourcesSnapshot
|
||||
val removedCount = removedSources.size
|
||||
for(removed <- removedSources)
|
||||
analysis.removeDependent(removed)
|
||||
|
||||
val unmodified = new HashSet[Source]
|
||||
val modified = new HashSet[Source]
|
||||
|
||||
for(source <- sourcesSnapshot)
|
||||
{
|
||||
if(isSourceModified(source))
|
||||
{
|
||||
log.debug("Source " + source + " directly modified.")
|
||||
modified += source
|
||||
}
|
||||
else
|
||||
{
|
||||
log.debug("Source " + source + " unmodified.")
|
||||
unmodified += source
|
||||
}
|
||||
}
|
||||
val directlyModifiedCount = modified.size
|
||||
for((external, info) <- externalInfo(analysis.allExternals))
|
||||
{
|
||||
val dependentSources = analysis.externalDependencies(external).getOrElse(Set.empty)
|
||||
if(info.available)
|
||||
{
|
||||
val dependencyLastModified = info.lastModified
|
||||
for(dependentSource <- dependentSources; dependentProducts <- analysis.products(dependentSource))
|
||||
{
|
||||
dependentProducts.find(p => productLastModified(p) < dependencyLastModified) match
|
||||
{
|
||||
case Some(modifiedProduct) =>
|
||||
{
|
||||
log.debug(productType + " " + modifiedProduct + " older than external dependency " + external)
|
||||
unmodified -= dependentSource
|
||||
modified += dependentSource
|
||||
}
|
||||
case None => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
log.debug("External dependency " + external + " not found.")
|
||||
unmodified --= dependentSources
|
||||
modified ++= dependentSources
|
||||
analysis.removeExternalDependency(external)
|
||||
}
|
||||
}
|
||||
|
||||
val handled = new scala.collection.mutable.HashSet[Source]
|
||||
val transitive = !java.lang.Boolean.getBoolean("sbt.intransitive")
|
||||
def markModified(changed: Iterable[Source]) { for(c <- changed if !handled.contains(c)) markSourceModified(c) }
|
||||
def markSourceModified(src: Source)
|
||||
{
|
||||
unmodified -= src
|
||||
modified += src
|
||||
handled += src
|
||||
if(transitive)
|
||||
markDependenciesModified(src)
|
||||
}
|
||||
def markDependenciesModified(src: Source) { analysis.removeDependencies(src).map(markModified) }
|
||||
|
||||
markModified(modified.toList)
|
||||
if(transitive)
|
||||
removedSources.foreach(markDependenciesModified)
|
||||
|
||||
for(changed <- removedSources ++ modified)
|
||||
analysis.removeSource(changed)
|
||||
|
||||
new ConditionalAnalysis
|
||||
{
|
||||
def dirtySources = wrap.Wrappers.readOnly(modified)
|
||||
def cleanSources = wrap.Wrappers.readOnly(unmodified)
|
||||
def directlyModifiedSourcesCount = directlyModifiedCount
|
||||
def invalidatedSourcesCount = dirtySources.size - directlyModifiedCount
|
||||
def removedSourcesCount = removedCount
|
||||
override def toString =
|
||||
{
|
||||
" Source analysis: " + directlyModifiedSourcesCount + " new/modified, " +
|
||||
invalidatedSourcesCount + " indirectly invalidated, " +
|
||||
removedSourcesCount + " removed."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected def checkLastModified = true
|
||||
protected def noProductsImpliesModified = true
|
||||
protected def isSourceModified(source: Source) =
|
||||
{
|
||||
analysis.products(source) match
|
||||
{
|
||||
case None =>
|
||||
{
|
||||
log.debug("New file " + source)
|
||||
true
|
||||
}
|
||||
case Some(sourceProducts) =>
|
||||
{
|
||||
val sourceModificationTime = sourceLastModified(source)
|
||||
def isOutofdate(p: Product) =
|
||||
!productExists(p) || (checkLastModified && productLastModified(p) < sourceModificationTime)
|
||||
|
||||
sourceProducts.find(isOutofdate) match
|
||||
{
|
||||
case Some(modifiedProduct) =>
|
||||
log.debug("Outdated " + productType + ": " + modifiedProduct + " for source " + source)
|
||||
true
|
||||
case None =>
|
||||
if(noProductsImpliesModified && sourceProducts.isEmpty)
|
||||
{
|
||||
// necessary for change detection that depends on last modified
|
||||
log.debug("Source " + source + " has no products, marking it modified.")
|
||||
true
|
||||
}
|
||||
else
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
protected def processingComplete(success: Boolean)
|
||||
{
|
||||
if(success)
|
||||
{
|
||||
analysis.save()
|
||||
log.info(" Post-analysis: " + analysis.allProducts.toSeq.length + " " + productTypePlural + ".")
|
||||
}
|
||||
else
|
||||
analysis.revert()
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AbstractCompileConfiguration extends NotNull
|
||||
{
|
||||
def label: String
|
||||
def sources: PathFinder
|
||||
def outputDirectory: Path
|
||||
def classpath: PathFinder
|
||||
def analysisPath: Path
|
||||
def projectPath: Path
|
||||
def log: Logger
|
||||
def options: Seq[String]
|
||||
def javaOptions: Seq[String]
|
||||
def maxErrors: Int
|
||||
def compileOrder: CompileOrder.Value
|
||||
}
|
||||
abstract class CompileConfiguration extends AbstractCompileConfiguration
|
||||
{
|
||||
def testDefinitionClassNames: Iterable[String]
|
||||
}
|
||||
import java.io.File
|
||||
class CompileConditional(override val config: CompileConfiguration) extends AbstractCompileConditional(config)
|
||||
{
|
||||
import config._
|
||||
type AnalysisType = CompileAnalysis
|
||||
protected def constructAnalysis(analysisPath: Path, projectPath: Path, log: Logger) =
|
||||
new CompileAnalysis(analysisPath, projectPath, log)
|
||||
protected def analysisCallback = new CompileAnalysisCallback
|
||||
protected class CompileAnalysisCallback extends BasicCompileAnalysisCallback(projectPath, testDefinitionClassNames, analysis)
|
||||
{
|
||||
def foundSubclass(sourcePath: Path, subclassName: String, superclassName: String, isModule: Boolean)
|
||||
{
|
||||
analysis.addTest(sourcePath, TestDefinition(isModule, subclassName, superclassName))
|
||||
}
|
||||
}
|
||||
}
|
||||
abstract class AbstractCompileConditional(val config: AbstractCompileConfiguration) extends Conditional[Path, Path, File]
|
||||
{
|
||||
import config._
|
||||
type AnalysisType <: BasicCompileAnalysis
|
||||
protected def loadAnalysis =
|
||||
{
|
||||
val a = constructAnalysis(analysisPath, projectPath, log)
|
||||
for(errorMessage <- a.load())
|
||||
error(errorMessage)
|
||||
a
|
||||
}
|
||||
protected def constructAnalysis(analysisPath: Path, projectPath: Path, log: Logger): AnalysisType
|
||||
|
||||
protected def log = config.log
|
||||
|
||||
protected def productType = "class"
|
||||
protected def productTypePlural = "classes"
|
||||
protected def sourcesToProcess = sources.get
|
||||
|
||||
protected def sourceExists(source: Path) = source.asFile.exists
|
||||
protected def sourceLastModified(source: Path) = source.asFile.lastModified
|
||||
|
||||
protected def productExists(product: Path) = product.asFile.exists
|
||||
protected def productLastModified(product: Path) = product.asFile.lastModified
|
||||
|
||||
protected def externalInfo(externals: Iterable[File]) =
|
||||
{
|
||||
val (classpathJars, classpathDirs) = ClasspathUtilities.buildSearchPaths(classpath.get)
|
||||
for(external <- externals) yield
|
||||
{
|
||||
val available = external.exists && ClasspathUtilities.onClasspath(classpathJars, classpathDirs, external)
|
||||
if(!available)
|
||||
log.debug("External " + external + (if(external.exists) " not on classpath." else " does not exist."))
|
||||
(external, ExternalInfo(available, external.lastModified))
|
||||
}
|
||||
}
|
||||
|
||||
import ChangeDetection.{LastModifiedOnly, HashOnly, HashAndLastModified, HashAndProductsExist}
|
||||
protected def changeDetectionMethod: ChangeDetection.Value = HashAndProductsExist
|
||||
override protected def checkLastModified = changeDetectionMethod != HashAndProductsExist
|
||||
override protected def noProductsImpliesModified = changeDetectionMethod == LastModifiedOnly
|
||||
override protected def isSourceModified(source: Path) =
|
||||
changeDetectionMethod match
|
||||
{
|
||||
case HashAndLastModified | HashAndProductsExist =>
|
||||
// behavior will differ because of checkLastModified
|
||||
// hash modified must come first so that the latest hash is calculated for every source
|
||||
hashModified(source) || super.isSourceModified(source)
|
||||
case HashOnly => hashModified(source)
|
||||
case LastModifiedOnly => super.isSourceModified(source)
|
||||
}
|
||||
|
||||
import scala.collection.mutable.{Buffer, ListBuffer}
|
||||
private val newHashes: Buffer[(Path, Option[Array[Byte]])] = new ListBuffer
|
||||
private def warnHashError(source: Path, message: String)
|
||||
{
|
||||
log.warn("Error computing hash for source " + source + ": " + message)
|
||||
newHashes += ((source, None))
|
||||
}
|
||||
protected def hashModified(source: Path) =
|
||||
{
|
||||
source.isDirectory ||
|
||||
(analysis.hash(source) match
|
||||
{
|
||||
case None =>
|
||||
log.debug("Source " + source + " had no hash, marking modified.")
|
||||
Hash(source, log).fold(err => warnHashError(source, err), newHash => newHashes += ((source, Some(newHash))))
|
||||
true
|
||||
case Some(oldHash) =>
|
||||
{
|
||||
Hash(source, log) match
|
||||
{
|
||||
case Left(err) =>
|
||||
warnHashError(source, err)
|
||||
log.debug("Assuming source is modified because of error.")
|
||||
true
|
||||
case Right(newHash) =>
|
||||
newHashes += ((source, Some(newHash)))
|
||||
val different = !(oldHash deepEquals newHash)
|
||||
if(different)
|
||||
log.debug("Hash for source " + source + " changed (was " + Hash.toHex(oldHash) +
|
||||
", is now " + Hash.toHex(newHash) + "), marking modified.")
|
||||
different
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
protected def execute(executeAnalysis: ConditionalAnalysis) =
|
||||
{
|
||||
log.info(executeAnalysis.toString)
|
||||
finishHashes()
|
||||
import executeAnalysis.dirtySources
|
||||
|
||||
// the output directory won't show up in the classpath unless it exists, so do this before classpath.get
|
||||
val outputDir = outputDirectory.asFile
|
||||
FileUtilities.createDirectory(outputDir, log)
|
||||
|
||||
val cp = classpath.get
|
||||
if(!dirtySources.isEmpty)
|
||||
checkClasspath(cp)
|
||||
val classpathString = Path.makeString(cp)
|
||||
val id = AnalysisCallback.register(analysisCallback)
|
||||
val allOptions = (("-Xplugin:" + FileUtilities.sbtJar.getAbsolutePath) ::
|
||||
("-P:sbt-analyzer:callback:" + id.toString) :: Nil) ++ options
|
||||
val r = (new Compile(config.maxErrors))(label, dirtySources, classpathString, outputDirectory, allOptions, javaOptions, compileOrder, log)
|
||||
AnalysisCallback.unregister(id)
|
||||
if(log.atLevel(Level.Debug))
|
||||
{
|
||||
/** This checks that the plugin accounted for all classes in the output directory.*/
|
||||
val classes = scala.collection.mutable.HashSet(analysis.allProducts.toSeq: _*)
|
||||
var missed = 0
|
||||
for(c <- (outputDirectory ** GlobFilter("*.class")).get)
|
||||
{
|
||||
if(!classes.contains(c))
|
||||
{
|
||||
missed += 1
|
||||
log.debug("Missed class: " + c)
|
||||
}
|
||||
}
|
||||
log.debug("Total missed classes: " + missed)
|
||||
}
|
||||
r
|
||||
}
|
||||
private def finishHashes()
|
||||
{
|
||||
if(changeDetectionMethod == LastModifiedOnly)
|
||||
analysis.clearHashes()
|
||||
else
|
||||
{
|
||||
for((path, hash) <- newHashes)
|
||||
{
|
||||
hash match
|
||||
{
|
||||
case None => analysis.clearHash(path)
|
||||
case Some(hash) => analysis.setHash(path, hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
newHashes.clear()
|
||||
}
|
||||
private def checkClasspath(cp: Iterable[Path])
|
||||
{
|
||||
import scala.collection.mutable.{HashMap, HashSet, Set}
|
||||
val collisions = new HashMap[String, Set[Path]]
|
||||
for(jar <- cp if ClasspathUtilities.isArchive(jar))
|
||||
collisions.getOrElseUpdate(jar.asFile.getName, new HashSet[Path]) += jar
|
||||
for((name, jars) <- collisions)
|
||||
{
|
||||
if(jars.size > 1)
|
||||
{
|
||||
log.warn("Possible duplicate classpath locations for jar " + name + ": ")
|
||||
for(jar <- jars) log.warn("\t" + jar.absolutePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected def analysisCallback: AnalysisCallback
|
||||
}
|
||||
object ChangeDetection extends Enumeration
|
||||
{
|
||||
val LastModifiedOnly, HashOnly, HashAndLastModified, HashAndProductsExist = Value
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
/** The trap methods execute the provided code in a try block and handle a thrown exception.*/
|
||||
object Control
|
||||
{
|
||||
def trap[T](errorMessagePrefix: => String, log: Logger)(execute: => Either[String, T]): Either[String, T] =
|
||||
try { execute }
|
||||
catch { case e => log.trace(e); Left(errorMessagePrefix + e.toString) }
|
||||
|
||||
def trapAndFinally[T](errorMessagePrefix: => String, log: Logger)(execute: => Either[String, T])(doFinally: => Unit): Either[String, T] =
|
||||
try { execute }
|
||||
catch { case e => log.trace(e); Left(errorMessagePrefix + e.toString) }
|
||||
finally { trapAndLog(log)(doFinally) }
|
||||
|
||||
def trapUnit(errorMessagePrefix: => String, log: Logger)(execute: => Option[String]): Option[String] =
|
||||
try { execute }
|
||||
catch { case e => log.trace(e); Some(errorMessagePrefix + e.toString) }
|
||||
|
||||
def trapUnitAndFinally(errorMessagePrefix: => String, log: Logger)(execute: => Option[String])(doFinally: => Unit): Option[String] =
|
||||
try { execute }
|
||||
catch { case e => log.trace(e); Some(errorMessagePrefix + e.toString) }
|
||||
finally { trapAndLog(log)(doFinally) }
|
||||
|
||||
def trap(execute: => Unit)
|
||||
{
|
||||
try { execute }
|
||||
catch { case e: Exception => () }
|
||||
}
|
||||
def trapAndLog(log: Logger)(execute: => Unit)
|
||||
{
|
||||
try { execute }
|
||||
catch { case e => log.trace(e); log.error(e.toString) }
|
||||
}
|
||||
def convertException[T](t: => T): Either[Exception, T] =
|
||||
{
|
||||
try { Right(t) }
|
||||
catch { case e: Exception => Left(e) }
|
||||
}
|
||||
def convertErrorMessage[T](log: Logger)(t: => T): Either[String, T] =
|
||||
{
|
||||
try { Right(t) }
|
||||
catch { case e: Exception => log.trace(e); Left(e.toString) }
|
||||
}
|
||||
|
||||
def getOrError[T](result: Either[String, T]): T = result.fold(error, x=>x)
|
||||
final def lazyFold[T](list: List[T])(f: T => Option[String]): Option[String] =
|
||||
list match
|
||||
{
|
||||
case Nil => None
|
||||
case head :: tail =>
|
||||
f(head) match
|
||||
{
|
||||
case None => lazyFold(tail)(f)
|
||||
case x => x
|
||||
}
|
||||
}
|
||||
final def lazyFold[T, S](list: List[T], value: S)(f: (S,T) => Either[String, S]): Either[String, S] =
|
||||
list match
|
||||
{
|
||||
case Nil => Right(value)
|
||||
case head :: tail =>
|
||||
f(value, head) match
|
||||
{
|
||||
case Right(newValue) => lazyFold(tail, newValue)(f)
|
||||
case x => x
|
||||
}
|
||||
}
|
||||
def thread[T](e: Either[String, T])(f: T => Option[String]): Option[String] =
|
||||
e.right.flatMap( t => f(t).toLeft(()) ).left.toOption
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008 David MacIver
|
||||
*/
|
||||
package sbt;
|
||||
|
||||
import scala.collection.mutable;
|
||||
|
||||
trait Dag[Node <: Dag[Node]]{
|
||||
self : Node =>
|
||||
|
||||
def dependencies : Iterable[Node]
|
||||
|
||||
def topologicalSort = {
|
||||
val discovered = new mutable.HashSet[Node];
|
||||
val finished = new wrap.MutableSetWrapper(new java.util.LinkedHashSet[Node])
|
||||
|
||||
def visit(dag : Node){
|
||||
if (!discovered(dag)) {
|
||||
discovered(dag) = true;
|
||||
dag.dependencies.foreach(visit);
|
||||
finished += dag;
|
||||
}
|
||||
}
|
||||
|
||||
visit(self);
|
||||
|
||||
finished.toList;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,458 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah, David MacIver
|
||||
*/
|
||||
package sbt
|
||||
|
||||
/** The default project when no project is explicitly configured and the common base class for
|
||||
* configuring a project.*/
|
||||
class DefaultProject(val info: ProjectInfo) extends BasicScalaProject with MavenStyleScalaPaths
|
||||
class DefaultWebProject(val info: ProjectInfo) extends BasicWebScalaProject with MavenStyleWebScalaPaths
|
||||
|
||||
|
||||
import BasicScalaProject._
|
||||
import ScalaProject.{optionsAsString, javaOptionsAsString}
|
||||
import java.io.File
|
||||
import java.util.jar.Attributes
|
||||
|
||||
/** This class defines concrete instances of actions from ScalaProject using overridable paths,
|
||||
* options, and configuration. */
|
||||
abstract class BasicScalaProject extends ScalaProject with BasicDependencyProject with ScalaPaths
|
||||
{
|
||||
/** The explicitly specified class to be run by the 'run' action.
|
||||
* See http://code.google.com/p/simple-build-tool/wiki/RunningProjectCode for details.*/
|
||||
def mainClass: Option[String] = None
|
||||
/** Gets the main class to use. This is used by package and run to determine which main
|
||||
* class to run or include as the Main-Class attribute.
|
||||
* If `mainClass` is explicitly specified, it is used. Otherwise, the main class is selected from
|
||||
* the classes with a main method as automatically detected by the analyzer plugin.
|
||||
* `promptIfMultipleChoices` controls the behavior when multiple main classes are detected.
|
||||
* If true, it prompts the user to select which main class to use. If false, it prints a warning
|
||||
* and returns no main class.*/
|
||||
def getMainClass(promptIfMultipleChoices: Boolean) =
|
||||
mainClass orElse
|
||||
{
|
||||
val applications = mainCompileConditional.analysis.allApplications.toList
|
||||
impl.SelectMainClass(promptIfMultipleChoices, applications) orElse
|
||||
{
|
||||
if(!promptIfMultipleChoices && !applications.isEmpty)
|
||||
warnMultipleMainClasses(log)
|
||||
None
|
||||
}
|
||||
}
|
||||
/** Specifies the value of the `Class-Path` attribute in the manifest of the main jar. */
|
||||
def manifestClassPath: Option[String] = None
|
||||
def dependencies = info.dependencies ++ subProjects.values.toList
|
||||
|
||||
val mainCompileConditional = new CompileConditional(mainCompileConfiguration)
|
||||
val testCompileConditional = new CompileConditional(testCompileConfiguration)
|
||||
|
||||
def compileOrder = CompileOrder.Mixed
|
||||
|
||||
/** The main artifact produced by this project. To redefine the main artifact, override `defaultMainArtifact`
|
||||
* Additional artifacts are defined by `val`s of type `Artifact`.*/
|
||||
lazy val mainArtifact = defaultMainArtifact
|
||||
/** Defines the default main Artifact assigned to `mainArtifact`. By default, this is a jar file with name given
|
||||
* by `artifactID`.*/
|
||||
protected def defaultMainArtifact = Artifact(artifactID, "jar", "jar")
|
||||
|
||||
import Project._
|
||||
|
||||
/** The options provided to the 'compile' action to pass to the Scala compiler.*/
|
||||
def compileOptions: Seq[CompileOption] = Deprecation :: Nil
|
||||
/** The options provided to the 'compile' action to pass to the Java compiler. */
|
||||
def javaCompileOptions: Seq[JavaCompileOption] = Nil
|
||||
/** The options provided to the 'test-compile' action, defaulting to those for the 'compile' action.*/
|
||||
def testCompileOptions: Seq[CompileOption] = compileOptions
|
||||
/** The options provided to the 'test-compile' action to pass to the Java compiler. */
|
||||
def testJavaCompileOptions: Seq[JavaCompileOption] = javaCompileOptions
|
||||
|
||||
/** The options provided to the 'doc' and 'docTest' actions.*/
|
||||
def documentOptions: Seq[ScaladocOption] =
|
||||
LinkSource ::
|
||||
documentTitle(name + " " + version + " API") ::
|
||||
windowTitle(name + " " + version + " API") ::
|
||||
Nil
|
||||
/** The options provided to the 'test' action..*/
|
||||
def testOptions: Seq[TestOption] =
|
||||
TestListeners(testListeners) ::
|
||||
TestFilter(includeTest) ::
|
||||
Nil
|
||||
/** The options provided to the clean action. You can add files to be removed and files to be preserved here.*/
|
||||
def cleanOptions: Seq[CleanOption] =
|
||||
ClearAnalysis(mainCompileConditional.analysis) ::
|
||||
ClearAnalysis(testCompileConditional.analysis) ::
|
||||
historyPath.map(history => Preserve(history)).toList
|
||||
|
||||
def packageOptions: Seq[PackageOption] =
|
||||
manifestClassPath.map(cp => ManifestAttributes( (Attributes.Name.CLASS_PATH, cp) )).toList :::
|
||||
getMainClass(false).map(MainClass(_)).toList
|
||||
|
||||
private def succeededTestPath = testAnalysisPath / "succeeded-tests"
|
||||
private def quickOptions(failedOnly: Boolean) =
|
||||
{
|
||||
val path = succeededTestPath
|
||||
val analysis = testCompileConditional.analysis
|
||||
TestFilter(new impl.TestQuickFilter(analysis, failedOnly, path, log)) :: TestListeners(new impl.TestStatusReporter(path, log) :: Nil) :: Nil
|
||||
}
|
||||
|
||||
protected def includeTest(test: String): Boolean = true
|
||||
|
||||
/** This is called to create the initial directories when a user makes a new project from
|
||||
* sbt.*/
|
||||
override final def initializeDirectories()
|
||||
{
|
||||
FileUtilities.createDirectories(directoriesToCreate.map(_.asFile), log) match
|
||||
{
|
||||
case Some(errorMessage) => log.error("Could not initialize directory structure: " + errorMessage)
|
||||
case None => log.success("Successfully initialized directory structure.")
|
||||
}
|
||||
}
|
||||
import Configurations._
|
||||
/** The managed configuration to use when determining the classpath for a Scala interpreter session.*/
|
||||
def consoleConfiguration = Test
|
||||
|
||||
/** A PathFinder that provides the classpath to pass to scaladoc. It is the same as the compile classpath
|
||||
* by default. */
|
||||
def docClasspath = compileClasspath
|
||||
/** A PathFinder that provides the classpath to pass to the compiler.*/
|
||||
def compileClasspath = fullClasspath(Compile) +++ optionalClasspath
|
||||
/** A PathFinder that provides the classpath to use when unit testing.*/
|
||||
def testClasspath = fullClasspath(Test) +++ optionalClasspath
|
||||
/** A PathFinder that provides the classpath to use when running the class specified by 'getMainClass'.*/
|
||||
def runClasspath = fullClasspath(Runtime) +++ optionalClasspath
|
||||
/** A PathFinder that provides the classpath to use for a Scala interpreter session.*/
|
||||
def consoleClasspath = fullClasspath(consoleConfiguration) +++ optionalClasspath
|
||||
/** A PathFinder that corresponds to Maven's optional scope. It includes any managed libraries in the
|
||||
* 'optional' configuration for this project only.*/
|
||||
def optionalClasspath = managedClasspath(Optional)
|
||||
/** A PathFinder that contains the jars that should be included in a comprehensive package. This is
|
||||
* by default the 'runtime' classpath excluding the 'provided' classpath.*/
|
||||
def publicClasspath = runClasspath --- fullClasspath(Provided)
|
||||
|
||||
/** This returns the unmanaged classpath for only this project for the given configuration. It by
|
||||
* default includes the main compiled classes for this project and the libraries in this project's
|
||||
* unmanaged library directory (lib) and the managed directory for the specified configuration. It
|
||||
* also adds the resource directories appropriate to the configuration.*/
|
||||
def fullUnmanagedClasspath(config: Configuration) =
|
||||
{
|
||||
config match
|
||||
{
|
||||
case CompilerPlugin => unmanagedClasspath
|
||||
case Runtime => runUnmanagedClasspath
|
||||
case Test => testUnmanagedClasspath
|
||||
case _ => mainUnmanagedClasspath
|
||||
}
|
||||
}
|
||||
/** The unmanaged base classpath. By default, the unmanaged classpaths for test and run include this classpath. */
|
||||
protected def mainUnmanagedClasspath = mainCompilePath +++ mainResourceClasspath +++ unmanagedClasspath
|
||||
/** The unmanaged classpath for the run configuration. By default, it includes the base classpath returned by
|
||||
* `mainUnmanagedClasspath`.*/
|
||||
protected def runUnmanagedClasspath = mainUnmanagedClasspath +++ mainDependencies.scalaCompiler
|
||||
/** The unmanaged classpath for the test configuration. By default, it includes the run classpath, which includes the base
|
||||
* classpath returned by `mainUnmanagedClasspath`.*/
|
||||
protected def testUnmanagedClasspath = testCompilePath +++ testResourceClasspath +++ testDependencies.scalaCompiler +++ runUnmanagedClasspath
|
||||
|
||||
/** @deprecated Use `mainDependencies.scalaJars`*/
|
||||
@deprecated protected final def scalaJars: Iterable[File] = mainDependencies.scalaJars.get.map(_.asFile)
|
||||
/** An analysis of the jar dependencies of the main Scala sources. It is only valid after main source compilation.
|
||||
* See the LibraryDependencies class for details. */
|
||||
final def mainDependencies = new LibraryDependencies(this, mainCompileConditional)
|
||||
/** An analysis of the jar dependencies of the test Scala sources. It is only valid after test source compilation.
|
||||
* See the LibraryDependencies class for details. */
|
||||
final def testDependencies = new LibraryDependencies(this, testCompileConditional)
|
||||
|
||||
/** The list of test frameworks to use for testing. Note that adding frameworks to this list
|
||||
* for an active project currently requires an explicit 'clean' to properly update the set of tests to
|
||||
* run*/
|
||||
def testFrameworks: Iterable[TestFramework] = ScalaCheckFramework :: SpecsFramework :: ScalaTestFramework :: Nil
|
||||
/** The list of listeners for testing. */
|
||||
def testListeners: Seq[TestReportListener] = new LogTestReportListener(log) :: Nil
|
||||
|
||||
def mainLabel = "main"
|
||||
def testLabel = "test"
|
||||
|
||||
def mainCompileConfiguration = new MainCompileConfig
|
||||
def testCompileConfiguration = new TestCompileConfig
|
||||
abstract class BaseCompileConfig extends CompileConfiguration
|
||||
{
|
||||
def log = BasicScalaProject.this.log
|
||||
def projectPath = info.projectPath
|
||||
def baseCompileOptions: Seq[CompileOption]
|
||||
lazy val localBaseOptions = baseCompileOptions
|
||||
def options = optionsAsString(localBaseOptions.filter(!_.isInstanceOf[MaxCompileErrors]))
|
||||
def maxErrors = maximumErrors(localBaseOptions)
|
||||
def compileOrder = BasicScalaProject.this.compileOrder
|
||||
}
|
||||
class MainCompileConfig extends BaseCompileConfig
|
||||
{
|
||||
def baseCompileOptions = compileOptions
|
||||
def label = mainLabel
|
||||
def sources = mainSources
|
||||
def outputDirectory = mainCompilePath
|
||||
def classpath = compileClasspath
|
||||
def analysisPath = mainAnalysisPath
|
||||
def testDefinitionClassNames = Nil
|
||||
def javaOptions = javaOptionsAsString(javaCompileOptions)
|
||||
}
|
||||
class TestCompileConfig extends BaseCompileConfig
|
||||
{
|
||||
def baseCompileOptions = testCompileOptions
|
||||
def label = testLabel
|
||||
def sources = testSources
|
||||
def outputDirectory = testCompilePath
|
||||
def classpath = testClasspath
|
||||
def analysisPath = testAnalysisPath
|
||||
def testDefinitionClassNames = testFrameworks.map(_.testSuperClassName)
|
||||
def javaOptions = javaOptionsAsString(testJavaCompileOptions)
|
||||
}
|
||||
|
||||
/** Configures forking the compiler and runner. Use ForkScalaCompiler, ForkScalaRun or mix together.*/
|
||||
def fork: Option[ForkScala] = None
|
||||
private def doCompile(conditional: CompileConditional) =
|
||||
{
|
||||
fork match
|
||||
{
|
||||
case Some(fc: ForkScalaCompiler) => ForkCompile(fc, conditional)
|
||||
case _ => conditional.run
|
||||
}
|
||||
}
|
||||
private def getRunner =
|
||||
{
|
||||
fork match
|
||||
{
|
||||
case Some(fr: ForkScalaRun) => new ForkRun(fr)
|
||||
case _ => Run
|
||||
}
|
||||
}
|
||||
|
||||
protected def compileAction = task { doCompile(mainCompileConditional) } describedAs MainCompileDescription
|
||||
protected def testCompileAction = task { doCompile(testCompileConditional) } dependsOn compile describedAs TestCompileDescription
|
||||
protected def cleanAction = cleanTask(outputPath, cleanOptions) describedAs CleanDescription
|
||||
protected def runAction = task { args => runTask(getMainClass(true), runClasspath, args, getRunner) dependsOn(compile) } describedAs RunDescription
|
||||
protected def consoleQuickAction = consoleTask(consoleClasspath, getRunner) describedAs ConsoleQuickDescription
|
||||
protected def consoleAction = consoleTask(consoleClasspath, getRunner).dependsOn(testCompile) describedAs ConsoleDescription
|
||||
protected def docAction = scaladocTask(mainLabel, mainSources, mainDocPath, docClasspath, documentOptions).dependsOn(compile) describedAs DocDescription
|
||||
protected def docTestAction = scaladocTask(testLabel, testSources, testDocPath, docClasspath, documentOptions).dependsOn(testCompile) describedAs TestDocDescription
|
||||
protected def testAction = defaultTestTask(testOptions)
|
||||
protected def testOnlyAction = testQuickMethod(testCompileConditional.analysis, testOptions)(options =>
|
||||
defaultTestTask(options)) describedAs(TestOnlyDescription)
|
||||
protected def testQuickAction = defaultTestQuickMethod(false) describedAs(TestQuickDescription)
|
||||
protected def testFailedAction = defaultTestQuickMethod(true) describedAs(TestFailedDescription)
|
||||
protected def defaultTestQuickMethod(failedOnly: Boolean) =
|
||||
testQuickMethod(testCompileConditional.analysis, testOptions)(options => defaultTestTask(quickOptions(failedOnly) ::: options.toList))
|
||||
protected def defaultTestTask(testOptions: => Seq[TestOption]) =
|
||||
testTask(testFrameworks, testClasspath, testCompileConditional.analysis, testOptions).dependsOn(testCompile) describedAs TestDescription
|
||||
|
||||
override protected def makePomAction = super.makePomAction dependsOn(`package`)
|
||||
override protected def deliverLocalAction = super.deliverLocalAction dependsOn(`package`)
|
||||
override protected def deliverAction = super.deliverAction dependsOn(`package`)
|
||||
|
||||
protected def packageAction = packageTask(packagePaths, jarPath, packageOptions).dependsOn(compile) describedAs PackageDescription
|
||||
protected def packageTestAction = packageTask(packageTestPaths, packageTestJar).dependsOn(testCompile) describedAs TestPackageDescription
|
||||
protected def packageDocsAction = packageTask(mainDocPath ##, packageDocsJar, Recursive).dependsOn(doc) describedAs DocPackageDescription
|
||||
protected def packageSrcAction = packageTask(packageSourcePaths, packageSrcJar) describedAs SourcePackageDescription
|
||||
protected def packageTestSrcAction = packageTask(packageTestSourcePaths, packageTestSrcJar) describedAs TestSourcePackageDescription
|
||||
protected def packageProjectAction = zipTask(packageProjectPaths, packageProjectZip) describedAs ProjectPackageDescription
|
||||
|
||||
protected def docAllAction = (doc && docTest) describedAs DocAllDescription
|
||||
protected def packageAllAction = task { None } dependsOn(`package`, packageTest, packageSrc, packageTestSrc, packageDocs) describedAs PackageAllDescription
|
||||
protected def graphAction = graphTask(graphPath, mainCompileConditional.analysis).dependsOn(compile)
|
||||
protected def incrementVersionAction = task { incrementVersionNumber(); None } describedAs IncrementVersionDescription
|
||||
protected def releaseAction = (test && packageAll && incrementVersion) describedAs ReleaseDescription
|
||||
|
||||
lazy val compile = compileAction
|
||||
lazy val testCompile = testCompileAction
|
||||
lazy val clean = cleanAction
|
||||
lazy val run = runAction
|
||||
lazy val consoleQuick = consoleQuickAction
|
||||
lazy val console = consoleAction
|
||||
lazy val doc = docAction
|
||||
lazy val docTest = docTestAction
|
||||
lazy val test = testAction
|
||||
lazy val `package` = packageAction
|
||||
lazy val packageTest = packageTestAction
|
||||
lazy val packageDocs = packageDocsAction
|
||||
lazy val packageSrc = packageSrcAction
|
||||
lazy val packageTestSrc = packageTestSrcAction
|
||||
lazy val packageProject = packageProjectAction
|
||||
lazy val docAll = docAllAction
|
||||
lazy val packageAll = packageAllAction
|
||||
lazy val graph = graphAction
|
||||
lazy val incrementVersion = incrementVersionAction
|
||||
lazy val release = releaseAction
|
||||
|
||||
lazy val testQuick = testQuickAction
|
||||
lazy val testFailed = testFailedAction
|
||||
lazy val testOnly = testOnlyAction
|
||||
|
||||
def jarsOfProjectDependencies = Path.lazyPathFinder {
|
||||
topologicalSort.dropRight(1) flatMap { p =>
|
||||
p match
|
||||
{
|
||||
case bpp: BasicScalaPaths => List(bpp.jarPath)
|
||||
case _ => Nil
|
||||
}
|
||||
}
|
||||
}
|
||||
override def deliverScalaDependencies: Iterable[ModuleID] =
|
||||
{
|
||||
val snapshot = mainDependencies.snapshot
|
||||
mapScalaModule(snapshot.scalaLibrary, ManageDependencies.ScalaLibraryID) ++
|
||||
mapScalaModule(snapshot.scalaCompiler, ManageDependencies.ScalaCompilerID)
|
||||
}
|
||||
override def watchPaths = mainSources +++ testSources +++ mainResources +++ testResources
|
||||
}
|
||||
abstract class BasicWebScalaProject extends BasicScalaProject with WebScalaProject with WebScalaPaths
|
||||
{
|
||||
import BasicWebScalaProject._
|
||||
override def watchPaths = super.watchPaths +++ webappResources
|
||||
|
||||
lazy val prepareWebapp = prepareWebappAction
|
||||
protected def prepareWebappAction =
|
||||
prepareWebappTask(webappResources, temporaryWarPath, webappClasspath, mainDependencies.scalaJars) dependsOn(compile)
|
||||
|
||||
def webappClasspath = publicClasspath
|
||||
def jettyRunClasspath = testClasspath
|
||||
def jettyWebappPath = temporaryWarPath
|
||||
lazy val jettyRun = jettyRunAction
|
||||
protected def jettyRunAction =
|
||||
jettyRunTask(jettyWebappPath, jettyContextPath, jettyPort, jettyRunClasspath, "test", scanDirectories.map(_.asFile), scanInterval) dependsOn(prepareWebapp) describedAs(JettyRunDescription)
|
||||
|
||||
/** The directories that should be watched to determine if the web application needs to be reloaded..*/
|
||||
def scanDirectories: Seq[Path] = jettyWebappPath :: Nil
|
||||
/** The time in seconds between scans that check whether the web application should be reloaded.*/
|
||||
def scanInterval: Int = 3
|
||||
/** The port that Jetty runs on. */
|
||||
def jettyPort: Int = JettyRun.DefaultPort
|
||||
|
||||
lazy val jettyRestart = jettyStop && jettyRun
|
||||
lazy val jettyStop = jettyStopAction
|
||||
protected def jettyStopAction = jettyStopTask describedAs(JettyStopDescription)
|
||||
|
||||
/** The clean action for a web project is modified so that it first stops jetty if it is running,
|
||||
* since the webapp directory will be removed by the clean.*/
|
||||
override def cleanAction = super.cleanAction dependsOn jettyStop
|
||||
|
||||
/** Redefine the `package` action to make a war file.*/
|
||||
override protected def packageAction = packageTask(descendents(temporaryWarPath ##, "*"), warPath, Nil) dependsOn(prepareWebapp) describedAs PackageWarDescription
|
||||
|
||||
/** Redefine the default main artifact to be a war file.*/
|
||||
override protected def defaultMainArtifact = Artifact(artifactID, "war", "war")
|
||||
}
|
||||
|
||||
object BasicScalaProject
|
||||
{
|
||||
val CleanDescription =
|
||||
"Deletes all generated files (the target directory)."
|
||||
val MainCompileDescription =
|
||||
"Compiles main sources."
|
||||
val TestCompileDescription =
|
||||
"Compiles test sources."
|
||||
val TestDescription =
|
||||
"Runs all tests detected during compilation."
|
||||
val TestOnlyDescription =
|
||||
"Runs the tests provided as arguments."
|
||||
val TestFailedDescription =
|
||||
"Runs the tests provided as arguments if they have not succeeded."
|
||||
val TestQuickDescription =
|
||||
"Runs the tests provided as arguments if they have not succeeded or their dependencies changed."
|
||||
val DocDescription =
|
||||
"Generates API documentation for main Scala source files using scaladoc."
|
||||
val TestDocDescription =
|
||||
"Generates API documentation for test Scala source files using scaladoc."
|
||||
val RunDescription =
|
||||
"Runs the main class for the project with the provided arguments."
|
||||
val ConsoleDescription =
|
||||
"Starts the Scala interpreter with the project classes on the classpath."
|
||||
val ConsoleQuickDescription =
|
||||
"Starts the Scala interpreter with the project classes on the classpath without running compile first."
|
||||
val PackageDescription =
|
||||
"Creates a jar file containing main classes and resources."
|
||||
val TestPackageDescription =
|
||||
"Creates a jar file containing test classes and resources."
|
||||
val DocPackageDescription =
|
||||
"Creates a jar file containing generated API documentation."
|
||||
val SourcePackageDescription =
|
||||
"Creates a jar file containing all main source files and resources."
|
||||
val TestSourcePackageDescription =
|
||||
"Creates a jar file containing all test source files and resources."
|
||||
val ProjectPackageDescription =
|
||||
"Creates a zip file containing the entire project, excluding generated files."
|
||||
val PackageAllDescription =
|
||||
"Executes all package tasks except package-project."
|
||||
val DocAllDescription =
|
||||
"Generates both main and test documentation."
|
||||
val IncrementVersionDescription =
|
||||
"Increments the micro part of the version (the third number) by one. (This is only valid for versions of the form #.#.#-*)"
|
||||
val ReleaseDescription =
|
||||
"Compiles, tests, generates documentation, packages, and increments the version."
|
||||
|
||||
private def warnMultipleMainClasses(log: Logger) =
|
||||
{
|
||||
log.warn("No Main-Class attribute will be added automatically added:")
|
||||
log.warn("Multiple classes with a main method were detected. Specify main class explicitly with:")
|
||||
log.warn(" override mainClass = Some(\"className\")")
|
||||
}
|
||||
private def mapScalaModule(in: Iterable[_], id: String) =
|
||||
{
|
||||
ScalaVersion.current.toList.flatMap { scalaVersion =>
|
||||
in.map(jar => ModuleID(ManageDependencies.ScalaOrganization, id, scalaVersion))
|
||||
}
|
||||
}
|
||||
}
|
||||
object BasicWebScalaProject
|
||||
{
|
||||
val PackageWarDescription =
|
||||
"Creates a war file."
|
||||
val JettyStopDescription =
|
||||
"Stops the Jetty server that was started with the jetty-run action."
|
||||
val JettyRunDescription =
|
||||
"Starts the Jetty server and serves this project as a web application."
|
||||
}
|
||||
/** Analyzes the dependencies of a project after compilation. All methods except `snapshot` return a
|
||||
* `PathFinder`. The underlying calculations are repeated for each call to PathFinder.get. */
|
||||
final class LibraryDependencies(project: Project, conditional: CompileConditional) extends NotNull
|
||||
{
|
||||
/** Library jars located in unmanaged or managed dependency paths.*/
|
||||
def libraries: PathFinder = pathFinder(snapshot.libraries)
|
||||
/** Library jars located outside of the project.*/
|
||||
def external: PathFinder = pathFinder(snapshot.external)
|
||||
/** The Scala library jar.*/
|
||||
def scalaLibrary: PathFinder = pathFinder(snapshot.scalaLibrary)
|
||||
/** The Scala compiler jar.*/
|
||||
def scalaCompiler: PathFinder = pathFinder(snapshot.scalaCompiler)
|
||||
/** All jar dependencies.*/
|
||||
def all: PathFinder = pathFinder(snapshot.all)
|
||||
/** The Scala library and compiler jars.*/
|
||||
def scalaJars: PathFinder = pathFinder(snapshot.scalaJars)
|
||||
|
||||
/** Returns an object that has all analyzed dependency information frozen at the time of this method call. */
|
||||
def snapshot = new Dependencies
|
||||
|
||||
private def rootProjectDirectory = project.rootProject.info.projectPath
|
||||
|
||||
final class Dependencies
|
||||
{
|
||||
import LibraryDependencies._
|
||||
val all = conditional.analysis.allExternals.filter(ClasspathUtilities.isArchive).map(_.getAbsoluteFile)
|
||||
private[this] val (internal, externalAll) = all.toList.partition(jar => Path.relativize(rootProjectDirectory, jar).isDefined)
|
||||
private[this] val (bootScalaJars, librariesNoScala) = internal.partition(isScalaJar)
|
||||
private[this] val (externalScalaJars, externalNoScala) = externalAll.partition(isScalaJar)
|
||||
val scalaJars = externalScalaJars ::: bootScalaJars
|
||||
val (scalaLibrary, scalaCompiler) = scalaJars.partition(isScalaLibraryJar)
|
||||
def external = externalNoScala
|
||||
def libraries = librariesNoScala
|
||||
}
|
||||
|
||||
private def pathFinder(it: => Iterable[File]) = Path.lazyPathFinder(it.map(Path.fromFile))
|
||||
}
|
||||
private object LibraryDependencies
|
||||
{
|
||||
private def ScalaLibraryPrefix = ManageDependencies.ScalaLibraryID
|
||||
private def ScalaCompilerPrefix = ManageDependencies.ScalaCompilerID
|
||||
private def ScalaJarPrefixes = List(ScalaCompilerPrefix, ScalaLibraryPrefix)
|
||||
private def isScalaJar(file: File) = ClasspathUtilities.isArchive(file) && ScalaJarPrefixes.exists(isNamed(file))
|
||||
private def isScalaLibraryJar(file: File) = isNamed(file)(ScalaLibraryPrefix)
|
||||
private def isNamed(file: File)(name: String) = file.getName.startsWith(name)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import java.io.{File, Writer}
|
||||
|
||||
object DotGraph
|
||||
{
|
||||
def apply(analysis: BasicCompileAnalysis, outputDirectory: Path, log: Logger) =
|
||||
{
|
||||
val outputDir = outputDirectory.asFile
|
||||
|
||||
def generateGraph[Key, Value](fileName: String, graphName: String, graph: Iterable[(Key, scala.collection.Set[Value])],
|
||||
keyToString: Key => String, valueToString: Value => String) =
|
||||
{
|
||||
FileUtilities.write(new File(outputDir, fileName), log)
|
||||
{
|
||||
(writer: Writer) =>
|
||||
{
|
||||
def writeLine(line: String) = FileUtilities.writeLine(writer, line)
|
||||
writeLine("digraph " + graphName + " {")
|
||||
for( (dependsOn, dependants) <- graph; dependant <- dependants)
|
||||
writeLine(valueToString(dependant) + " -> " + keyToString(dependsOn))
|
||||
writeLine("}")
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
FileUtilities.createDirectory(outputDir, log) orElse
|
||||
generateGraph(BasicAnalysis.DependenciesFileName, "dependencies", analysis.allDependencies,
|
||||
sourceToString, sourceToString) orElse
|
||||
generateGraph(BasicAnalysis.ExternalDependenciesFileName, "externalDependencies", analysis.allExternalDependencies,
|
||||
fileToString, sourceToString)
|
||||
}
|
||||
private def sourceToString(source: Path) = fileToString(source.asFile)
|
||||
private def fileToString(file: File) =
|
||||
{
|
||||
val rawName = file.getName
|
||||
val name =
|
||||
if(rawName.endsWith(".scala"))
|
||||
rawName.substring(0, rawName.length - ".scala".length)
|
||||
else
|
||||
rawName
|
||||
"\"" + name + "\""
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008 Mark Harrah, David MacIver
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import impl.PropertiesUtilities
|
||||
import scala.reflect.Manifest
|
||||
|
||||
trait Environment
|
||||
{
|
||||
abstract class Property[T] extends NotNull
|
||||
{
|
||||
/** Explicitly sets the value of this property to 'v'.*/
|
||||
def update(v: T): Unit
|
||||
/** Returns the current value of this property or throws an exception if the value could not be obtained.*/
|
||||
def value: T = resolve.value
|
||||
/** Returns the current value of this property in an 'Option'. 'None' is used to indicate that the
|
||||
* value could not obtained.*/
|
||||
def get: Option[T] = resolve.toOption
|
||||
/** Returns full information about this property's current value. */
|
||||
def resolve: PropertyResolution[T]
|
||||
|
||||
def foreach(f: T => Unit): Unit = resolve.foreach(f)
|
||||
}
|
||||
|
||||
/** Creates a system property with the given name and no default value.*/
|
||||
def system[T](propName: String)(implicit format: Format[T]): Property[T]
|
||||
/** Creates a system property with the given name and the given default value to use if no value is explicitly specified.*/
|
||||
def systemOptional[T](propName: String, defaultValue: => T)(implicit format: Format[T]): Property[T]
|
||||
/** Creates a user-defined property that has no default value. The property will try to inherit its value
|
||||
* from a parent environment (if one exists) if its value is not explicitly specified. An explicitly specified
|
||||
* value will persist between builds if the object returned by this method is assigned to a 'val' in this
|
||||
* 'Environment'.*/
|
||||
def property[T](implicit manifest: Manifest[T], format: Format[T]): Property[T]
|
||||
/** Creates a user-defined property that has no default value. The property will try to inherit its value
|
||||
* from a parent environment (if one exists) if its value is not explicitly specified. An explicitly specified
|
||||
* value will persist between builds if the object returned by this method is assigned to a 'val' in this
|
||||
* 'Environment'. The given 'format' is used to convert an instance of 'T' to and from the 'String' representation
|
||||
* used for persistence.*/
|
||||
def propertyF[T](format: Format[T])(implicit manifest: Manifest[T]): Property[T] = property(manifest, format)
|
||||
/** Creates a user-defined property with no default value and no value inheritance from a parent environment.
|
||||
* Its value will persist between builds if the returned object is assigned to a 'val' in this 'Environment'.*/
|
||||
def propertyLocal[T](implicit manifest: Manifest[T], format: Format[T]): Property[T]
|
||||
/** Creates a user-defined property with no default value and no value inheritance from a parent environment.
|
||||
* The property's value will persist between builds if the object returned by this method is assigned to a
|
||||
* 'val' in this 'Environment'. The given 'format' is used to convert an instance of 'T' to and from the
|
||||
* 'String' representation used for persistence.*/
|
||||
def propertyLocalF[T](format: Format[T])(implicit manifest: Manifest[T]): Property[T] = propertyLocal(manifest, format)
|
||||
/** Creates a user-defined property that uses the given default value if no value is explicitly specified for this property. The property's value will persist between builds
|
||||
* if the object returned by this method is assigned to a 'val' in this 'Environment'.*/
|
||||
def propertyOptional[T](defaultValue: => T)(implicit manifest: Manifest[T], format: Format[T]): Property[T]
|
||||
/** Creates a user-defined property with no value inheritance from a parent environment but with the given default
|
||||
* value if no value is explicitly specified for this property. The property's value will persist between builds
|
||||
* if the object returned by this method is assigned to a 'val' in this 'Environment'. The given 'format' is used
|
||||
* to convert an instance of 'T' to and from the 'String' representation used for persistence.*/
|
||||
def propertyOptionalF[T](defaultValue: => T, format: Format[T])(implicit manifest: Manifest[T]): Property[T] =
|
||||
propertyOptional(defaultValue)(manifest, format)
|
||||
}
|
||||
|
||||
import scala.collection.Map
|
||||
trait BasicEnvironment extends Environment
|
||||
{
|
||||
protected def log: Logger
|
||||
/** The location of the properties file that backs the user-defined properties. */
|
||||
def envBackingPath: Path
|
||||
/** The environment from which user-defined properties inherit (if enabled). */
|
||||
protected def parentEnvironment: Option[BasicEnvironment] = None
|
||||
/** The identifier used in messages to refer to this environment. */
|
||||
def environmentLabel = envBackingPath.absolutePath
|
||||
|
||||
private[this] var isModified = false
|
||||
private[sbt] def setEnvironmentModified(modified: Boolean) { synchronized { isModified = modified } }
|
||||
private[this] def isEnvironmentModified = synchronized { isModified }
|
||||
|
||||
|
||||
implicit val IntFormat: Format[Int] = new SimpleFormat[Int] { def fromString(s: String) = java.lang.Integer.parseInt(s) }
|
||||
implicit val LongFormat: Format[Long] = new SimpleFormat[Long] { def fromString(s: String) = java.lang.Long.parseLong(s) }
|
||||
implicit val DoubleFormat: Format[Double] = new SimpleFormat[Double] { def fromString(s: String) = java.lang.Double.parseDouble(s) }
|
||||
implicit val BooleanFormat: Format[Boolean] = new SimpleFormat[Boolean] { def fromString(s: String) = java.lang.Boolean.valueOf(s).booleanValue }
|
||||
implicit val StringFormat: Format[String] = Format.string
|
||||
val NonEmptyStringFormat: Format[String] = new SimpleFormat[String]
|
||||
{
|
||||
def fromString(s: String) =
|
||||
{
|
||||
val trimmed = s.trim
|
||||
if(trimmed.isEmpty)
|
||||
error("The empty string is not allowed.")
|
||||
trimmed
|
||||
}
|
||||
}
|
||||
implicit val VersionFormat: Format[Version] =
|
||||
new SimpleFormat[Version]
|
||||
{
|
||||
def fromString(s: String) = Version.fromString(s).fold(msg => error(msg), x => x)
|
||||
}
|
||||
implicit val FileFormat = Format.file
|
||||
|
||||
|
||||
/** Implementation of 'Property' for user-defined properties. */
|
||||
private[sbt] class UserProperty[T](lazyDefaultValue: => Option[T], format: Format[T], inheritEnabled: Boolean,
|
||||
inheritFirst: Boolean, private[BasicEnvironment] val manifest: Manifest[T]) extends Property[T]
|
||||
{
|
||||
/** The name of this property is used for persistence in the properties file and as an identifier in messages.*/
|
||||
lazy val name = propertyMap.find( p => p._2 eq this ).map(_._1)
|
||||
/** Gets the name of this property or an alternative if the name is not available.*/
|
||||
private def nameString = name.getOrElse("<unnamed>")
|
||||
/** The lazily evaluated default value for this property.*/
|
||||
private lazy val defaultValue = lazyDefaultValue
|
||||
/** The explicitly set value for this property.*/
|
||||
private[BasicEnvironment] var explicitValue =
|
||||
{
|
||||
def initialValue = for(n <- name; stringValue <- initialValues.get(n)) yield format.fromString(stringValue)
|
||||
new LazyVar[Option[T]](initialValue) // ensure propertyMap is initialized before a read occurs
|
||||
}
|
||||
def update(v: T): Unit = synchronized { explicitValue() = Some(v); setEnvironmentModified(true) }
|
||||
def resolve: PropertyResolution[T] =
|
||||
synchronized
|
||||
{
|
||||
if(inheritFirst) resolveInheritFirst
|
||||
else resolveDefaultFirst
|
||||
}
|
||||
private def resolveInheritFirst =
|
||||
explicitValue() match
|
||||
{
|
||||
case Some(v) => DefinedValue(v, false, false)
|
||||
case None =>
|
||||
val inherited = inheritedValue
|
||||
// note that the following means the default value will not be used if an exception occurs inheriting
|
||||
inherited orElse
|
||||
{
|
||||
defaultValue match
|
||||
{
|
||||
case Some(v) => DefinedValue(v, false, true)
|
||||
case None => inherited
|
||||
}
|
||||
}
|
||||
}
|
||||
private def resolveDefaultFirst =
|
||||
(explicitValue() orElse defaultValue) match
|
||||
{
|
||||
case Some(v) => DefinedValue(v, false, explicitValue().isEmpty)
|
||||
case None => inheritedValue
|
||||
}
|
||||
|
||||
private def inheritedValue: PropertyResolution[T] =
|
||||
{
|
||||
val propOption = if(inheritEnabled) parentProperty else None
|
||||
propOption match
|
||||
{
|
||||
case Some(prop) => tryToInherit(prop)
|
||||
case None => UndefinedValue(nameString, environmentLabel)
|
||||
}
|
||||
}
|
||||
private def parentProperty = for(parent <- parentEnvironment; n <- name; prop <- parent.propertyMap.get(n)) yield prop
|
||||
|
||||
private def tryToInherit[R](prop: BasicEnvironment#UserProperty[R]): PropertyResolution[T] =
|
||||
{
|
||||
if(prop.manifest <:< manifest)
|
||||
markInherited(prop.resolve.asInstanceOf[PropertyResolution[T]])
|
||||
else
|
||||
ResolutionException("Could not inherit property '" + nameString + "' from '" + environmentLabel + "':\n" +
|
||||
"\t Property had type " + prop.manifest + ", expected type " + manifest, None)
|
||||
}
|
||||
private def markInherited(result: PropertyResolution[T]) =
|
||||
result match
|
||||
{
|
||||
case DefinedValue(v, isInherited, isDefault) => DefinedValue(v, true, isDefault)
|
||||
case x => x
|
||||
}
|
||||
|
||||
override def toString = nameString + "=" + resolve
|
||||
|
||||
/** Gets the explicitly set value converted to a 'String'.*/
|
||||
private[sbt] def getStringValue: Option[String] = explicitValue().map(format.toString)
|
||||
/** Explicitly sets the value for this property by converting the given string value.*/
|
||||
private[sbt] def setStringValue(s: String) { update(format.fromString(s)) }
|
||||
}
|
||||
/** Implementation of 'Property' for system properties (i.e. System.getProperty/setProperty) */
|
||||
private class SystemProperty[T](val name: String, lazyDefaultValue: => Option[T], val format: Format[T]) extends Property[T]
|
||||
{
|
||||
def resolve =
|
||||
{
|
||||
val rawValue = System.getProperty(name)
|
||||
if(rawValue == null)
|
||||
notFound
|
||||
else
|
||||
{
|
||||
Control.convertException(format.fromString(rawValue)) match
|
||||
{
|
||||
case Left(e) => ResolutionException("Error parsing system property '" + name + "': " + e.toString, Some(e))
|
||||
case Right(x) => DefinedValue(x, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Handles resolution when the property has no explicit value. If there is a default value, that is returned,
|
||||
* otherwise, UndefinedValue is returned.*/
|
||||
private def notFound =
|
||||
{
|
||||
defaultValue match
|
||||
{
|
||||
case Some(dv) =>
|
||||
{
|
||||
log.debug("System property '" + name + "' does not exist, using provided default.")
|
||||
DefinedValue(dv, false, true)
|
||||
}
|
||||
case None => UndefinedValue(name, environmentLabel)
|
||||
}
|
||||
}
|
||||
protected lazy val defaultValue = lazyDefaultValue
|
||||
def update(t: T)
|
||||
{
|
||||
for(e <- Control.convertException(System.setProperty(name, format.toString(t))).left)
|
||||
{
|
||||
log.trace(e)
|
||||
log.warn("Error setting system property '" + name + "': " + e.toString)
|
||||
}
|
||||
}
|
||||
override def toString = name + "=" + resolve
|
||||
}
|
||||
|
||||
def system[T](propertyName: String)(implicit format: Format[T]): Property[T] =
|
||||
new SystemProperty[T](propertyName, None, format)
|
||||
def systemOptional[T](propertyName: String, defaultValue: => T)(implicit format: Format[T]): Property[T] =
|
||||
new SystemProperty[T](propertyName, Some(defaultValue), format)
|
||||
|
||||
def property[T](implicit manifest: Manifest[T], format: Format[T]): Property[T] =
|
||||
new UserProperty[T](None, format, true, false, manifest)
|
||||
def propertyLocal[T](implicit manifest: Manifest[T], format: Format[T]): Property[T] =
|
||||
new UserProperty[T](None, format, false, false, manifest)
|
||||
def propertyOptional[T](defaultValue: => T)(implicit manifest: Manifest[T], format: Format[T]): Property[T] =
|
||||
propertyOptional(defaultValue, false)(manifest, format)
|
||||
def propertyOptional[T](defaultValue: => T, inheritFirst: Boolean)(implicit manifest: Manifest[T], format: Format[T]): Property[T] =
|
||||
new UserProperty[T](Some(defaultValue), format, true, inheritFirst, manifest)
|
||||
|
||||
private type AnyUserProperty = UserProperty[_]
|
||||
/** Maps property name to property. The map is constructed by reflecting vals defined on this object,
|
||||
* so it should not be referenced during initialization or else subclass properties will be missed.**/
|
||||
private lazy val propertyMap: Map[String, AnyUserProperty] =
|
||||
{
|
||||
log.debug("Discovering properties")
|
||||
val propertyMap = new scala.collection.mutable.HashMap[String, AnyUserProperty]
|
||||
// AnyProperty is required because the return type of the property*[T] methods is Property[T]
|
||||
// and so the vals we are looking for have type Property[T] and not UserProperty[T]
|
||||
// We then only keep instances of UserProperty
|
||||
val vals = Environment.reflectiveMappings(this, classOf[Property[_]])
|
||||
for( (name, property: AnyUserProperty) <- vals)
|
||||
propertyMap(name) = property
|
||||
propertyMap.readOnly
|
||||
}
|
||||
private val initialValues: Map[String, String] =
|
||||
{
|
||||
val map = new scala.collection.mutable.HashMap[String, String]
|
||||
for(errorMsg <- impl.MapUtilities.read(map, envBackingPath, log))
|
||||
log.error("Error loading properties from " + environmentLabel + " : " + errorMsg)
|
||||
map.readOnly
|
||||
}
|
||||
|
||||
def propertyNames: Iterable[String] = propertyMap.keys.toList
|
||||
def getPropertyNamed(name: String): Option[UserProperty[_]] = propertyMap.get(name)
|
||||
def propertyNamed(name: String): UserProperty[_] = propertyMap(name)
|
||||
def saveEnvironment(): Option[String] =
|
||||
{
|
||||
if(isEnvironmentModified)
|
||||
{
|
||||
val properties = new java.util.Properties
|
||||
for( (name, variable) <- propertyMap; stringValue <- variable.getStringValue)
|
||||
properties.setProperty(name, stringValue)
|
||||
val result = PropertiesUtilities.write(properties, "Project properties", envBackingPath, log)
|
||||
setEnvironmentModified(false)
|
||||
result
|
||||
}
|
||||
else
|
||||
None
|
||||
}
|
||||
private[sbt] def uninitializedProperties: Iterable[(String, Property[_])] = propertyMap.filter(_._2.get.isEmpty)
|
||||
}
|
||||
private object Environment
|
||||
{
|
||||
def reflectiveMappings[T](obj: AnyRef, clazz: Class[T]): Map[String, T] =
|
||||
{
|
||||
val mappings = new scala.collection.mutable.OpenHashMap[String, T]
|
||||
for ((name, value) <- ReflectUtilities.allValsC(obj, clazz))
|
||||
mappings(ReflectUtilities.transformCamelCase(name, '.')) = value
|
||||
mappings
|
||||
}
|
||||
}
|
||||
|
||||
sealed trait PropertyResolution[+T] extends NotNull
|
||||
{
|
||||
def value: T
|
||||
def orElse[R >: T](r: => PropertyResolution[R]): PropertyResolution[R]
|
||||
def toOption: Option[T]
|
||||
def foreach(f: T => Unit): Unit
|
||||
def map[R](f: T => R): PropertyResolution[R]
|
||||
def flatMap[R](f: T => PropertyResolution[R]): PropertyResolution[R]
|
||||
}
|
||||
sealed trait NoPropertyValue extends PropertyResolution[Nothing]
|
||||
{ self: RuntimeException with PropertyResolution[Nothing] =>
|
||||
|
||||
def value = throw this
|
||||
def toOption = None
|
||||
def map[R](f: Nothing => R): PropertyResolution[R] = this
|
||||
def flatMap[R](f: Nothing => PropertyResolution[R]): PropertyResolution[R] = this
|
||||
def foreach(f: Nothing => Unit) {}
|
||||
}
|
||||
final case class ResolutionException(message: String, exception: Option[Throwable])
|
||||
extends RuntimeException(message, exception.getOrElse(null)) with NoPropertyValue
|
||||
{
|
||||
def orElse[R](r: => PropertyResolution[R]) = this
|
||||
}
|
||||
final case class UndefinedValue(name: String, environmentLabel: String)
|
||||
extends RuntimeException("Value for property '" + name + "' from " + environmentLabel + " is undefined.") with NoPropertyValue
|
||||
{
|
||||
def orElse[R](r: => PropertyResolution[R]) =
|
||||
r match
|
||||
{
|
||||
case u: UndefinedValue => this
|
||||
case _ => r
|
||||
}
|
||||
}
|
||||
final case class DefinedValue[T](value: T, isInherited: Boolean, isDefault: Boolean) extends PropertyResolution[T]
|
||||
{
|
||||
def toOption = Some(value)
|
||||
def orElse[R >: T](r: => PropertyResolution[R]) = this
|
||||
def map[R](f: T => R) = DefinedValue[R](f(value), isInherited, isDefault)
|
||||
def flatMap[R](f: T => PropertyResolution[R]) = f(value)
|
||||
def foreach(f: T => Unit) { f(value) }
|
||||
}
|
||||
private final class LazyVar[T](initialValue: => T) extends NotNull
|
||||
{
|
||||
private[this] var value: Option[T] = None
|
||||
def apply() =
|
||||
synchronized
|
||||
{
|
||||
value match
|
||||
{
|
||||
case Some(v) => v
|
||||
case None =>
|
||||
val newValue = initialValue
|
||||
value = Some(newValue)
|
||||
newValue
|
||||
}
|
||||
}
|
||||
def update(newValue: T) = synchronized { value = Some(newValue) }
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
/** Defines a function to call as sbt exits.*/
|
||||
trait ExitHook extends NotNull
|
||||
{
|
||||
/** Provides a name for this hook to be used to provide feedback to the user. */
|
||||
def name: String
|
||||
/** Subclasses should implement this method, which is called when this hook is executed. */
|
||||
def runBeforeExiting(): Unit
|
||||
}
|
||||
|
||||
object ExitHooks
|
||||
{
|
||||
/** This is a list of hooks to call when sbt is finished executing.*/
|
||||
private val exitHooks = new scala.collection.mutable.HashSet[ExitHook]
|
||||
/** Adds a hook to call before sbt exits. */
|
||||
private[sbt] def register(hook: ExitHook) { exitHooks += hook }
|
||||
/** Removes a hook. */
|
||||
private[sbt] def unregister(hook: ExitHook) { exitHooks -= hook }
|
||||
/** Calls each registered exit hook, trapping any exceptions so that each hook is given a chance to run. */
|
||||
private[sbt] def runExitHooks(log: Logger)
|
||||
{
|
||||
for(hook <- exitHooks.toList)
|
||||
{
|
||||
try
|
||||
{
|
||||
log.debug("Running exit hook '" + hook.name + "'...")
|
||||
hook.runBeforeExiting()
|
||||
}
|
||||
catch
|
||||
{
|
||||
case e =>
|
||||
{
|
||||
log.trace(e);
|
||||
log.error("Error running exit hook '" + hook.name + "': " + e.toString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import scala.collection.{mutable, Map, Set}
|
||||
|
||||
sealed trait ProductsSources extends NotNull
|
||||
{
|
||||
def products: Iterable[Path]
|
||||
def sources: Iterable[Path]
|
||||
}
|
||||
sealed trait ProductsWrapper extends NotNull
|
||||
{
|
||||
def from(sources: => Iterable[Path]): ProductsSources = from(Path.lazyPathFinder(sources))
|
||||
def from(sources: PathFinder): ProductsSources
|
||||
}
|
||||
/** Provides methods to define tasks with basic conditional execution based on the sources
|
||||
* and products of the task. */
|
||||
trait FileTasks extends Project
|
||||
{
|
||||
implicit def wrapProduct(product: => Path): ProductsWrapper = FileTasks.wrapProduct(product)
|
||||
implicit def wrapProducts(productsList: => Iterable[Path]): ProductsWrapper = FileTasks.wrapProducts(productsList)
|
||||
/** Runs 'action' if the given products are out of date with respect to the given sources. */
|
||||
def fileTask(label: String, files: ProductsSources)(action: => Option[String]): Task =
|
||||
task { FileTasks.runOption(label, files, log)(action) }
|
||||
/** Runs 'action' if any of the given products do not exist. */
|
||||
def fileTask(label: String, products: => Iterable[Path])(action: => Option[String]): Task =
|
||||
task { FileTasks.existenceCheck[Option[String]](label, products, log)(action)(None) }
|
||||
|
||||
/** Creates a new task that performs 'action' only when the given products are out of date with respect to the given sources.. */
|
||||
def fileTask(files: ProductsSources)(action: => Option[String]): Task = fileTask("", files)(action)
|
||||
/** Creates a new task that performs 'action' only when at least one of the given products does not exist.. */
|
||||
def fileTask(products: => Iterable[Path])(action: => Option[String]): Task = fileTask("", products)(action)
|
||||
|
||||
}
|
||||
object FileTasks
|
||||
{
|
||||
implicit def wrapProduct(product: => Path): ProductsWrapper = wrapProducts(product :: Nil)
|
||||
implicit def wrapProducts(productsList: => Iterable[Path]): ProductsWrapper =
|
||||
new ProductsWrapper
|
||||
{
|
||||
def from(sourceFinder: PathFinder) =
|
||||
new ProductsSources
|
||||
{
|
||||
def products = productsList
|
||||
def sources = sourceFinder.get
|
||||
}
|
||||
}
|
||||
/** Runs 'ifOutofdate' if the given products are out of date with respect to the given sources.*/
|
||||
def runOption(label: String, files: ProductsSources, log: Logger)(ifOutofdate: => Option[String]): Option[String] =
|
||||
{
|
||||
val result = apply[Option[String]](label, files, log)(ifOutofdate)(None)
|
||||
if(result.isDefined)
|
||||
FileUtilities.clean(files.products, true, log)
|
||||
result
|
||||
}
|
||||
/** Returns 'ifOutofdate' if the given products are out of date with respect to the given sources. Otherwise, returns ifUptodate. */
|
||||
def apply[T](label: String, files: ProductsSources, log: Logger)(ifOutofdate: => T)(ifUptodate: => T): T =
|
||||
{
|
||||
val products = files.products
|
||||
existenceCheck[T](label, products, log)(ifOutofdate)
|
||||
{
|
||||
val sources = files.sources
|
||||
if(sources.isEmpty)
|
||||
{
|
||||
log.debug("Running " + label + " task because no sources exist.")
|
||||
ifOutofdate
|
||||
}
|
||||
else
|
||||
{
|
||||
val oldestProductModifiedTime = mapLastModified(products).reduceLeft(_ min _)
|
||||
val newestSourceModifiedTime = mapLastModified(sources).reduceLeft(_ max _)
|
||||
if(oldestProductModifiedTime < newestSourceModifiedTime)
|
||||
{
|
||||
if(log.atLevel(Level.Debug))
|
||||
{
|
||||
log.debug("Running " + label + " task because the following sources are newer than at least one product: ")
|
||||
logDebugIndented(sources.filter(_.lastModified > oldestProductModifiedTime), log)
|
||||
log.debug(" The following products are older than at least one source: ")
|
||||
logDebugIndented(products.filter(_.lastModified < newestSourceModifiedTime), log)
|
||||
}
|
||||
ifOutofdate
|
||||
}
|
||||
else
|
||||
ifUptodate
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Checks that all 'products' exist. If they do, 'ifAllExists' is returned, otherwise 'products' is returned.*/
|
||||
private def existenceCheck[T](label: String, products: Iterable[Path], log: Logger)(action: => T)(ifAllExist: => T) =
|
||||
{
|
||||
val nonexisting = products.filter(!_.exists)
|
||||
if(nonexisting.isEmpty)
|
||||
ifAllExist
|
||||
else
|
||||
{
|
||||
if(log.atLevel(Level.Debug))
|
||||
{
|
||||
log.debug("Running " + label + " task because at least one product does not exist:")
|
||||
logDebugIndented(nonexisting, log)
|
||||
}
|
||||
action
|
||||
}
|
||||
}
|
||||
private def logDebugIndented[T](it: Iterable[T], log: Logger) { it.foreach(x => log.debug("\t" + x)) }
|
||||
private def mapLastModified(paths: Iterable[Path]): Iterable[Long] = paths.map(_.lastModified)
|
||||
}
|
||||
|
|
@ -0,0 +1,892 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah, Nathan Hamblen
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import java.io.{Closeable, File, FileInputStream, FileOutputStream, InputStream, OutputStream}
|
||||
import java.io.{ByteArrayOutputStream, InputStreamReader, OutputStreamWriter}
|
||||
import java.io.{BufferedReader, BufferedWriter, FileReader, FileWriter, Reader, Writer}
|
||||
import java.util.zip.{GZIPInputStream, GZIPOutputStream}
|
||||
import java.net.URL
|
||||
import java.nio.charset.{Charset, CharsetDecoder, CharsetEncoder}
|
||||
import java.nio.channels.FileChannel
|
||||
import java.util.jar.{Attributes, JarEntry, JarFile, JarInputStream, JarOutputStream, Manifest}
|
||||
import java.util.zip.{GZIPOutputStream, ZipEntry, ZipFile, ZipInputStream, ZipOutputStream}
|
||||
|
||||
import OpenResource._
|
||||
|
||||
final class Preserved private[sbt](toRestore: scala.collection.Map[File, Path], temp: File) extends NotNull
|
||||
{
|
||||
def restore(log: Logger) =
|
||||
{
|
||||
try
|
||||
{
|
||||
Control.lazyFold(toRestore.toList) { case (src, dest) =>
|
||||
FileUtilities.copyFile(src, dest.asFile, log)
|
||||
}
|
||||
}
|
||||
finally { FileUtilities.clean(Path.fromFile(temp) :: Nil, true, log) }
|
||||
}
|
||||
}
|
||||
|
||||
/** A collection of file related methods. */
|
||||
object FileUtilities
|
||||
{
|
||||
import wrap.Wrappers.readOnly
|
||||
/** The size of the byte or char buffer used in various methods.*/
|
||||
private val BufferSize = 8192
|
||||
private val Newline = System.getProperty("line.separator")
|
||||
/** A pattern used to split a String by path separator characters.*/
|
||||
private val PathSeparatorPattern = java.util.regex.Pattern.compile(File.pathSeparator)
|
||||
|
||||
/** Splits a String around path separator characters. */
|
||||
private[sbt] def pathSplit(s: String) = PathSeparatorPattern.split(s)
|
||||
|
||||
def preserve(paths: Iterable[Path], log: Logger): Either[String, Preserved] =
|
||||
{
|
||||
for(tmp <- createTemporaryDirectory(log).right) yield
|
||||
{
|
||||
val pathMap = new scala.collection.mutable.HashMap[File, Path]
|
||||
val destinationDirectory = Path.fromFile(tmp)
|
||||
for(source <- paths)
|
||||
{
|
||||
val toPath = Path.fromString(destinationDirectory, source.relativePath)
|
||||
copyFile(source, toPath, log)
|
||||
pathMap(toPath.asFile) = source
|
||||
}
|
||||
new Preserved(readOnly(pathMap), tmp)
|
||||
}
|
||||
}
|
||||
|
||||
/** Gzips the file 'in' and writes it to 'out'. 'in' cannot be the same file as 'out'. */
|
||||
def gzip(in: Path, out: Path, log: Logger): Option[String] =
|
||||
{
|
||||
require(in != out, "Input file cannot be the same as the output file.")
|
||||
readStream(in.asFile, log) { inputStream =>
|
||||
writeStream(out.asFile, log) { outputStream =>
|
||||
gzip(inputStream, outputStream, log)
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Gzips the InputStream 'in' and writes it to 'output'. Neither stream is closed.*/
|
||||
def gzip(input: InputStream, output: OutputStream, log: Logger): Option[String] =
|
||||
gzipOutputStream.ioOption(output, "gzipping", log) { gzStream => transfer(input, gzStream, log) }
|
||||
|
||||
def gunzip(input: InputStream, output: OutputStream, log: Logger): Option[String] =
|
||||
gzipInputStream.ioOption(input, "gunzipping", log) { gzStream => transfer(gzStream, output, log) }
|
||||
/** Gunzips the file 'in' and writes it to 'out'. 'in' cannot be the same file as 'out'. */
|
||||
def gunzip(in: Path, out: Path, log: Logger): Option[String] =
|
||||
{
|
||||
require(in != out, "Input file cannot be the same as the output file.")
|
||||
readStream(in.asFile, log) { inputStream =>
|
||||
writeStream(out.asFile, log) { outputStream =>
|
||||
gunzip(inputStream, outputStream, log)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates a jar file.
|
||||
* @param sources The files to include in the jar file. The path used for the jar is
|
||||
* relative to the base directory for the source. That is, the path in the jar for source
|
||||
* <code>(basePath ##) / x / y</code> is <code>x / y</code>.
|
||||
* @param outputJar The file to write the jar to.
|
||||
* @param manifest The manifest for the jar.
|
||||
* @param recursive If true, any directories in <code>sources</code> are recursively processed. Otherwise,
|
||||
* they are not
|
||||
* @param log The Logger to use. */
|
||||
def jar(sources: Iterable[Path], outputJar: Path, manifest: Manifest, recursive: Boolean, log: Logger) =
|
||||
archive(sources, outputJar, Some(manifest), recursive, log)
|
||||
@deprecated def pack(sources: Iterable[Path], outputJar: Path, manifest: Manifest, recursive: Boolean, log: Logger) =
|
||||
jar(sources, outputJar, manifest, recursive, log)
|
||||
/** Creates a zip file.
|
||||
* @param sources The files to include in the jar file. The path used for the jar is
|
||||
* relative to the base directory for the source. That is, the path in the jar for source
|
||||
* <code>(basePath ##) / x / y</code> is <code>x / y</code>.
|
||||
* @param outputZip The file to write the zip to.
|
||||
* @param recursive If true, any directories in <code>sources</code> are recursively processed. Otherwise,
|
||||
* they are not
|
||||
* @param log The Logger to use. */
|
||||
def zip(sources: Iterable[Path], outputZip: Path, recursive: Boolean, log: Logger) =
|
||||
archive(sources, outputZip, None, recursive, log)
|
||||
|
||||
private def archive(sources: Iterable[Path], outputPath: Path, manifest: Option[Manifest], recursive: Boolean, log: Logger) =
|
||||
{
|
||||
log.info("Packaging " + outputPath + " ...")
|
||||
val outputFile = outputPath.asFile
|
||||
if(outputFile.isDirectory)
|
||||
Some("Specified output file " + outputFile + " is a directory.")
|
||||
else
|
||||
{
|
||||
val outputDir = outputFile.getParentFile
|
||||
val result = createDirectory(outputDir, log) orElse
|
||||
withZipOutput(outputFile, manifest, log)
|
||||
{ output =>
|
||||
val createEntry: (String => ZipEntry) = if(manifest.isDefined) new JarEntry(_) else new ZipEntry(_)
|
||||
writeZip(sources, output, recursive, log)(createEntry)
|
||||
}
|
||||
if(result.isEmpty)
|
||||
log.info("Packaging complete.")
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
private def writeZip(sources: Iterable[Path], output: ZipOutputStream, recursive: Boolean, log: Logger)(createEntry: String => ZipEntry) =
|
||||
{
|
||||
def add(source: Path)
|
||||
{
|
||||
val sourceFile = source.asFile
|
||||
if(sourceFile.isDirectory)
|
||||
{
|
||||
if(recursive)
|
||||
wrapNull(sourceFile.listFiles).foreach(file => add(source / file.getName))
|
||||
}
|
||||
else if(sourceFile.exists)
|
||||
{
|
||||
val relativePath = source.relativePathString("/")
|
||||
log.debug("\tAdding " + source + " as " + relativePath + " ...")
|
||||
val nextEntry = createEntry(relativePath)
|
||||
nextEntry.setTime(sourceFile.lastModified)
|
||||
output.putNextEntry(nextEntry)
|
||||
transferAndClose(new FileInputStream(sourceFile), output, log)
|
||||
}
|
||||
else
|
||||
log.warn("\tSource " + source + " does not exist.")
|
||||
}
|
||||
sources.foreach(add)
|
||||
output.closeEntry()
|
||||
None
|
||||
}
|
||||
|
||||
private def withZipOutput(file: File, manifest: Option[Manifest], log: Logger)(f: ZipOutputStream => Option[String]): Option[String] =
|
||||
{
|
||||
writeStream(file, log)
|
||||
{
|
||||
fileOut =>
|
||||
{
|
||||
val (zipOut, ext) =
|
||||
manifest match
|
||||
{
|
||||
case Some(mf) =>
|
||||
{
|
||||
import Attributes.Name.MANIFEST_VERSION
|
||||
val main = mf.getMainAttributes
|
||||
if(!main.containsKey(MANIFEST_VERSION))
|
||||
main.put(MANIFEST_VERSION, "1.0")
|
||||
(new JarOutputStream(fileOut, mf), "jar")
|
||||
}
|
||||
case None => (new ZipOutputStream(fileOut), "zip")
|
||||
}
|
||||
Control.trapUnitAndFinally("Error writing " + ext + ": ", log)
|
||||
{ f(zipOut) } { zipOut.close }
|
||||
}
|
||||
}
|
||||
}
|
||||
import scala.collection.Set
|
||||
/** Unzips the contents of the zip file <code>from</code> to the <code>toDirectory</code> directory.*/
|
||||
def unzip(from: Path, toDirectory: Path, log: Logger): Either[String, Set[Path]] =
|
||||
unzip(from, toDirectory, AllPassFilter, log)
|
||||
/** Unzips the contents of the zip file <code>from</code> to the <code>toDirectory</code> directory.*/
|
||||
def unzip(from: File, toDirectory: Path, log: Logger): Either[String, Set[Path]] =
|
||||
unzip(from, toDirectory, AllPassFilter, log)
|
||||
/** Unzips the contents of the zip file <code>from</code> to the <code>toDirectory</code> directory.*/
|
||||
def unzip(from: InputStream, toDirectory: Path, log: Logger): Either[String, Set[Path]] =
|
||||
unzip(from, toDirectory, AllPassFilter, log)
|
||||
/** Unzips the contents of the zip file <code>from</code> to the <code>toDirectory</code> directory.*/
|
||||
def unzip(from: URL, toDirectory: Path, log: Logger): Either[String, Set[Path]] =
|
||||
unzip(from, toDirectory, AllPassFilter, log)
|
||||
|
||||
/** Unzips the contents of the zip file <code>from</code> to the <code>toDirectory</code> directory.
|
||||
* Only the entries that match the given filter are extracted. */
|
||||
def unzip(from: Path, toDirectory: Path, filter: NameFilter, log: Logger): Either[String, Set[Path]] =
|
||||
unzip(from.asFile, toDirectory, filter, log)
|
||||
/** Unzips the contents of the zip file <code>from</code> to the <code>toDirectory</code> directory.
|
||||
* Only the entries that match the given filter are extracted. */
|
||||
def unzip(from: File, toDirectory: Path, filter: NameFilter, log: Logger): Either[String, Set[Path]] =
|
||||
readStreamValue(from, log)(in => unzip(in, toDirectory, filter, log))
|
||||
/** Unzips the contents of the zip file <code>from</code> to the <code>toDirectory</code> directory.
|
||||
* Only the entries that match the given filter are extracted. */
|
||||
def unzip(from: URL, toDirectory: Path, filter: NameFilter, log: Logger): Either[String, Set[Path]] =
|
||||
readStreamValue(from, log) { stream => unzip(stream, toDirectory, filter, log) }
|
||||
/** Unzips the contents of the zip file <code>from</code> to the <code>toDirectory</code> directory.
|
||||
* Only the entries that match the given filter are extracted. */
|
||||
def unzip(from: InputStream, toDirectory: Path, filter: NameFilter, log: Logger): Either[String, Set[Path]] =
|
||||
{
|
||||
createDirectory(toDirectory, log) match
|
||||
{
|
||||
case Some(err) => Left(err)
|
||||
case None => zipInputStream.io(from, "unzipping", log) { zipInput => extract(zipInput, toDirectory, filter, log) }
|
||||
}
|
||||
}
|
||||
private def extract(from: ZipInputStream, toDirectory: Path, filter: NameFilter, log: Logger) =
|
||||
{
|
||||
val set = new scala.collection.mutable.HashSet[Path]
|
||||
def next(): Option[String] =
|
||||
{
|
||||
val entry = from.getNextEntry
|
||||
if(entry == null)
|
||||
None
|
||||
else
|
||||
{
|
||||
val name = entry.getName
|
||||
val result =
|
||||
if(filter.accept(name))
|
||||
{
|
||||
val target = Path.fromString(toDirectory, name)
|
||||
log.debug("Extracting zip entry '" + name + "' to '" + target + "'")
|
||||
val result =
|
||||
if(entry.isDirectory)
|
||||
createDirectory(target, log)
|
||||
else
|
||||
{
|
||||
set += target
|
||||
writeStream(target.asFile, log) { out => FileUtilities.transfer(from, out, log) }
|
||||
}
|
||||
//target.asFile.setLastModified(entry.getTime)
|
||||
result
|
||||
}
|
||||
else
|
||||
{
|
||||
log.debug("Ignoring zip entry '" + name + "'")
|
||||
None
|
||||
}
|
||||
from.closeEntry()
|
||||
result match { case None => next(); case x => x }
|
||||
}
|
||||
}
|
||||
next().toLeft(readOnly(set))
|
||||
}
|
||||
|
||||
/** Copies all bytes from the given input stream to the given output stream.
|
||||
* Neither stream is closed.*/
|
||||
def transfer(in: InputStream, out: OutputStream, log: Logger): Option[String] =
|
||||
transferImpl(in, out, false, log)
|
||||
/** Copies all bytes from the given input stream to the given output stream. The
|
||||
* input stream is closed after the method completes.*/
|
||||
def transferAndClose(in: InputStream, out: OutputStream, log: Logger): Option[String] =
|
||||
transferImpl(in, out, true, log)
|
||||
private def transferImpl(in: InputStream, out: OutputStream, close: Boolean, log: Logger): Option[String] =
|
||||
{
|
||||
Control.trapUnitAndFinally("Error during transfer: ", log)
|
||||
{
|
||||
val buffer = new Array[Byte](BufferSize)
|
||||
def read: None.type =
|
||||
{
|
||||
val byteCount = in.read(buffer)
|
||||
if(byteCount >= 0)
|
||||
{
|
||||
out.write(buffer, 0, byteCount)
|
||||
read
|
||||
}
|
||||
else
|
||||
None
|
||||
}
|
||||
read
|
||||
}
|
||||
{ if(close) in.close }
|
||||
}
|
||||
|
||||
/** Creates a file at the given location.*/
|
||||
def touch(path: Path, log: Logger): Option[String] = touch(path.asFile, log)
|
||||
/** Creates a file at the given location.*/
|
||||
def touch(file: File, log: Logger): Option[String] =
|
||||
{
|
||||
Control.trapUnit("Could not create file " + file + ": ", log)
|
||||
{
|
||||
if(file.exists)
|
||||
{
|
||||
def updateFailBase = "Could not update last modified for file " + file
|
||||
Control.trapUnit(updateFailBase + ": ", log)
|
||||
{ if(file.setLastModified(System.currentTimeMillis)) None else Some(updateFailBase) }
|
||||
}
|
||||
else
|
||||
createDirectory(file.getParentFile, log) orElse { file.createNewFile(); None }
|
||||
}
|
||||
}
|
||||
/** Creates a directory at the given location.*/
|
||||
def createDirectory(dir: Path, log: Logger): Option[String] = createDirectory(dir.asFile, log)
|
||||
/** Creates a directory at the given location.*/
|
||||
def createDirectory(dir: File, log: Logger): Option[String] =
|
||||
{
|
||||
Control.trapUnit("Could not create directory " + dir + ": ", log)
|
||||
{
|
||||
if(dir.exists)
|
||||
{
|
||||
if(dir.isDirectory)
|
||||
None
|
||||
else
|
||||
Some(dir + " exists and is not a directory.")
|
||||
}
|
||||
else
|
||||
{
|
||||
dir.mkdirs()
|
||||
log.debug("Created directory " + dir)
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Creates directories at the given locations.*/
|
||||
def createDirectories(d: Seq[Path], log: Logger): Option[String] = createDirectories(d.toList.map(_.asFile), log)
|
||||
/** Creates directories at the given locations.*/
|
||||
def createDirectories(d: List[File], log: Logger): Option[String] =
|
||||
d match
|
||||
{
|
||||
case Nil => None
|
||||
case head :: tail => createDirectory(head, log) orElse createDirectories(tail, log)
|
||||
}
|
||||
/** The maximum number of times a unique temporary filename is attempted to be created.*/
|
||||
private val MaximumTries = 10
|
||||
/** Creates a temporary directory and returns it.*/
|
||||
def createTemporaryDirectory(log: Logger): Either[String, File] =
|
||||
{
|
||||
def create(tries: Int): Either[String, File] =
|
||||
{
|
||||
if(tries > MaximumTries)
|
||||
Left("Could not create temporary directory.")
|
||||
else
|
||||
{
|
||||
val randomName = "sbt_" + java.lang.Integer.toHexString(random.nextInt)
|
||||
val f = new File(temporaryDirectory, randomName)
|
||||
|
||||
if(createDirectory(f, log).isEmpty)
|
||||
Right(f)
|
||||
else
|
||||
create(tries + 1)
|
||||
}
|
||||
}
|
||||
create(0)
|
||||
}
|
||||
|
||||
def withTemporaryDirectory(log: Logger)(action: File => Option[String]): Option[String] =
|
||||
doInTemporaryDirectory(log: Logger)(file => action(file).toLeft(())).left.toOption
|
||||
/** Creates a temporary directory and provides its location to the given function. The directory
|
||||
* is deleted after the function returns.*/
|
||||
def doInTemporaryDirectory[T](log: Logger)(action: File => Either[String, T]): Either[String, T] =
|
||||
{
|
||||
def doInDirectory(dir: File): Either[String, T] =
|
||||
{
|
||||
Control.trapAndFinally("", log)
|
||||
{ action(dir) }
|
||||
{ delete(dir, true, log) }
|
||||
}
|
||||
createTemporaryDirectory(log).right.flatMap(doInDirectory)
|
||||
}
|
||||
def withTemporaryFile[T](log: Logger, prefix: String, postfix: String)(action: File => Either[String, T]): Either[String, T] =
|
||||
{
|
||||
Control.trap("Error creating temporary file: ", log)
|
||||
{
|
||||
val file = File.createTempFile(prefix, postfix)
|
||||
Control.trapAndFinally("", log)
|
||||
{ action(file) }
|
||||
{ file.delete() }
|
||||
}
|
||||
}
|
||||
|
||||
/** Copies the files declared in <code>sources</code> to the <code>destinationDirectory</code>
|
||||
* directory. The source directory hierarchy is flattened so that all copies are immediate
|
||||
* children of <code>destinationDirectory</code>. Directories are not recursively entered.*/
|
||||
def copyFlat(sources: Iterable[Path], destinationDirectory: Path, log: Logger) =
|
||||
{
|
||||
val targetSet = new scala.collection.mutable.HashSet[Path]
|
||||
copyImpl(sources, destinationDirectory, log)
|
||||
{
|
||||
source =>
|
||||
{
|
||||
val from = source.asFile
|
||||
val toPath = destinationDirectory / from.getName
|
||||
targetSet += toPath
|
||||
val to = toPath.asFile
|
||||
if(!to.exists || from.lastModified > to.lastModified && !from.isDirectory)
|
||||
{
|
||||
log.debug("Copying " + source + " to " + toPath)
|
||||
copyFile(from, to, log)
|
||||
}
|
||||
else
|
||||
None
|
||||
}
|
||||
}.toLeft(readOnly(targetSet))
|
||||
}
|
||||
private def copyImpl(sources: Iterable[Path], destinationDirectory: Path, log: Logger)
|
||||
(doCopy: Path => Option[String]): Option[String] =
|
||||
{
|
||||
val target = destinationDirectory.asFile
|
||||
val creationError =
|
||||
if(target.isDirectory)
|
||||
None
|
||||
else
|
||||
createDirectory(target, log)
|
||||
def copy(sources: List[Path]): Option[String] =
|
||||
{
|
||||
sources match
|
||||
{
|
||||
case src :: remaining =>
|
||||
{
|
||||
doCopy(src) match
|
||||
{
|
||||
case None => copy(remaining)
|
||||
case error => error
|
||||
}
|
||||
}
|
||||
case Nil => None
|
||||
}
|
||||
}
|
||||
creationError orElse ( Control.trapUnit("", log) { copy(sources.toList) } )
|
||||
}
|
||||
/** Retrieves the content of the given URL and writes it to the given File. */
|
||||
def download(url: URL, to: File, log: Logger) =
|
||||
{
|
||||
readStream(url, log) { inputStream =>
|
||||
writeStream(to, log) { outputStream =>
|
||||
transfer(inputStream, outputStream, log)
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Copies the files declared in <code>sources</code> to the <code>destinationDirectory</code>
|
||||
* directory. Directories are not recursively entered. The destination hierarchy matches the
|
||||
* source paths relative to any base directories. For example:
|
||||
*
|
||||
* A source <code>(basePath ##) / x / y</code> is copied to <code>destinationDirectory / x / y</code>.
|
||||
* */
|
||||
def copy(sources: Iterable[Path], destinationDirectory: Path, log: Logger) =
|
||||
{
|
||||
val targetSet = new scala.collection.mutable.HashSet[Path]
|
||||
copyImpl(sources, destinationDirectory, log)
|
||||
{
|
||||
source =>
|
||||
{
|
||||
val from = source.asFile
|
||||
val toPath = Path.fromString(destinationDirectory, source.relativePath)
|
||||
targetSet += toPath
|
||||
val to = toPath.asFile
|
||||
if(!to.exists || from.lastModified > to.lastModified)
|
||||
{
|
||||
if(from.isDirectory)
|
||||
createDirectory(to, log)
|
||||
else
|
||||
{
|
||||
log.debug("Copying " + source + " to " + toPath)
|
||||
copyFile(from, to, log)
|
||||
}
|
||||
}
|
||||
else
|
||||
None
|
||||
}
|
||||
}.toLeft(readOnly(targetSet))
|
||||
}
|
||||
|
||||
/** Copies the files declared in <code>sources</code> to the <code>targetDirectory</code>
|
||||
* directory. The source directory hierarchy is flattened so that all copies are immediate
|
||||
* children of <code>targetDirectory</code>. Directories are not recursively entered.*/
|
||||
def copyFilesFlat(sources: Iterable[File], targetDirectory: Path, log: Logger) =
|
||||
{
|
||||
require(targetDirectory.asFile.isDirectory, "Target '" + targetDirectory + "' is not a directory.")
|
||||
val byName = new scala.collection.mutable.HashMap[String, File]
|
||||
for(source <- sources) byName.put(source.getName, source)
|
||||
val uniquelyNamedSources = byName.values
|
||||
val targetSet = new scala.collection.mutable.HashSet[Path]
|
||||
def copy(source: File): Option[String] =
|
||||
{
|
||||
if(source.isDirectory)
|
||||
copyAll(source.listFiles.toList)
|
||||
else if(source.exists)
|
||||
{
|
||||
val targetPath = targetDirectory / source.getName
|
||||
targetSet += targetPath
|
||||
if(!targetPath.exists || source.lastModified > targetPath.lastModified)
|
||||
{
|
||||
log.debug("Copying " + source + " to " + targetPath)
|
||||
copyFile(source, targetPath.asFile, log)
|
||||
}
|
||||
else
|
||||
None
|
||||
}
|
||||
else
|
||||
None
|
||||
}
|
||||
def copyAll(sources: List[File]): Option[String] =
|
||||
sources match
|
||||
{
|
||||
case head :: tail =>
|
||||
copy(head) match
|
||||
{
|
||||
case None => copyAll(tail)
|
||||
case x => x
|
||||
}
|
||||
case Nil => None
|
||||
}
|
||||
|
||||
Control.trap("Error copying files: ", log) { copyAll(uniquelyNamedSources.toList).toLeft(readOnly(targetSet)) }
|
||||
}
|
||||
/** Copies <code>sourceFile</code> to <code>targetFile</code>. If <code>targetFile</code>
|
||||
* exists, it is overwritten. Note that unlike higher level copies in FileUtilities, this
|
||||
* method always performs the copy, even if sourceFile is older than targetFile.*/
|
||||
def copyFile(sourceFile: Path, targetFile: Path, log: Logger): Option[String] =
|
||||
copyFile(sourceFile.asFile, targetFile.asFile, log)
|
||||
/** Copies <code>sourceFile</code> to <code>targetFile</code>. If <code>targetFile</code>
|
||||
* exists, it is overwritten. Note that unlike higher level copies in FileUtilities, this
|
||||
* method always performs the copy, even if sourceFile is older than targetFile.*/
|
||||
def copyFile(sourceFile: File, targetFile: File, log: Logger): Option[String] =
|
||||
{
|
||||
require(sourceFile.exists, "Source file '" + sourceFile.getAbsolutePath + "' does not exist.")
|
||||
require(!sourceFile.isDirectory, "Source file '" + sourceFile.getAbsolutePath + "' is a directory.")
|
||||
readChannel(sourceFile, log)(
|
||||
in => writeChannel(targetFile, log) {
|
||||
out => {
|
||||
val copied = out.transferFrom(in, 0, in.size)
|
||||
if(copied == in.size)
|
||||
None
|
||||
else
|
||||
Some("Could not copy '" + sourceFile + "' to '" + targetFile + "' (" + copied + "/" + in.size + " bytes copied)")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** Synchronizes the contents of the <code>sourceDirectory</code> directory to the
|
||||
* <code>targetDirectory</code> directory.*/
|
||||
def sync(sourceDirectory: Path, targetDirectory: Path, log: Logger): Option[String] =
|
||||
{
|
||||
copy(((sourceDirectory ##) ** AllPassFilter).get, targetDirectory, log).right.flatMap
|
||||
{ copiedTo => prune(targetDirectory, copiedTo, log).toLeft(()) }.left.toOption
|
||||
}
|
||||
def prune(directory: Path, keepOnly: Iterable[Path], log: Logger): Option[String] =
|
||||
{
|
||||
val existing = ((directory ##) ** AllPassFilter).get
|
||||
val toRemove = scala.collection.mutable.HashSet(existing.toSeq: _*)
|
||||
toRemove --= keepOnly
|
||||
if(log.atLevel(Level.Debug))
|
||||
toRemove.foreach(r => log.debug("Pruning " + r))
|
||||
clean(toRemove, true, log)
|
||||
}
|
||||
|
||||
/** Copies the contents of the <code>source</code> directory to the <code>target</code> directory .*/
|
||||
def copyDirectory(source: Path, target: Path, log: Logger): Option[String] =
|
||||
copyDirectory(source.asFile, target.asFile, log)
|
||||
/** Copies the contents of the <code>source</code> directory to the <code>target</code> directory .*/
|
||||
def copyDirectory(source: File, target: File, log: Logger): Option[String] =
|
||||
{
|
||||
require(source.isDirectory, "Source '" + source.getAbsolutePath + "' is not a directory.")
|
||||
require(!target.exists, "Target '" + target.getAbsolutePath + "' already exists.")
|
||||
def copyDirectory(sourceDir: File, targetDir: File): Option[String] =
|
||||
createDirectory(targetDir, log) orElse copyContents(sourceDir, targetDir)
|
||||
def copyContents(sourceDir: File, targetDir: File): Option[String] =
|
||||
sourceDir.listFiles.foldLeft(None: Option[String])
|
||||
{
|
||||
(result, file) =>
|
||||
result orElse
|
||||
{
|
||||
val targetFile = new File(targetDir, file.getName)
|
||||
if(file.isDirectory)
|
||||
copyDirectory(file, targetFile)
|
||||
else
|
||||
copyFile(file, targetFile, log)
|
||||
}
|
||||
}
|
||||
copyDirectory(source, target)
|
||||
}
|
||||
|
||||
|
||||
/** Deletes the given file recursively.*/
|
||||
def clean(file: Path, log: Logger): Option[String] = clean(file :: Nil, log)
|
||||
/** Deletes the given files recursively.*/
|
||||
def clean(files: Iterable[Path], log: Logger): Option[String] = clean(files, false, log)
|
||||
/** Deletes the given files recursively. <code>quiet</code> determines the logging level.
|
||||
* If it is true, each file in <code>files</code> is logged at the <code>info</code> level.
|
||||
* If it is false, the <code>debug</code> level is used.*/
|
||||
def clean(files: Iterable[Path], quiet: Boolean, log: Logger): Option[String] =
|
||||
deleteFiles(files.map(_.asFile), quiet, log)
|
||||
|
||||
private def deleteFiles(files: Iterable[File], quiet: Boolean, log: Logger): Option[String] =
|
||||
((None: Option[String]) /: files)( (result, file) => result orElse delete(file, quiet, log))
|
||||
private def delete(file: File, quiet: Boolean, log: Logger): Option[String] =
|
||||
{
|
||||
def logMessage(message: => String)
|
||||
{
|
||||
log.log(if(quiet) Level.Debug else Level.Info, message)
|
||||
}
|
||||
Control.trapUnit("Error deleting file " + file + ": ", log)
|
||||
{
|
||||
if(file.isDirectory)
|
||||
{
|
||||
logMessage("Deleting directory " + file)
|
||||
deleteFiles(wrapNull(file.listFiles), true, log)
|
||||
file.delete
|
||||
}
|
||||
else if(file.exists)
|
||||
{
|
||||
logMessage("Deleting file " + file)
|
||||
file.delete
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/** Appends the given <code>String content</code> to the provided <code>file</code> using the default encoding.
|
||||
* A new file is created if it does not exist.*/
|
||||
def append(file: File, content: String, log: Logger): Option[String] = append(file, content, Charset.defaultCharset, log)
|
||||
/** Appends the given <code>String content</code> to the provided <code>file</code> using the given encoding.
|
||||
* A new file is created if it does not exist.*/
|
||||
def append(file: File, content: String, charset: Charset, log: Logger): Option[String] =
|
||||
write(file, content, charset, true, log)
|
||||
|
||||
/** Writes the given <code>String content</code> to the provided <code>file</code> using the default encoding.
|
||||
* If the file exists, it is overwritten.*/
|
||||
def write(file: File, content: String, log: Logger): Option[String] = write(file, content, Charset.defaultCharset, log)
|
||||
/** Writes the given <code>String content</code> to the provided <code>file</code> using the given encoding.
|
||||
* If the file already exists, it is overwritten.*/
|
||||
def write(file: File, content: String, charset: Charset, log: Logger): Option[String] =
|
||||
write(file, content, charset, false, log)
|
||||
private def write(file: File, content: String, charset: Charset, append: Boolean, log: Logger): Option[String] =
|
||||
{
|
||||
if(charset.newEncoder.canEncode(content))
|
||||
write(file, charset, append, log) { w => w.write(content); None }
|
||||
else
|
||||
Some("String cannot be encoded by default charset.")
|
||||
}
|
||||
|
||||
/** Opens a <code>Writer</code> on the given file using the default encoding,
|
||||
* passes it to the provided function, and closes the <code>Writer</code>.*/
|
||||
def write(file: File, log: Logger)(f: Writer => Option[String]): Option[String] =
|
||||
write(file, Charset.defaultCharset, log)(f)
|
||||
/** Opens a <code>Writer</code> on the given file using the given encoding,
|
||||
* passes it to the provided function, and closes the <code>Writer</code>.*/
|
||||
def write(file: File, charset: Charset, log: Logger)(f: Writer => Option[String]): Option[String] =
|
||||
write(file, charset, false, log)(f)
|
||||
private def write(file: File, charset: Charset, append: Boolean, log: Logger)(f: Writer => Option[String]): Option[String] =
|
||||
fileWriter(charset, append).ioOption(file, Writing, log)(f)
|
||||
|
||||
/** Opens a <code>Reader</code> on the given file using the default encoding,
|
||||
* passes it to the provided function, and closes the <code>Reader</code>.*/
|
||||
def read(file: File, log: Logger)(f: Reader => Option[String]): Option[String] =
|
||||
read(file, Charset.defaultCharset, log)(f)
|
||||
/** Opens a <code>Reader</code> on the given file using the default encoding,
|
||||
* passes it to the provided function, and closes the <code>Reader</code>.*/
|
||||
def read(file: File, charset: Charset, log: Logger)(f: Reader => Option[String]): Option[String] =
|
||||
fileReader(charset).ioOption(file, Reading, log)(f)
|
||||
/** Opens a <code>Reader</code> on the given file using the default encoding,
|
||||
* passes it to the provided function, and closes the <code>Reader</code>.*/
|
||||
def readValue[R](file: File, log: Logger)(f: Reader => Either[String, R]): Either[String, R] =
|
||||
readValue(file, Charset.defaultCharset, log)(f)
|
||||
/** Opens a <code>Reader</code> on the given file using the given encoding,
|
||||
* passes it to the provided function, and closes the <code>Reader</code>.*/
|
||||
def readValue[R](file: File, charset: Charset, log: Logger)(f: Reader => Either[String, R]): Either[String, R] =
|
||||
fileReader(charset).io(file, Reading, log)(f)
|
||||
|
||||
/** Reads the contents of the given file into a <code>String</code> using the default encoding.
|
||||
* The resulting <code>String</code> is wrapped in <code>Right</code>.*/
|
||||
def readString(file: File, log: Logger): Either[String, String] = readString(file, Charset.defaultCharset, log)
|
||||
/** Reads the contents of the given file into a <code>String</code> using the given encoding.
|
||||
* The resulting <code>String</code> is wrapped in <code>Right</code>.*/
|
||||
def readString(file: File, charset: Charset, log: Logger): Either[String, String] = readValue(file, charset, log)(readString)
|
||||
|
||||
def readString(in: InputStream, log: Logger): Either[String, String] = readString(in, Charset.defaultCharset, log)
|
||||
def readString(in: InputStream, charset: Charset, log: Logger): Either[String, String] =
|
||||
streamReader.io((in, charset), Reading, log)(readString)
|
||||
def readString(in: Reader, log: Logger): Either[String, String] =
|
||||
Control.trapAndFinally("Error reading bytes from reader: ", log)
|
||||
{ readString(in) }
|
||||
{ in.close() }
|
||||
private def readString(in: Reader): Either[String, String] =
|
||||
{
|
||||
val builder = new StringBuilder
|
||||
val buffer = new Array[Char](BufferSize)
|
||||
def readNext()
|
||||
{
|
||||
val read = in.read(buffer, 0, buffer.length)
|
||||
if(read >= 0)
|
||||
{
|
||||
builder.append(buffer, 0, read)
|
||||
readNext()
|
||||
}
|
||||
else
|
||||
None
|
||||
}
|
||||
readNext()
|
||||
Right(builder.toString)
|
||||
}
|
||||
/** Appends the given bytes to the given file. */
|
||||
def append(file: File, bytes: Array[Byte], log: Logger): Option[String] =
|
||||
writeBytes(file, bytes, true, log)
|
||||
/** Writes the given bytes to the given file. If the file already exists, it is overwritten.*/
|
||||
def write(file: File, bytes: Array[Byte], log: Logger): Option[String] =
|
||||
writeBytes(file, bytes, false, log)
|
||||
private def writeBytes(file: File, bytes: Array[Byte], append: Boolean, log: Logger): Option[String] =
|
||||
writeStream(file, append, log) { out => out.write(bytes); None }
|
||||
|
||||
/** Reads the entire file into a byte array. */
|
||||
def readBytes(file: File, log: Logger): Either[String, Array[Byte]] = readStreamValue(file, log)(readBytes)
|
||||
def readBytes(in: InputStream, log: Logger): Either[String, Array[Byte]] =
|
||||
Control.trapAndFinally("Error reading bytes from input stream: ", log)
|
||||
{ readBytes(in) }
|
||||
{ in.close() }
|
||||
private def readBytes(in: InputStream): Either[String, Array[Byte]] =
|
||||
{
|
||||
val out = new ByteArrayOutputStream
|
||||
val buffer = new Array[Byte](BufferSize)
|
||||
def readNext()
|
||||
{
|
||||
val read = in.read(buffer)
|
||||
if(read >= 0)
|
||||
{
|
||||
out.write(buffer, 0, read)
|
||||
readNext()
|
||||
}
|
||||
}
|
||||
readNext()
|
||||
Right(out.toByteArray)
|
||||
}
|
||||
|
||||
/** Opens an <code>OutputStream</code> on the given file with append=true and passes the stream
|
||||
* to the provided function. The stream is closed before this function returns.*/
|
||||
def appendStream(file: File, log: Logger)(f: OutputStream => Option[String]): Option[String] =
|
||||
fileOutputStream(true).ioOption(file, Appending, log)(f)
|
||||
/** Opens an <code>OutputStream</code> on the given file and passes the stream
|
||||
* to the provided function. The stream is closed before this function returns.*/
|
||||
def writeStream(file: File, log: Logger)(f: OutputStream => Option[String]): Option[String] =
|
||||
fileOutputStream(false).ioOption(file, Writing, log)(f)
|
||||
private def writeStream(file: File, append: Boolean, log: Logger)(f: OutputStream => Option[String]): Option[String] =
|
||||
if(append) appendStream(file, log)(f) else writeStream(file, log)(f)
|
||||
/** Opens an <code>InputStream</code> on the given file and passes the stream
|
||||
* to the provided function. The stream is closed before this function returns.*/
|
||||
def readStream(file: File, log: Logger)(f: InputStream => Option[String]): Option[String] =
|
||||
fileInputStream.ioOption(file, Reading, log)(f)
|
||||
/** Opens an <code>InputStream</code> on the given file and passes the stream
|
||||
* to the provided function. The stream is closed before this function returns.*/
|
||||
def readStreamValue[R](file: File, log: Logger)(f: InputStream => Either[String, R]): Either[String, R] =
|
||||
fileInputStream.io(file, Reading, log)(f)
|
||||
/** Opens an <code>InputStream</code> on the given <code>URL</code> and passes the stream
|
||||
* to the provided function. The stream is closed before this function returns.*/
|
||||
def readStream(url: URL, log: Logger)(f: InputStream => Option[String]): Option[String] =
|
||||
urlInputStream.ioOption(url, Reading, log)(f)
|
||||
/** Opens an <code>InputStream</code> on the given <code>URL</code> and passes the stream
|
||||
* to the provided function. The stream is closed before this function returns.*/
|
||||
def readStreamValue[R](url: URL, log: Logger)(f: InputStream => Either[String, R]): Either[String, R] =
|
||||
urlInputStream.io(url, Reading, log)(f)
|
||||
|
||||
/** Opens a <code>FileChannel</code> on the given file for writing and passes the channel
|
||||
* to the given function. The channel is closed before this function returns.*/
|
||||
def writeChannel(file: File, log: Logger)(f: FileChannel => Option[String]): Option[String] =
|
||||
fileOutputChannel.ioOption(file, Writing, log)(f)
|
||||
/** Opens a <code>FileChannel</code> on the given file for reading and passes the channel
|
||||
* to the given function. The channel is closed before this function returns.*/
|
||||
def readChannel(file: File, log: Logger)(f: FileChannel => Option[String]): Option[String] =
|
||||
fileInputChannel.ioOption(file, Reading, log)(f)
|
||||
/** Opens a <code>FileChannel</code> on the given file for reading and passes the channel
|
||||
* to the given function. The channel is closed before this function returns.*/
|
||||
def readChannelValue[R](file: File, log: Logger)(f: FileChannel => Either[String, R]): Either[String, R] =
|
||||
fileInputChannel.io(file, Reading, log)(f)
|
||||
|
||||
private[sbt] def wrapNull(a: Array[File]): Array[File] =
|
||||
if(a == null)
|
||||
new Array[File](0)
|
||||
else
|
||||
a
|
||||
|
||||
/** Writes the given string to the writer followed by a newline.*/
|
||||
private[sbt] def writeLine(writer: Writer, line: String)
|
||||
{
|
||||
writer.write(line)
|
||||
writer.write(Newline)
|
||||
}
|
||||
|
||||
/** The directory in which temporary files are placed.*/
|
||||
val temporaryDirectory = new File(System.getProperty("java.io.tmpdir"))
|
||||
def classLocation(cl: Class[_]): URL = cl.getProtectionDomain.getCodeSource.getLocation
|
||||
def classLocationFile(cl: Class[_]): File = new File(classLocation(cl).toURI)
|
||||
def classLocation[T](implicit mf: scala.reflect.Manifest[T]): URL = classLocation(mf.erasure)
|
||||
def classLocationFile[T](implicit mf: scala.reflect.Manifest[T]): File = classLocationFile(mf.erasure)
|
||||
|
||||
/** The location of the jar containing this class.*/
|
||||
lazy val sbtJar: File = classLocationFile(getClass)
|
||||
lazy val scalaLibraryJar: File = classLocationFile[scala.ScalaObject]
|
||||
lazy val scalaCompilerJar: File = classLocationFile[scala.tools.nsc.Settings]
|
||||
|
||||
/** The producer of randomness for unique name generation.*/
|
||||
private val random = new java.util.Random
|
||||
|
||||
private val Reading = "reading"
|
||||
private val Writing = "writing"
|
||||
private val Appending = "appending"
|
||||
}
|
||||
|
||||
private abstract class OpenResource[Source, T] extends NotNull
|
||||
{
|
||||
import OpenResource.{unwrapEither, wrapEither}
|
||||
protected def open(src: Source, log: Logger): Either[String, T]
|
||||
def ioOption(src: Source, op: String, log: Logger)(f: T => Option[String]) =
|
||||
unwrapEither( io(src, op, log)(wrapEither(f)) )
|
||||
def io[R](src: Source, op: String, log: Logger)(f: T => Either[String,R]): Either[String, R] =
|
||||
open(src, log).right flatMap
|
||||
{
|
||||
resource => Control.trapAndFinally("Error " + op + " "+ src + ": ", log)
|
||||
{ f(resource) }
|
||||
{ close(resource) }
|
||||
}
|
||||
protected def close(out: T): Unit
|
||||
}
|
||||
private trait CloseableOpenResource[Source, T <: Closeable] extends OpenResource[Source, T]
|
||||
{
|
||||
protected def close(out: T): Unit = out.close()
|
||||
}
|
||||
import scala.reflect.{Manifest => SManifest}
|
||||
private abstract class WrapOpenResource[Source, T <: Closeable](implicit srcMf: SManifest[Source], targetMf: SManifest[T]) extends CloseableOpenResource[Source, T]
|
||||
{
|
||||
private def label[S](m: SManifest[S]) = m.erasure.getSimpleName
|
||||
protected def open(source: Source): T
|
||||
protected final def open(source: Source, log: Logger): Either[String, T] =
|
||||
Control.trap("Error wrapping " + label(srcMf) + " in " + label(targetMf) + ": ", log) { Right(open(source)) }
|
||||
}
|
||||
private abstract class OpenFile[T] extends OpenResource[File, T]
|
||||
{
|
||||
protected def open(file: File): T
|
||||
protected final def open(file: File, log: Logger): Either[String, T] =
|
||||
{
|
||||
val parent = file.getParentFile
|
||||
if(parent != null)
|
||||
FileUtilities.createDirectory(parent, log)
|
||||
Control.trap("Error opening " + file + ": ", log) { Right(open(file)) }
|
||||
}
|
||||
}
|
||||
private abstract class CloseableOpenFile[T <: Closeable] extends OpenFile[T] with CloseableOpenResource[File, T]
|
||||
private object OpenResource
|
||||
{
|
||||
private def wrapEither[R](f: R => Option[String]): (R => Either[String, Unit]) = (r: R) => f(r).toLeft(())
|
||||
private def unwrapEither(e: Either[String, Unit]): Option[String] = e.left.toOption
|
||||
|
||||
def fileOutputStream(append: Boolean) =
|
||||
new CloseableOpenFile[FileOutputStream] { protected def open(file: File) = new FileOutputStream(file, append) }
|
||||
def fileInputStream = new CloseableOpenFile[FileInputStream]
|
||||
{ protected def open(file: File) = new FileInputStream(file) }
|
||||
def urlInputStream = new CloseableOpenResource[URL, InputStream]
|
||||
{ protected def open(url: URL, log: Logger) = Control.trap("Error opening " + url + ": ", log) { Right(url.openStream) } }
|
||||
def fileOutputChannel = new CloseableOpenFile[FileChannel]
|
||||
{ protected def open(f: File) = (new FileOutputStream(f)).getChannel }
|
||||
def fileInputChannel = new CloseableOpenFile[FileChannel]
|
||||
{ protected def open(f: File) = (new FileInputStream(f)).getChannel }
|
||||
def fileWriter(charset: Charset, append: Boolean) = new CloseableOpenFile[Writer]
|
||||
{ protected def open(f: File) = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f, append), charset)) }
|
||||
def fileReader(charset: Charset) = new CloseableOpenFile[Reader]
|
||||
{ protected def open(f: File) = new BufferedReader(new InputStreamReader(new FileInputStream(f), charset)) }
|
||||
def jarFile(verify: Boolean) = new OpenFile[JarFile]
|
||||
{ protected def open(f: File) = new JarFile(f, verify)
|
||||
override protected def close(j: JarFile) = j.close() }
|
||||
def zipFile = new OpenFile[ZipFile]
|
||||
{ protected def open(f: File) = new ZipFile(f)
|
||||
override protected def close(z: ZipFile) = z.close() }
|
||||
def streamReader = new WrapOpenResource[(InputStream, Charset), Reader]
|
||||
{ protected def open(streamCharset: (InputStream, Charset)) = new InputStreamReader(streamCharset._1, streamCharset._2) }
|
||||
def gzipInputStream = new WrapOpenResource[InputStream, GZIPInputStream]
|
||||
{ protected def open(in: InputStream) = new GZIPInputStream(in) }
|
||||
def zipInputStream = new WrapOpenResource[InputStream, ZipInputStream]
|
||||
{ protected def open(in: InputStream) = new ZipInputStream(in) }
|
||||
def gzipOutputStream = new WrapOpenResource[OutputStream, GZIPOutputStream]
|
||||
{ protected def open(out: OutputStream) = new GZIPOutputStream(out)
|
||||
override protected def close(out: GZIPOutputStream) = out.finish() }
|
||||
def jarOutputStream = new WrapOpenResource[OutputStream, JarOutputStream]
|
||||
{ protected def open(out: OutputStream) = new JarOutputStream(out) }
|
||||
def jarInputStream = new WrapOpenResource[InputStream, JarInputStream]
|
||||
{ protected def open(in: InputStream) = new JarInputStream(in) }
|
||||
def zipEntry(zip: ZipFile) = new CloseableOpenResource[ZipEntry, InputStream] {
|
||||
protected def open(entry: ZipEntry, log: Logger) =
|
||||
Control.trap("Error opening " + entry.getName + " in " + zip + ": ", log) { Right(zip.getInputStream(entry)) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import java.io.File
|
||||
|
||||
abstract class ForkJava extends NotNull
|
||||
{
|
||||
def javaHome: Option[File] = None
|
||||
}
|
||||
abstract class ForkScala extends ForkJava
|
||||
{
|
||||
def scalaJars: Iterable[File] = None
|
||||
}
|
||||
trait ForkScalaRun extends ForkScala
|
||||
{
|
||||
def workingDirectory: Option[File] = None
|
||||
def runJVMOptions: Seq[String] = Nil
|
||||
}
|
||||
trait ForkScalaCompiler extends ForkScala
|
||||
{
|
||||
def compileJVMOptions: Seq[String] = Nil
|
||||
}
|
||||
|
||||
import java.lang.{ProcessBuilder => JProcessBuilder}
|
||||
object Fork
|
||||
{
|
||||
private val ScalacMainClass = "scala.tools.nsc.Main"
|
||||
private val ScalaMainClass = "scala.tools.nsc.MainGenericRunner"
|
||||
val java = new ForkJava("java")
|
||||
val javac = new ForkJava("javac")
|
||||
val scala = new ForkScala(ScalaMainClass)
|
||||
val scalac = new ForkScala(ScalacMainClass)
|
||||
|
||||
private def javaCommand(javaHome: Option[File], name: String): File =
|
||||
{
|
||||
val home = javaHome.getOrElse(new File(System.getProperty("java.home")))
|
||||
new File(new File(home, "bin"), name)
|
||||
}
|
||||
final class ForkJava(commandName: String) extends NotNull
|
||||
{
|
||||
def apply(javaHome: Option[File], options: Seq[String], log: Logger): Int =
|
||||
apply(javaHome, options, None, log)
|
||||
def apply(javaHome: Option[File], options: Seq[String], workingDirectory: File, log: Logger): Int =
|
||||
apply(javaHome, options, Some(workingDirectory), log)
|
||||
def apply(javaHome: Option[File], options: Seq[String], workingDirectory: Option[File], log: Logger): Int =
|
||||
apply(javaHome, options, workingDirectory, Map.empty, log)
|
||||
/** env is additional environment variables*/
|
||||
def apply(javaHome: Option[File], options: Seq[String], workingDirectory: Option[File], env: Map[String, String], log: Logger): Int =
|
||||
{
|
||||
val executable = javaCommand(javaHome, commandName).getAbsolutePath
|
||||
val command = (executable :: options.toList).toArray
|
||||
log.debug("Forking java process: " + command.mkString(" ") + workingDirectory.map("\n\tin " + _.getAbsolutePath).getOrElse(""))
|
||||
val builder = new JProcessBuilder(command : _*)
|
||||
workingDirectory.foreach(wd => builder.directory(wd))
|
||||
val environment = builder.environment
|
||||
for( (key, value) <- env )
|
||||
environment.put(key, value)
|
||||
Process(builder) ! log
|
||||
}
|
||||
}
|
||||
final class ForkScala(mainClassName: String) extends NotNull
|
||||
{
|
||||
def apply(javaHome: Option[File], jvmOptions: Seq[String], scalaJars: Iterable[File], arguments: Seq[String], log: Logger): Int =
|
||||
apply(javaHome, jvmOptions, scalaJars, arguments, None, log)
|
||||
def apply(javaHome: Option[File], jvmOptions: Seq[String], scalaJars: Iterable[File], arguments: Seq[String], workingDirectory: File, log: Logger): Int =
|
||||
apply(javaHome, jvmOptions, scalaJars, arguments, Some(workingDirectory), log)
|
||||
def apply(javaHome: Option[File], jvmOptions: Seq[String], scalaJars: Iterable[File], arguments: Seq[String], workingDirectory: Option[File], log: Logger): Int =
|
||||
{
|
||||
val scalaClasspath =
|
||||
if(scalaJars.isEmpty)
|
||||
FileUtilities.scalaLibraryJar :: FileUtilities.scalaCompilerJar :: Nil
|
||||
else
|
||||
scalaJars
|
||||
val scalaClasspathString = "-Xbootclasspath/a:" + scalaClasspath.map(_.getAbsolutePath).mkString(File.pathSeparator)
|
||||
val mainClass = if(mainClassName.isEmpty) Nil else mainClassName :: Nil
|
||||
val options = jvmOptions ++ (scalaClasspathString :: mainClass ::: arguments.toList)
|
||||
Fork.java(javaHome, options, workingDirectory, log)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008 Mark Harrah, David MacIver
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import java.io.File
|
||||
import scala.collection.mutable.{HashSet, Set}
|
||||
|
||||
trait Format[T] extends NotNull
|
||||
{
|
||||
def toString(t: T): String
|
||||
def fromString(s: String): T
|
||||
}
|
||||
abstract class SimpleFormat[T] extends Format[T]
|
||||
{
|
||||
def toString(t: T) = t.toString
|
||||
}
|
||||
object Format
|
||||
{
|
||||
def path(basePath: Path): Format[Path] = new Format[Path]
|
||||
{
|
||||
def toString(path: Path) = path.relativePath
|
||||
def fromString(s: String) = Path.fromString(basePath, s)
|
||||
}
|
||||
implicit val file: Format[File] = new Format[File]
|
||||
{
|
||||
def toString(file: File) = file.getAbsolutePath
|
||||
def fromString(s: String) = (new File(s)).getAbsoluteFile
|
||||
}
|
||||
implicit val hash: Format[Array[Byte]] = new Format[Array[Byte]]
|
||||
{
|
||||
def toString(hash: Array[Byte]) = Hash.toHex(hash)
|
||||
def fromString(hash: String) = Hash.fromHex(hash)
|
||||
}
|
||||
def set[T](implicit format: Format[T]): Format[Set[T]] = new Format[Set[T]]
|
||||
{
|
||||
def toString(set: Set[T]) = set.toList.map(format.toString).mkString(File.pathSeparator)
|
||||
def fromString(s: String) = (new HashSet[T]) ++ FileUtilities.pathSplit(s).map(_.trim).filter(!_.isEmpty).map(format.fromString)
|
||||
}
|
||||
implicit val string: Format[String] = new SimpleFormat[String] { def fromString(s: String) = s }
|
||||
implicit val test: Format[TestDefinition] = new SimpleFormat[TestDefinition]
|
||||
{
|
||||
def fromString(s: String) = TestParser.parse(s).fold(error, x => x)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import java.io.{ByteArrayInputStream, File, InputStream}
|
||||
|
||||
object Hash
|
||||
{
|
||||
private val BufferSize = 8192
|
||||
def toHex(bytes: Array[Byte]): String =
|
||||
{
|
||||
val buffer = new StringBuilder(bytes.length * 2)
|
||||
for(i <- 0 until bytes.length)
|
||||
{
|
||||
val b = bytes(i)
|
||||
val bi: Int = if(b < 0) b + 256 else b
|
||||
buffer append toHex((bi >>> 4).asInstanceOf[Byte])
|
||||
buffer append toHex((bi & 0x0F).asInstanceOf[Byte])
|
||||
}
|
||||
buffer.toString
|
||||
}
|
||||
def fromHex(hex: String): Array[Byte] =
|
||||
{
|
||||
require((hex.length & 1) == 0, "Hex string must have length 2n.")
|
||||
val array = new Array[Byte](hex.length >> 1)
|
||||
for(i <- 0 until hex.length by 2)
|
||||
{
|
||||
val c1 = hex.charAt(i)
|
||||
val c2 = hex.charAt(i+1)
|
||||
array(i >> 1) = ((fromHex(c1) << 4) | fromHex(c2)).asInstanceOf[Byte]
|
||||
}
|
||||
array
|
||||
}
|
||||
/** Calculates the SHA-1 hash of the given String.*/
|
||||
def apply(s: String, log: Logger): Either[String, Array[Byte]] = apply(new ByteArrayInputStream(s.getBytes("UTF-8")), log)
|
||||
/** Calculates the SHA-1 hash of the given file.*/
|
||||
def apply(path: Path, log: Logger): Either[String, Array[Byte]] = apply(path.asFile, log)
|
||||
/** Calculates the SHA-1 hash of the given file.*/
|
||||
def apply(file: File, log: Logger): Either[String, Array[Byte]] =
|
||||
FileUtilities.readStreamValue(file, log) { stream => apply(stream, log) }
|
||||
/** Calculates the SHA-1 hash of the given stream, closing it when finished.*/
|
||||
def apply(stream: InputStream, log: Logger): Either[String, Array[Byte]] =
|
||||
{
|
||||
import java.security.{MessageDigest, DigestInputStream}
|
||||
val digest = MessageDigest.getInstance("SHA")
|
||||
Control.trapAndFinally("Error computing digest: ", log)
|
||||
{
|
||||
val dis = new DigestInputStream(stream, digest)
|
||||
val buffer = new Array[Byte](BufferSize)
|
||||
while(dis.read(buffer) >= 0) {}
|
||||
dis.close()
|
||||
Right(digest.digest)
|
||||
}
|
||||
{ stream.close() }
|
||||
}
|
||||
|
||||
private def toHex(b: Byte): Char =
|
||||
{
|
||||
require(b >= 0 && b <= 15, "Byte " + b + " was not between 0 and 15")
|
||||
if(b < 10)
|
||||
('0'.asInstanceOf[Int] + b).asInstanceOf[Char]
|
||||
else
|
||||
('a'.asInstanceOf[Int] + (b-10)).asInstanceOf[Char]
|
||||
}
|
||||
private def fromHex(c: Char): Int =
|
||||
{
|
||||
val b =
|
||||
if(c >= '0' && c <= '9')
|
||||
(c - '0')
|
||||
else if(c >= 'a' && c <= 'f')
|
||||
(c - 'a') + 10
|
||||
else if(c >= 'A' && c <= 'F')
|
||||
(c - 'A') + 10
|
||||
else
|
||||
throw new RuntimeException("Invalid hex character: '" + c + "'.")
|
||||
b
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Steven Blundy, Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import ScalaProject.{optionsAsString, javaOptionsAsString}
|
||||
|
||||
trait IntegrationTesting extends NotNull
|
||||
{
|
||||
/** Override to provide pre-test setup. */
|
||||
protected def pretests: Option[String] = None
|
||||
/** Override to provide post-test cleanup. */
|
||||
protected def posttests: Option[String] = None
|
||||
}
|
||||
trait ScalaIntegrationTesting extends IntegrationTesting
|
||||
{ self: ScalaProject =>
|
||||
|
||||
protected def integrationTestTask(frameworks: Iterable[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, options: => Seq[TestOption]) =
|
||||
testTask(frameworks, classpath, analysis, options)
|
||||
}
|
||||
|
||||
trait BasicScalaIntegrationTesting extends BasicIntegrationTesting with MavenStyleIntegrationTestPaths
|
||||
{ self: BasicScalaProject => }
|
||||
/** A fully featured integration testing that may be mixed in with any subclass of <code>BasicScalaProject</code>.
|
||||
* Pre-suite setup and post-suite cleanup are provide by overriding <code>pretests</code> and <code>posttests</code> respectively.*/
|
||||
trait BasicIntegrationTesting extends ScalaIntegrationTesting with IntegrationTestPaths with BasicDependencyProject
|
||||
{
|
||||
self: BasicScalaProject =>
|
||||
|
||||
import BasicScalaIntegrationTesting._
|
||||
|
||||
lazy val integrationTestCompile = integrationTestCompileAction
|
||||
lazy val integrationTest = integrationTestAction
|
||||
|
||||
val integrationTestCompileConditional = new CompileConditional(integrationTestCompileConfiguration)
|
||||
|
||||
protected def integrationTestAction = integrationTestTask(integrationTestFrameworks, integrationTestClasspath, integrationTestCompileConditional.analysis, integrationTestOptions) dependsOn integrationTestCompile describedAs IntegrationTestCompileDescription
|
||||
protected def integrationTestCompileAction = integrationTestCompileTask() dependsOn compile describedAs IntegrationTestDescription
|
||||
|
||||
protected def integrationTestCompileTask() = task{ integrationTestCompileConditional.run }
|
||||
|
||||
def integrationTestOptions: Seq[TestOption] =
|
||||
TestSetup(() => pretests) ::
|
||||
TestCleanup(() => posttests) ::
|
||||
testOptions.toList
|
||||
def integrationTestCompileOptions = testCompileOptions
|
||||
def javaIntegrationTestCompileOptions: Seq[JavaCompileOption] = testJavaCompileOptions
|
||||
|
||||
def integrationTestConfiguration = if(useIntegrationTestConfiguration) Configurations.IntegrationTest else Configurations.Test
|
||||
def integrationTestClasspath = fullClasspath(integrationTestConfiguration) +++ optionalClasspath
|
||||
|
||||
def integrationTestLabel = "integration-test"
|
||||
def integrationTestCompileConfiguration = new IntegrationTestCompileConfig
|
||||
|
||||
protected def integrationTestDependencies = new LibraryDependencies(this, integrationTestCompileConditional)
|
||||
|
||||
def integrationTestFrameworks = testFrameworks
|
||||
override def useIntegrationTestConfiguration = false
|
||||
abstract override def fullUnmanagedClasspath(config: Configuration) =
|
||||
{
|
||||
val superClasspath = super.fullUnmanagedClasspath(config)
|
||||
if(config == integrationTestConfiguration)
|
||||
integrationTestCompilePath +++ integrationTestResourcesPath +++ superClasspath
|
||||
else
|
||||
superClasspath
|
||||
}
|
||||
|
||||
class IntegrationTestCompileConfig extends BaseCompileConfig
|
||||
{
|
||||
def label = integrationTestLabel
|
||||
def sources = integrationTestSources
|
||||
def outputDirectory = integrationTestCompilePath
|
||||
def classpath = integrationTestClasspath
|
||||
def analysisPath = integrationTestAnalysisPath
|
||||
def baseCompileOptions = integrationTestCompileOptions
|
||||
def javaOptions = javaOptionsAsString(javaCompileOptions)
|
||||
def testDefinitionClassNames = integrationTestFrameworks.map(_.testSuperClassName)
|
||||
}
|
||||
}
|
||||
|
||||
object BasicScalaIntegrationTesting
|
||||
{
|
||||
val IntegrationTestCompileDescription = "Compiles integration test sources."
|
||||
val IntegrationTestDescription = "Runs all integration tests detected during compilation."
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
trait LineReader extends NotNull
|
||||
{
|
||||
def readLine(prompt: String): Option[String]
|
||||
}
|
||||
class Completors(val projectAction: String, val projectNames: Iterable[String],
|
||||
val generalCommands: Iterable[String], val propertyActions: Iterable[String],
|
||||
val prefixes: Iterable[String]) extends NotNull
|
||||
import jline.ConsoleReader
|
||||
abstract class JLine extends LineReader
|
||||
{
|
||||
protected[this] val reader: ConsoleReader
|
||||
def readLine(prompt: String) =
|
||||
reader.readLine(prompt) match
|
||||
{
|
||||
case null => None
|
||||
case x => Some(x.trim)
|
||||
}
|
||||
}
|
||||
object SimpleReader extends JLine
|
||||
{
|
||||
protected[this] val reader =
|
||||
{
|
||||
val cr = new ConsoleReader
|
||||
cr.setBellEnabled(false)
|
||||
cr
|
||||
}
|
||||
}
|
||||
class JLineReader(historyPath: Option[Path], completors: Completors, log: Logger) extends JLine
|
||||
{
|
||||
import completors._
|
||||
import jline.{ArgumentCompletor, Completor, MultiCompletor, NullCompletor, SimpleCompletor}
|
||||
|
||||
private val generalCompletor = simpleCompletor(generalCommands)
|
||||
private val projectCompletor = simpleArgumentCompletor(projectAction :: Nil, projectNames)
|
||||
|
||||
private val completor = new MultiCompletor()
|
||||
|
||||
protected[this] val reader =
|
||||
{
|
||||
val cr = new ConsoleReader
|
||||
cr.setBellEnabled(false)
|
||||
for(historyLocation <- historyPath)
|
||||
{
|
||||
val historyFile = historyLocation.asFile
|
||||
Control.trapAndLog(log)
|
||||
{
|
||||
historyFile.getParentFile.mkdirs()
|
||||
cr.getHistory.setHistoryFile(historyFile)
|
||||
}
|
||||
}
|
||||
cr.addCompletor(completor)
|
||||
cr
|
||||
}
|
||||
|
||||
/** Used for a single argument so that the argument can have spaces in it.*/
|
||||
object SingleArgumentDelimiter extends ArgumentCompletor.AbstractArgumentDelimiter
|
||||
{
|
||||
def isDelimiterChar(buffer: String, pos: Int) =
|
||||
(buffer.charAt(pos) == ' ') && buffer.substring(0, pos).trim.indexOf(' ') == -1
|
||||
}
|
||||
|
||||
private def simpleCompletor(completions: Iterable[String]) = new SimpleCompletor(completions.toList.toArray)
|
||||
private def simpleArgumentCompletor(first: Iterable[String], second: Iterable[String]) =
|
||||
singleArgumentCompletor(simpleCompletor(first), simpleCompletor(second))
|
||||
private def singleArgumentCompletor(first: Completor, second: Completor) =
|
||||
{
|
||||
val completors = Array(first, second, new NullCompletor)
|
||||
val c = new ArgumentCompletor(completors, SingleArgumentDelimiter)
|
||||
c.setStrict(true)
|
||||
c
|
||||
}
|
||||
private def repeatedArgumentCompletor(first: Completor, repeat: Completor) =
|
||||
{
|
||||
val c = new ArgumentCompletor(Array(first, repeat))
|
||||
c.setStrict(true)
|
||||
c
|
||||
}
|
||||
|
||||
private def propertyCompletor(propertyNames: Iterable[String]) =
|
||||
simpleArgumentCompletor(propertyActions, propertyNames)
|
||||
private def prefixedCompletor(baseCompletor: Completor) =
|
||||
singleArgumentCompletor(simpleCompletor(prefixes.toList.toArray), baseCompletor)
|
||||
def setVariableCompletions(taskNames: Iterable[String], propertyNames: Iterable[String], extra: Iterable[(String, Iterable[String])] )
|
||||
{
|
||||
import scala.collection.immutable.TreeSet
|
||||
val taskCompletor = simpleCompletor(TreeSet(taskNames.toSeq : _*))
|
||||
val extraCompletors = for( (first, repeat) <- extra) yield repeatedArgumentCompletor(simpleCompletor(first :: Nil), simpleCompletor(repeat))
|
||||
val baseCompletors = generalCompletor :: taskCompletor :: projectCompletor :: propertyCompletor(propertyNames) :: extraCompletors.toList
|
||||
val baseCompletor = new MultiCompletor(baseCompletors.toArray)
|
||||
completor.setCompletors( Array(baseCompletor, prefixedCompletor(baseCompletor)) )
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,337 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import scala.collection.mutable.{Buffer, HashMap, ListBuffer}
|
||||
|
||||
sealed trait LogEvent extends NotNull
|
||||
final class Success(val msg: String) extends LogEvent
|
||||
final class Log(val level: Level.Value, val msg: String) extends LogEvent
|
||||
final class Trace(val exception: Throwable) extends LogEvent
|
||||
final class SetLevel(val newLevel: Level.Value) extends LogEvent
|
||||
final class SetTrace(val enabled: Boolean) extends LogEvent
|
||||
final class ControlEvent(val event: ControlEvent.Value, val msg: String) extends LogEvent
|
||||
|
||||
object ControlEvent extends Enumeration
|
||||
{
|
||||
val Start, Header, Finish = Value
|
||||
}
|
||||
|
||||
abstract class Logger extends NotNull
|
||||
{
|
||||
def getLevel: Level.Value
|
||||
def setLevel(newLevel: Level.Value)
|
||||
def enableTrace(flag: Boolean)
|
||||
def traceEnabled: Boolean
|
||||
|
||||
def atLevel(level: Level.Value) = level.id >= getLevel.id
|
||||
def trace(t: => Throwable): Unit
|
||||
final def debug(message: => String): Unit = log(Level.Debug, message)
|
||||
final def info(message: => String): Unit = log(Level.Info, message)
|
||||
final def warn(message: => String): Unit = log(Level.Warn, message)
|
||||
final def error(message: => String): Unit = log(Level.Error, message)
|
||||
def success(message: => String): Unit
|
||||
def log(level: Level.Value, message: => String): Unit
|
||||
def control(event: ControlEvent.Value, message: => String): Unit
|
||||
|
||||
/** Use this method to ensure calls. */
|
||||
def logAll(events: Seq[LogEvent]): Unit
|
||||
/** Defined in terms of other methods in Logger and should not be called from them. */
|
||||
final def log(event: LogEvent)
|
||||
{
|
||||
event match
|
||||
{
|
||||
case s: Success => success(s.msg)
|
||||
case l: Log => log(l.level, l.msg)
|
||||
case t: Trace => trace(t.exception)
|
||||
case setL: SetLevel => setLevel(setL.newLevel)
|
||||
case setT: SetTrace => enableTrace(setT.enabled)
|
||||
case c: ControlEvent => control(c.event, c.msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Implements the level-setting methods of Logger.*/
|
||||
abstract class BasicLogger extends Logger
|
||||
{
|
||||
private var traceEnabledVar = true
|
||||
private var level: Level.Value = Level.Info
|
||||
def getLevel = level
|
||||
def setLevel(newLevel: Level.Value) { level = newLevel }
|
||||
def enableTrace(flag: Boolean) { traceEnabledVar = flag }
|
||||
def traceEnabled = traceEnabledVar
|
||||
}
|
||||
|
||||
final class SynchronizedLogger(delegate: Logger) extends Logger
|
||||
{
|
||||
def getLevel = { synchronized { delegate.getLevel } }
|
||||
def setLevel(newLevel: Level.Value) { synchronized { delegate.setLevel(newLevel) } }
|
||||
def enableTrace(enabled: Boolean) { synchronized { delegate.enableTrace(enabled) } }
|
||||
def traceEnabled: Boolean = { synchronized { delegate.traceEnabled } }
|
||||
|
||||
def trace(t: => Throwable) { synchronized { delegate.trace(t) } }
|
||||
def log(level: Level.Value, message: => String) { synchronized { delegate.log(level, message) } }
|
||||
def success(message: => String) { synchronized { delegate.success(message) } }
|
||||
def control(event: ControlEvent.Value, message: => String) { synchronized { delegate.control(event, message) } }
|
||||
def logAll(events: Seq[LogEvent]) { synchronized { delegate.logAll(events) } }
|
||||
}
|
||||
|
||||
final class MultiLogger(delegates: List[Logger]) extends BasicLogger
|
||||
{
|
||||
override def setLevel(newLevel: Level.Value)
|
||||
{
|
||||
super.setLevel(newLevel)
|
||||
dispatch(new SetLevel(newLevel))
|
||||
}
|
||||
override def enableTrace(enabled: Boolean)
|
||||
{
|
||||
super.enableTrace(enabled)
|
||||
dispatch(new SetTrace(enabled))
|
||||
}
|
||||
def trace(t: => Throwable) { dispatch(new Trace(t)) }
|
||||
def log(level: Level.Value, message: => String) { dispatch(new Log(level, message)) }
|
||||
def success(message: => String) { dispatch(new Success(message)) }
|
||||
def logAll(events: Seq[LogEvent]) { delegates.foreach(_.logAll(events)) }
|
||||
def control(event: ControlEvent.Value, message: => String) { delegates.foreach(_.control(event, message)) }
|
||||
private def dispatch(event: LogEvent) { delegates.foreach(_.log(event)) }
|
||||
}
|
||||
|
||||
/** A logger that can buffer the logging done on it by currently executing Thread and
|
||||
* then can flush the buffer to the delegate logger provided in the constructor. Use
|
||||
* 'startRecording' to start buffering and then 'play' from to flush the buffer for the
|
||||
* current Thread to the backing logger. The logging level set at the
|
||||
* time a message is originally logged is used, not the level at the time 'play' is
|
||||
* called.
|
||||
*
|
||||
* This class assumes that it is the only client of the delegate logger.
|
||||
*
|
||||
* This logger is thread-safe.
|
||||
* */
|
||||
final class BufferedLogger(delegate: Logger) extends Logger
|
||||
{
|
||||
private[this] val buffers = wrap.Wrappers.weakMap[Thread, Buffer[LogEvent]]
|
||||
/* The recording depth part is to enable a weak nesting of recording calls. When recording is
|
||||
* nested (recordingDepth >= 2), calls to play/playAll add the buffers for worker Threads to the
|
||||
* serial buffer (main Thread) and calls to clear/clearAll clear worker Thread buffers only. */
|
||||
private[this] def recording = recordingDepth > 0
|
||||
private[this] var recordingDepth = 0
|
||||
|
||||
private[this] val mainThread = Thread.currentThread
|
||||
private[this] def getBuffer(key: Thread) = buffers.getOrElseUpdate(key, new ListBuffer[LogEvent])
|
||||
private[this] def buffer = getBuffer(key)
|
||||
private[this] def key = Thread.currentThread
|
||||
private[this] def serialBuffer = getBuffer(mainThread)
|
||||
|
||||
private[this] def inWorker = Thread.currentThread ne mainThread
|
||||
|
||||
/** Enables buffering. */
|
||||
def startRecording() { synchronized { recordingDepth += 1 } }
|
||||
/** Flushes the buffer to the delegate logger for the current thread. This method calls logAll on the delegate
|
||||
* so that the messages are written consecutively. The buffer is cleared in the process. */
|
||||
def play(): Unit =
|
||||
synchronized
|
||||
{
|
||||
if(recordingDepth == 1)
|
||||
delegate.logAll(wrap.Wrappers.readOnly(buffer))
|
||||
else if(recordingDepth > 1 && inWorker)
|
||||
serialBuffer ++= buffer
|
||||
}
|
||||
def playAll(): Unit =
|
||||
synchronized
|
||||
{
|
||||
if(recordingDepth == 1)
|
||||
{
|
||||
for(buffer <- buffers.values)
|
||||
delegate.logAll(wrap.Wrappers.readOnly(buffer))
|
||||
}
|
||||
else if(recordingDepth > 1)
|
||||
{
|
||||
for((key, buffer) <- buffers.toList if key ne mainThread)
|
||||
serialBuffer ++= buffer
|
||||
}
|
||||
}
|
||||
/** Clears buffered events for the current thread. It does not disable buffering. */
|
||||
def clear(): Unit = synchronized { if(recordingDepth == 1 || inWorker) buffers -= key }
|
||||
/** Clears buffered events for all threads and disables buffering. */
|
||||
def stop(): Unit =
|
||||
synchronized
|
||||
{
|
||||
clearAll()
|
||||
if(recordingDepth > 0)
|
||||
recordingDepth -= 1
|
||||
}
|
||||
/** Clears buffered events for all threads. */
|
||||
def clearAll(): Unit =
|
||||
synchronized
|
||||
{
|
||||
if(recordingDepth <= 1)
|
||||
buffers.clear()
|
||||
else
|
||||
{
|
||||
val serial = serialBuffer
|
||||
buffers.clear()
|
||||
buffers(mainThread) = serial
|
||||
}
|
||||
}
|
||||
def runAndFlush[T](f: => T): T =
|
||||
{
|
||||
try { f }
|
||||
finally { play(); clear() }
|
||||
}
|
||||
|
||||
def setLevel(newLevel: Level.Value): Unit =
|
||||
synchronized {
|
||||
if(recording) buffer += new SetLevel(newLevel)
|
||||
delegate.setLevel(newLevel)
|
||||
}
|
||||
def getLevel = synchronized { delegate.getLevel }
|
||||
def traceEnabled = synchronized { delegate.traceEnabled }
|
||||
def enableTrace(flag: Boolean): Unit =
|
||||
synchronized
|
||||
{
|
||||
if(recording) buffer += new SetTrace(flag)
|
||||
delegate.enableTrace(flag)
|
||||
}
|
||||
|
||||
def trace(t: => Throwable): Unit =
|
||||
synchronized
|
||||
{
|
||||
if(traceEnabled)
|
||||
{
|
||||
if(recording) buffer += new Trace(t)
|
||||
else delegate.trace(t)
|
||||
}
|
||||
}
|
||||
def success(message: => String): Unit =
|
||||
synchronized
|
||||
{
|
||||
if(atLevel(Level.Info))
|
||||
{
|
||||
if(recording)
|
||||
buffer += new Success(message)
|
||||
else
|
||||
delegate.success(message)
|
||||
}
|
||||
}
|
||||
def log(level: Level.Value, message: => String): Unit =
|
||||
synchronized
|
||||
{
|
||||
if(atLevel(level))
|
||||
{
|
||||
if(recording)
|
||||
buffer += new Log(level, message)
|
||||
else
|
||||
delegate.log(level, message)
|
||||
}
|
||||
}
|
||||
def logAll(events: Seq[LogEvent]): Unit =
|
||||
synchronized
|
||||
{
|
||||
if(recording)
|
||||
buffer ++= events
|
||||
else
|
||||
delegate.logAll(events)
|
||||
}
|
||||
def control(event: ControlEvent.Value, message: => String): Unit =
|
||||
synchronized
|
||||
{
|
||||
if(atLevel(Level.Info))
|
||||
{
|
||||
if(recording)
|
||||
buffer += new ControlEvent(event, message)
|
||||
else
|
||||
delegate.control(event, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ConsoleLogger
|
||||
{
|
||||
private def os = System.getProperty("os.name")
|
||||
private def isWindows = os.toLowerCase.indexOf("windows") >= 0
|
||||
private def formatExplicitlyDisabled = java.lang.Boolean.getBoolean("sbt.log.noformat")
|
||||
private val formatEnabled = !(formatExplicitlyDisabled || isWindows)
|
||||
}
|
||||
|
||||
/** A logger that logs to the console. On non-windows systems, the level labels are
|
||||
* colored.
|
||||
*
|
||||
* This logger is not thread-safe.*/
|
||||
class ConsoleLogger extends BasicLogger
|
||||
{
|
||||
import ConsoleLogger.formatEnabled
|
||||
def messageColor(level: Level.Value) = Console.RESET
|
||||
def labelColor(level: Level.Value) =
|
||||
level match
|
||||
{
|
||||
case Level.Error => Console.RED
|
||||
case Level.Warn => Console.YELLOW
|
||||
case _ => Console.RESET
|
||||
}
|
||||
def successLabelColor = Console.GREEN
|
||||
def successMessageColor = Console.RESET
|
||||
override def success(message: => String)
|
||||
{
|
||||
if(atLevel(Level.Info))
|
||||
log(successLabelColor, Level.SuccessLabel, successMessageColor, message)
|
||||
}
|
||||
def trace(t: => Throwable): Unit =
|
||||
System.out.synchronized
|
||||
{
|
||||
if(traceEnabled)
|
||||
t.printStackTrace
|
||||
}
|
||||
def log(level: Level.Value, message: => String)
|
||||
{
|
||||
if(atLevel(level))
|
||||
log(labelColor(level), level.toString, messageColor(level), message)
|
||||
}
|
||||
private def setColor(color: String)
|
||||
{
|
||||
if(formatEnabled)
|
||||
System.out.synchronized { System.out.print(color) }
|
||||
}
|
||||
private def log(labelColor: String, label: String, messageColor: String, message: String): Unit =
|
||||
System.out.synchronized
|
||||
{
|
||||
for(line <- message.split("""\n"""))
|
||||
{
|
||||
setColor(Console.RESET)
|
||||
System.out.print('[')
|
||||
setColor(labelColor)
|
||||
System.out.print(label)
|
||||
setColor(Console.RESET)
|
||||
System.out.print("] ")
|
||||
setColor(messageColor)
|
||||
System.out.print(line)
|
||||
setColor(Console.RESET)
|
||||
System.out.println()
|
||||
}
|
||||
}
|
||||
|
||||
def logAll(events: Seq[LogEvent]) = System.out.synchronized { events.foreach(log) }
|
||||
def control(event: ControlEvent.Value, message: => String)
|
||||
{ log(labelColor(Level.Info), Level.Info.toString, Console.BLUE, message) }
|
||||
}
|
||||
|
||||
/** An enumeration defining the levels available for logging. A level includes all of the levels
|
||||
* with id larger than its own id. For example, Warn (id=3) includes Error (id=4).*/
|
||||
object Level extends Enumeration with NotNull
|
||||
{
|
||||
val Debug = Value(1, "debug")
|
||||
val Info = Value(2, "info")
|
||||
val Warn = Value(3, "warn")
|
||||
val Error = Value(4, "error")
|
||||
/** Defines the label to use for success messages. A success message is logged at the info level but
|
||||
* uses this label. Because the label for levels is defined in this module, the success
|
||||
* label is also defined here. */
|
||||
val SuccessLabel = "success"
|
||||
|
||||
// added because elements was renamed to iterator in 2.8.0 nightly
|
||||
def levels = Debug :: Info :: Warn :: Error :: Nil
|
||||
/** Returns the level with the given name wrapped in Some, or None if no level exists for that name. */
|
||||
def apply(s: String) = levels.find(s == _.toString)
|
||||
/** Same as apply, defined for use in pattern matching. */
|
||||
private[sbt] def unapply(s: String) = apply(s)
|
||||
}
|
||||
|
|
@ -0,0 +1,673 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Steven Blundy, Mark Harrah, David MacIver, Mikko Peltonen
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import scala.collection.immutable.TreeSet
|
||||
|
||||
private trait RunCompleteAction extends NotNull
|
||||
private class Exit(val code: Int) extends RunCompleteAction
|
||||
private object Reload extends RunCompleteAction
|
||||
|
||||
/** This class is the entry point for sbt. If it is given any arguments, it interprets them
|
||||
* as actions, executes the corresponding actions, and exits. If there were no arguments provided,
|
||||
* sbt enters interactive mode.*/
|
||||
object Main
|
||||
{
|
||||
/** The entry point for sbt. If arguments are specified, they are interpreted as actions, executed,
|
||||
* and then the program terminates. If no arguments are specified, the program enters interactive
|
||||
* mode. Call run if you need to run sbt in the same JVM.*/
|
||||
def main(args: Array[String])
|
||||
{
|
||||
val exitCode = run(args)
|
||||
if(exitCode == RebootExitCode)
|
||||
{
|
||||
println("Rebooting is not supported when the sbt loader is not used.")
|
||||
println("Please manually restart sbt.")
|
||||
}
|
||||
System.exit(exitCode)
|
||||
}
|
||||
val RebootExitCode = -1
|
||||
val NormalExitCode = 0
|
||||
val SetupErrorExitCode = 1
|
||||
val SetupDeclinedExitCode = 2
|
||||
val LoadErrorExitCode = 3
|
||||
val UsageErrorExitCode = 4
|
||||
val BuildErrorExitCode = 5
|
||||
def run(args: Array[String]): Int =
|
||||
{
|
||||
val startTime = System.currentTimeMillis
|
||||
Project.loadProject match
|
||||
{
|
||||
case err: LoadSetupError =>
|
||||
println("\n" + err.message)
|
||||
ExitHooks.runExitHooks(Project.bootLogger)
|
||||
SetupErrorExitCode
|
||||
case LoadSetupDeclined =>
|
||||
ExitHooks.runExitHooks(Project.bootLogger)
|
||||
SetupDeclinedExitCode
|
||||
case err: LoadError =>
|
||||
{
|
||||
val log = Project.bootLogger
|
||||
println(err.message)
|
||||
ExitHooks.runExitHooks(log)
|
||||
// Because this is an error that can probably be corrected, prompt user to try again.
|
||||
val line =
|
||||
try { SimpleReader.readLine("\n Hit enter to retry or 'exit' to quit: ") }
|
||||
catch
|
||||
{
|
||||
case e =>
|
||||
log.trace(e)
|
||||
log.error(e.toString)
|
||||
None
|
||||
}
|
||||
line match
|
||||
{
|
||||
case Some(l) => if(!isTerminateAction(l)) run(args) else NormalExitCode
|
||||
case None => LoadErrorExitCode
|
||||
}
|
||||
}
|
||||
case success: LoadSuccess =>
|
||||
{
|
||||
import success.project
|
||||
val doNext: RunCompleteAction =
|
||||
// in interactive mode, fill all undefined properties
|
||||
if(args.length > 0 || fillUndefinedProjectProperties(project.topologicalSort.toList.reverse))
|
||||
startProject(project, args, startTime)
|
||||
else
|
||||
new Exit(NormalExitCode)
|
||||
ExitHooks.runExitHooks(project.log)
|
||||
doNext match
|
||||
{
|
||||
case Reload => run(args)
|
||||
case x: Exit => x.code
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Returns true if the project should be reloaded, false if sbt should exit.*/
|
||||
private def startProject(project: Project, args: Array[String], startTime: Long): RunCompleteAction =
|
||||
{
|
||||
project.log.info("Building project " + project.name + " " + project.version.toString + " using " + project.getClass.getName)
|
||||
val scalaVersionOpt = ScalaVersion.current orElse project.scalaVersion.get
|
||||
for(sbtVersion <- project.sbtVersion.get; scalaVersion <- scalaVersionOpt if !sbtVersion.isEmpty && !scalaVersion.isEmpty)
|
||||
project.log.info(" with sbt " + sbtVersion + " and Scala " + scalaVersion)
|
||||
args match
|
||||
{
|
||||
case Array() =>
|
||||
CrossBuild.load() match
|
||||
{
|
||||
case None =>
|
||||
project.log.info("No actions specified, interactive session started. Execute 'help' for more information.")
|
||||
val doNext = interactive(project)
|
||||
printTime(project, startTime, "session")
|
||||
doNext
|
||||
case Some(cross) =>
|
||||
crossBuildNext(project, cross)
|
||||
new Exit(RebootExitCode)
|
||||
}
|
||||
case CrossBuild(action) =>
|
||||
val exitCode =
|
||||
CrossBuild.load() match
|
||||
{
|
||||
case None => if(startCrossBuild(project, action)) RebootExitCode else BuildErrorExitCode
|
||||
case Some(cross) => if(crossBuildNext(project, cross)) RebootExitCode else NormalExitCode
|
||||
}
|
||||
new Exit(exitCode)
|
||||
case _ =>
|
||||
val exitCode =
|
||||
Control.lazyFold(args.toList)(handleBatchCommand(project)) match
|
||||
{
|
||||
case None => project.log.success("Build completed successfully."); NormalExitCode
|
||||
case Some(errorMessage) =>
|
||||
project.log.error("Error during build" + (if(errorMessage.isEmpty) "." else ": " + errorMessage) )
|
||||
BuildErrorExitCode
|
||||
}
|
||||
printTime(project, startTime, "build")
|
||||
new Exit(exitCode)
|
||||
}
|
||||
}
|
||||
private def crossBuildNext(project: Project, cross: CrossBuild) =
|
||||
{
|
||||
val setScalaVersion = (newVersion: String) => { System.setProperty(ScalaVersion.LiveKey, newVersion); () }
|
||||
val complete =
|
||||
if(handleAction(project, cross.command))
|
||||
cross.versionComplete(setScalaVersion)
|
||||
else
|
||||
cross.error(setScalaVersion)
|
||||
if(complete)
|
||||
printTime(project, cross.startTime, "cross-build")
|
||||
!complete
|
||||
}
|
||||
|
||||
/** The name of the command that loads a console with access to the current project through the variable 'project'.*/
|
||||
val ProjectConsoleAction = "console-project"
|
||||
/** The name of the command that shows the current project and logging level of that project.*/
|
||||
val ShowCurrent = "current"
|
||||
/** The name of the command that shows all available actions.*/
|
||||
val ShowActions = "actions"
|
||||
/** The name of the command that sets the currently active project.*/
|
||||
val ProjectAction = "project"
|
||||
/** The name of the command that shows all available projects.*/
|
||||
val ShowProjectsAction = "projects"
|
||||
/** The list of lowercase command names that may be used to terminate the program.*/
|
||||
val TerminateActions: Iterable[String] = "exit" :: "quit" :: Nil
|
||||
/** The name of the command that sets the value of the property given as its argument.*/
|
||||
val SetAction = "set"
|
||||
/** The name of the command that gets the value of the property given as its argument.*/
|
||||
val GetAction = "get"
|
||||
/** The name of the command that displays the help message. */
|
||||
val HelpAction = "help"
|
||||
/** The command for rebooting sbt. Requires sbt to have been launched by the loader.*/
|
||||
val RebootCommand = "reboot"
|
||||
/** The name of the command that reloads a project. This is useful for when the project definition has changed. */
|
||||
val ReloadAction = "reload"
|
||||
/** The name of the command that toggles logging stacktraces. */
|
||||
val TraceCommand = "trace"
|
||||
/** The name of the command that compiles all sources continuously when they are modified. */
|
||||
val ContinuousCompileCommand = "cc"
|
||||
/** The prefix used to identify a request to execute the remaining input on source changes.*/
|
||||
val ContinuousExecutePrefix = "~"
|
||||
/** The prefix used to identify a request to execute the remaining input across multiple Scala versions.*/
|
||||
val CrossBuildPrefix = "+"
|
||||
/** Error message for when the user tries to prefix an action with CrossBuildPrefix but the loader is not used.*/
|
||||
val CrossBuildUnsupported = "Cross-building is not supported when the loader is not used."
|
||||
|
||||
/** The number of seconds between polling by the continuous compile command.*/
|
||||
val ContinuousCompilePollDelaySeconds = 1
|
||||
|
||||
/** The list of all available commands at the interactive prompt in addition to the tasks defined
|
||||
* by a project.*/
|
||||
protected def interactiveCommands: Iterable[String] = basicCommands.toList ++ logLevels.toList
|
||||
/** The list of logging levels.*/
|
||||
private def logLevels: Iterable[String] = TreeSet.empty[String] ++ Level.levels.map(_.toString)
|
||||
/** The list of all interactive commands other than logging level.*/
|
||||
private def basicCommands: Iterable[String] = TreeSet(ShowProjectsAction, ShowActions, ShowCurrent, HelpAction,
|
||||
RebootCommand, ReloadAction, TraceCommand, ContinuousCompileCommand, ProjectConsoleAction)
|
||||
|
||||
/** Enters interactive mode for the given root project. It uses JLine for tab completion and
|
||||
* history. It returns normally when the user terminates or reloads the interactive session. That is,
|
||||
* it does not call System.exit to quit.
|
||||
**/
|
||||
private def interactive(baseProject: Project): RunCompleteAction =
|
||||
{
|
||||
val projectNames = baseProject.topologicalSort.map(_.name)
|
||||
val prefixes = ContinuousExecutePrefix :: Nil
|
||||
val completors = new Completors(ProjectAction, projectNames, interactiveCommands, List(GetAction, SetAction), prefixes)
|
||||
val reader = new JLineReader(baseProject.historyPath, completors, baseProject.log)
|
||||
def updateTaskCompletions(project: Project)
|
||||
{
|
||||
val methodCompletions = for( (name, method) <- project.methods) yield (name, method.completions)
|
||||
reader.setVariableCompletions(project.taskNames, project.propertyNames, methodCompletions)
|
||||
}
|
||||
|
||||
/** Prompts the user for the next command using 'currentProject' as context.
|
||||
* If the command indicates that the user wishes to terminate or reload the session,
|
||||
* the function returns the appropriate value.
|
||||
* Otherwise, the command is handled and this function is called again
|
||||
* (tail recursively) to prompt for the next command. */
|
||||
def loop(currentProject: Project): RunCompleteAction =
|
||||
{
|
||||
updateTaskCompletions(baseProject) // this is done after every command because the completions could change due to the action invoked
|
||||
reader.readLine("> ") match
|
||||
{
|
||||
case Some(line) =>
|
||||
{
|
||||
val trimmed = line.trim
|
||||
if(trimmed.isEmpty)
|
||||
loop(currentProject)
|
||||
else if(isTerminateAction(trimmed))
|
||||
new Exit(NormalExitCode)
|
||||
else if(ReloadAction == trimmed)
|
||||
Reload
|
||||
else if(RebootCommand == trimmed)
|
||||
new Exit(RebootExitCode)
|
||||
else if(trimmed.startsWith(CrossBuildPrefix))
|
||||
{
|
||||
if(startCrossBuild(currentProject, trimmed.substring(CrossBuildPrefix.length).trim))
|
||||
new Exit(RebootExitCode)
|
||||
else
|
||||
loop(currentProject)
|
||||
}
|
||||
else if(trimmed.startsWith(ProjectAction + " "))
|
||||
{
|
||||
val projectName = trimmed.substring(ProjectAction.length + 1)
|
||||
baseProject.topologicalSort.find(_.name == projectName) match
|
||||
{
|
||||
case Some(newProject) =>
|
||||
{
|
||||
printProject("Set current project to ", newProject)
|
||||
updateTaskCompletions(newProject)
|
||||
loop(newProject)
|
||||
}
|
||||
case None =>
|
||||
{
|
||||
currentProject.log.error("Invalid project name '" + projectName + "' (type 'projects' to list available projects).")
|
||||
loop(currentProject)
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(trimmed == HelpAction)
|
||||
displayInteractiveHelp()
|
||||
else if(trimmed == ShowProjectsAction)
|
||||
baseProject.topologicalSort.foreach(listProject)
|
||||
else if(trimmed.startsWith(SetAction + " "))
|
||||
setProperty(currentProject, trimmed.substring(SetAction.length + 1))
|
||||
else if(trimmed.startsWith(GetAction + " "))
|
||||
getProperty(currentProject, trimmed.substring(GetAction.length + 1))
|
||||
else if(trimmed == ProjectConsoleAction)
|
||||
showResult(Run.projectConsole(currentProject), currentProject.log)
|
||||
else
|
||||
handleInteractiveCommand(currentProject, trimmed)
|
||||
loop(currentProject)
|
||||
}
|
||||
}
|
||||
case None => new Exit(NormalExitCode)
|
||||
}
|
||||
}
|
||||
|
||||
loop(baseProject)
|
||||
}
|
||||
private def printCmd(name:String, desc:String) = Console.println("\t" + name + ": " + desc)
|
||||
private def displayBatchHelp() = {
|
||||
Console.println("You may execute any project action or method or one of the commands described below.")
|
||||
Console.println("Available Commands:")
|
||||
printCommonCommands()
|
||||
}
|
||||
private def printCommonCommands()
|
||||
{
|
||||
printCmd("<action name>", "Executes the project specified action.")
|
||||
printCmd("<method name> <parameter>*", "Executes the project specified method.")
|
||||
printCmd(ContinuousExecutePrefix + " <command>", "Executes the project specified action or method whenever source files change.")
|
||||
printCmd(CrossBuildPrefix + " <command>", "Executes the project specified action or method for all versions of Scala defined in crossScalaVersions.")
|
||||
printCmd(ShowActions, "Shows all available actions.")
|
||||
printCmd(RebootCommand, "Changes to scala.version or sbt.version are processed and the project definition is reloaded.")
|
||||
printCmd(HelpAction, "Displays this help message.")
|
||||
}
|
||||
private def displayInteractiveHelp() = {
|
||||
Console.println("You may execute any project action or one of the commands described below. Only one action " +
|
||||
"may be executed at a time in interactive mode and is entered by name, as it would be at the command line." +
|
||||
" Also, tab completion is available.")
|
||||
Console.println("Available Commands:")
|
||||
|
||||
printCommonCommands()
|
||||
printCmd(ReloadAction, "Reloads sbt, recompiling modified project definitions if necessary.")
|
||||
printCmd(ShowCurrent, "Shows the current project and logging level of that project.")
|
||||
printCmd(Level.levels.mkString(", "), "Set logging for the current project to the specified level.")
|
||||
printCmd(TraceCommand, "Toggles whether logging stack traces is enabled.")
|
||||
printCmd(ProjectAction + " <project name>", "Sets the currently active project.")
|
||||
printCmd(ShowProjectsAction, "Shows all available projects.")
|
||||
printCmd(TerminateActions.elements.mkString(", "), "Terminates the program.")
|
||||
printCmd(SetAction + " <property> <value>", "Sets the value of the property given as its argument.")
|
||||
printCmd(GetAction + " <property>", "Gets the value of the property given as its argument.")
|
||||
printCmd(ProjectConsoleAction, "Enters the Scala interpreter with the current project bound to the variable 'current' and all members imported.")
|
||||
}
|
||||
private def listProject(p: Project) = printProject("\t", p)
|
||||
private def printProject(prefix: String, p: Project): Unit =
|
||||
Console.println(prefix + p.name + " " + p.version)
|
||||
|
||||
private def startCrossBuild(project: Project, action: String) =
|
||||
{
|
||||
checkBooted && checkAction(project, action) &&
|
||||
{
|
||||
val againstScalaVersions = project.crossScalaVersions
|
||||
val versionsDefined = !againstScalaVersions.isEmpty
|
||||
if(versionsDefined)
|
||||
CrossBuild(againstScalaVersions, action, System.currentTimeMillis)
|
||||
else
|
||||
Console.println("Project does not declare any Scala versions to cross-build against.")
|
||||
versionsDefined
|
||||
}
|
||||
}
|
||||
private def checkBooted =
|
||||
Project.booted ||
|
||||
{
|
||||
Console.println(CrossBuildUnsupported)
|
||||
false
|
||||
}
|
||||
|
||||
/** Handles the given command string provided by batch mode execution..*/
|
||||
private def handleBatchCommand(project: Project)(command: String): Option[String] =
|
||||
{
|
||||
command.trim match
|
||||
{
|
||||
case HelpAction => displayBatchHelp(); None
|
||||
case ShowActions => showActions(project); None
|
||||
case CrossBuild(crossBuildAction) => Some(CrossBuildUnsupported)
|
||||
case action => if(handleAction(project, action)) None else Some("")
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles the given command string provided at the command line.*/
|
||||
private def handleInteractiveCommand(project: Project, command: String)
|
||||
{
|
||||
command match
|
||||
{
|
||||
case GetAction => getArgumentError(project.log)
|
||||
case SetAction => setArgumentError(project.log)
|
||||
case ProjectAction => setProjectError(project.log)
|
||||
case ShowCurrent =>
|
||||
printProject("Current project is ", project)
|
||||
Console.println("Current log level is " + project.log.getLevel)
|
||||
printTraceEnabled(project)
|
||||
case ShowActions => showActions(project)
|
||||
case TraceCommand => toggleTrace(project)
|
||||
case Level(level) => setLevel(project, level)
|
||||
case ContinuousCompileCommand => compileContinuously(project)
|
||||
case action if action.startsWith(ContinuousExecutePrefix) =>
|
||||
executeContinuously(project, action.substring(ContinuousExecutePrefix.length).trim)
|
||||
case action => handleAction(project, action)
|
||||
}
|
||||
}
|
||||
private def showActions(project: Project): Unit =
|
||||
Console.println(project.taskAndMethodList)
|
||||
|
||||
// returns true if it succeeded (needed by noninteractive handleCommand)
|
||||
private def handleAction(project: Project, action: String): Boolean =
|
||||
{
|
||||
def show(result: Option[String]): Boolean = showResult(result, project.log)
|
||||
val startTime = System.currentTimeMillis
|
||||
val result = withAction(project, action)( (name, params) => show(project.call(name, params)))( name => show(project.act(name)))
|
||||
printTime(project, startTime, "")
|
||||
result
|
||||
}
|
||||
private def showResult(result: Option[String], log: Logger): Boolean =
|
||||
{
|
||||
result match
|
||||
{
|
||||
case Some(errorMessage) => log.error(errorMessage); false
|
||||
case None => log.success("Successful."); true
|
||||
}
|
||||
}
|
||||
// true if the action exists
|
||||
private def checkAction(project: Project, actionString: String): Boolean =
|
||||
withAction(project, actionString)( (n,p) => true)( n => true)
|
||||
private def withAction(project: Project, actionString: String)(ifMethod: (String, Array[String]) => Boolean)(ifAction: String => Boolean): Boolean =
|
||||
{
|
||||
def didNotExist(taskType: String, name: String) =
|
||||
{
|
||||
project.log.error("No " + taskType + " named '" + name + "' exists.")
|
||||
project.log.info("Execute 'help' for a list of commands or 'actions' for a list of available project actions and methods.")
|
||||
false
|
||||
}
|
||||
impl.CommandParser.parse(actionString) match
|
||||
{
|
||||
case Left(errMsg) => project.log.error(errMsg); false
|
||||
case Right((name, parameters)) =>
|
||||
if(project.methods.contains(name))
|
||||
ifMethod(name, parameters.toArray)
|
||||
else if(!parameters.isEmpty)
|
||||
didNotExist("method", name)
|
||||
else if(project.deepTasks.contains(name))
|
||||
ifAction(name)
|
||||
else
|
||||
didNotExist("action", name)
|
||||
}
|
||||
}
|
||||
|
||||
/** Toggles whether stack traces are enabled.*/
|
||||
private def toggleTrace(project: Project)
|
||||
{
|
||||
val newValue = !project.log.traceEnabled
|
||||
project.topologicalSort.foreach(_.log.enableTrace(newValue))
|
||||
printTraceEnabled(project)
|
||||
}
|
||||
private def printTraceEnabled(project: Project)
|
||||
{
|
||||
Console.println("Stack traces are " + (if(project.log.traceEnabled) "enabled" else "disabled"))
|
||||
}
|
||||
/** Sets the logging level on the given project.*/
|
||||
private def setLevel(project: Project, level: Level.Value)
|
||||
{
|
||||
project.topologicalSort.foreach(_.log.setLevel(level))
|
||||
Console.println("Set log level to " + project.log.getLevel)
|
||||
}
|
||||
/** Prints the elapsed time to the given project's log using the given
|
||||
* initial time and the label 's'.*/
|
||||
private def printTime(project: Project, startTime: Long, s: String)
|
||||
{
|
||||
val endTime = System.currentTimeMillis()
|
||||
project.log.info("")
|
||||
val ss = if(s.isEmpty) "" else s + " "
|
||||
project.log.info("Total " + ss + "time: " + (endTime - startTime + 500) / 1000 + " s")
|
||||
}
|
||||
/** Provides a partial message describing why the given property is undefined. */
|
||||
private def undefinedMessage(property: Project#UserProperty[_]): String =
|
||||
{
|
||||
property.resolve match
|
||||
{
|
||||
case vu: UndefinedValue => " is not defined."
|
||||
case e: ResolutionException => " has invalid value: " + e.toString
|
||||
case _ => ""
|
||||
}
|
||||
}
|
||||
/** Prompts the user for the value of undefined properties. 'first' is true if this is the first time
|
||||
* that the current property has been prompted.*/
|
||||
private def fillUndefinedProperties(project: Project, properties: List[(String, Project#Property[_])], first: Boolean): Boolean =
|
||||
{
|
||||
properties match
|
||||
{
|
||||
case (name, variable) :: tail =>
|
||||
{
|
||||
val shouldAdvanceOrQuit =
|
||||
variable match
|
||||
{
|
||||
case property: Project#UserProperty[_] =>
|
||||
if(first)
|
||||
project.log.error(" Property '" + name + "' " + undefinedMessage(property))
|
||||
for(newValue <- SimpleReader.readLine(" Enter new value for " + name + " : ")) yield
|
||||
{
|
||||
try
|
||||
{
|
||||
property.setStringValue(newValue)
|
||||
true
|
||||
}
|
||||
catch
|
||||
{
|
||||
case e =>
|
||||
project.log.error("Invalid value: " + e.getMessage)
|
||||
false
|
||||
}
|
||||
}
|
||||
case _ => Some(true)
|
||||
}
|
||||
shouldAdvanceOrQuit match
|
||||
{
|
||||
case Some(shouldAdvance) => fillUndefinedProperties(project, if(shouldAdvance) tail else properties, shouldAdvance)
|
||||
case None => false
|
||||
}
|
||||
}
|
||||
case Nil => true
|
||||
}
|
||||
}
|
||||
/** Iterates over the undefined properties in the given projects, prompting the user for the value of each undefined
|
||||
* property.*/
|
||||
private def fillUndefinedProjectProperties(projects: List[Project]): Boolean =
|
||||
{
|
||||
projects match
|
||||
{
|
||||
case project :: remaining =>
|
||||
{
|
||||
val uninitialized = project.uninitializedProperties.toList
|
||||
if(uninitialized.isEmpty)
|
||||
fillUndefinedProjectProperties(remaining)
|
||||
else
|
||||
{
|
||||
project.log.error("Project in " + project.info.projectDirectory.getAbsolutePath + " has undefined properties.")
|
||||
val result = fillUndefinedProperties(project, uninitialized, true) && fillUndefinedProjectProperties(remaining)
|
||||
project.saveEnvironment()
|
||||
result
|
||||
}
|
||||
}
|
||||
case Nil => true
|
||||
}
|
||||
}
|
||||
/** Prints the value of the property with the given name in the given project. */
|
||||
private def getProperty(project: Project, propertyName: String)
|
||||
{
|
||||
if(propertyName.isEmpty)
|
||||
project.log.error("No property name specified.")
|
||||
else
|
||||
{
|
||||
project.getPropertyNamed(propertyName) match
|
||||
{
|
||||
case Some(property) =>
|
||||
{
|
||||
property.resolve match
|
||||
{
|
||||
case u: UndefinedValue => project.log.error("Value of property '" + propertyName + "' is undefined.")
|
||||
case ResolutionException(m, e) => project.log.error(m)
|
||||
case DefinedValue(value, isInherited, isDefault) => Console.println(value.toString)
|
||||
}
|
||||
}
|
||||
case None =>
|
||||
{
|
||||
val value = System.getProperty(propertyName)
|
||||
if(value == null)
|
||||
project.log.error("No property named '" + propertyName + "' is defined.")
|
||||
else
|
||||
Console.println(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Separates the space separated property name/value pair and stores the value in the user-defined property
|
||||
* with the given name in the given project. If no such property exists, the value is stored in a system
|
||||
* property. */
|
||||
private def setProperty(project: Project, propertyNameAndValue: String)
|
||||
{
|
||||
val m = """(\S+)(\s+\S.*)?""".r.pattern.matcher(propertyNameAndValue)
|
||||
if(m.matches())
|
||||
{
|
||||
val name = m.group(1)
|
||||
val newValue =
|
||||
{
|
||||
val v = m.group(2)
|
||||
if(v == null) "" else v.trim
|
||||
}
|
||||
project.getPropertyNamed(name) match
|
||||
{
|
||||
case Some(property) =>
|
||||
{
|
||||
val succeeded =
|
||||
try
|
||||
{
|
||||
property.setStringValue(newValue)
|
||||
val isScalaVersion = property == project.scalaVersion
|
||||
val isSbtVersion = property == project.sbtVersion
|
||||
if(isScalaVersion || isSbtVersion)
|
||||
{
|
||||
val changed = if(isSbtVersion) "sbt " else "Scala "
|
||||
Console.println(" Build will use " + changed + newValue + " after running 'reboot' command or restarting sbt.")
|
||||
}
|
||||
else
|
||||
Console.println(" Set property '" + name + "' = '" + newValue + "'")
|
||||
}
|
||||
catch { case e => project.log.error("Error setting property '" + name + "' in " + project.environmentLabel + ": " + e.toString) }
|
||||
project.saveEnvironment()
|
||||
}
|
||||
case None =>
|
||||
{
|
||||
System.setProperty(name, newValue)
|
||||
project.log.info(" Set system property '" + name + "' = '" + newValue + "'")
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
setArgumentError(project.log)
|
||||
}
|
||||
|
||||
private def compileContinuously(project: Project) = executeContinuously(project, "test-compile")
|
||||
private def executeContinuously(project: Project, action: String)
|
||||
{
|
||||
if(checkAction(project, action))
|
||||
{
|
||||
SourceModificationWatch.watchUntil(project, ContinuousCompilePollDelaySeconds)(System.in.available() > 0)
|
||||
{
|
||||
handleAction(project, action)
|
||||
Console.println("Waiting for source changes... (press any key to interrupt)")
|
||||
}
|
||||
while (System.in.available() > 0) System.in.read()
|
||||
}
|
||||
}
|
||||
|
||||
private def isTerminateAction(s: String) = TerminateActions.elements.contains(s.toLowerCase)
|
||||
private def setArgumentError(log: Logger) { log.error("Invalid arguments for 'set': expected property name and new value.") }
|
||||
private def getArgumentError(log: Logger) { log.error("Invalid arguments for 'get': expected property name.") }
|
||||
private def setProjectError(log: Logger) { log.error("Invalid arguments for 'project': expected project name.") }
|
||||
}
|
||||
private class CrossBuild(val remainingScalaVersions: Set[String], val command: String, val startTime: Long)
|
||||
{
|
||||
def error(setScalaVersion: String => Unit) = clearScalaVersion(setScalaVersion)
|
||||
private def clearScalaVersion(setScalaVersion: String => Unit) =
|
||||
{
|
||||
CrossBuild.clear()
|
||||
setScalaVersion("")
|
||||
true
|
||||
}
|
||||
def versionComplete(setScalaVersion: String => Unit) =
|
||||
{
|
||||
val remaining = remainingScalaVersions - ScalaVersion.currentString
|
||||
if(remaining.isEmpty)
|
||||
clearScalaVersion(setScalaVersion)
|
||||
else
|
||||
{
|
||||
CrossBuild.setProperties(remaining, command, startTime.toString)
|
||||
setScalaVersion(remaining.toSeq.first)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
private object CrossBuild
|
||||
{
|
||||
private val RemainingScalaVersionsKey = "sbt.remaining.scala.versions"
|
||||
private val CrossCommandKey = "sbt.cross.build.command"
|
||||
private val StartTimeKey = "sbt.cross.start.time"
|
||||
private def setProperties(remainingScalaVersions: Set[String], command: String, startTime: String)
|
||||
{
|
||||
System.setProperty(RemainingScalaVersionsKey, remainingScalaVersions.mkString(" "))
|
||||
System.setProperty(CrossCommandKey, command)
|
||||
System.setProperty(StartTimeKey, startTime)
|
||||
}
|
||||
private def getProperty(key: String) =
|
||||
{
|
||||
val value = System.getProperty(key)
|
||||
if(value == null)
|
||||
""
|
||||
else
|
||||
value.trim
|
||||
}
|
||||
private def clear() { setProperties(Set.empty, "", "") }
|
||||
def load() =
|
||||
{
|
||||
val command = getProperty(CrossCommandKey)
|
||||
val remaining = getProperty(RemainingScalaVersionsKey)
|
||||
val startTime = getProperty(StartTimeKey)
|
||||
if(command.isEmpty || remaining.isEmpty || startTime.isEmpty)
|
||||
None
|
||||
else
|
||||
Some(new CrossBuild(Set(remaining.split(" ") : _*), command, startTime.toLong))
|
||||
}
|
||||
def apply(remainingScalaVersions: Set[String], command: String, startTime: Long) =
|
||||
{
|
||||
setProperties(remainingScalaVersions, command, startTime.toString)
|
||||
new CrossBuild(remainingScalaVersions, command, startTime)
|
||||
}
|
||||
import Main.CrossBuildPrefix
|
||||
def unapply(s: String): Option[String] =
|
||||
{
|
||||
val trimmed = s.trim
|
||||
if(trimmed.startsWith(CrossBuildPrefix))
|
||||
Some(trimmed.substring(CrossBuildPrefix.length).trim)
|
||||
else
|
||||
None
|
||||
}
|
||||
def unapply(s: Array[String]): Option[String] =
|
||||
s match
|
||||
{
|
||||
case Array(CrossBuild(crossBuildAction)) => Some(crossBuildAction)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,743 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import java.util.Collections
|
||||
import scala.collection.mutable.HashSet
|
||||
|
||||
import Artifact.{defaultExtension, defaultType}
|
||||
|
||||
import org.apache.ivy.{core, plugins, util, Ivy}
|
||||
import core.LogOptions
|
||||
import core.cache.DefaultRepositoryCacheManager
|
||||
import core.deliver.DeliverOptions
|
||||
import core.module.descriptor.{DefaultArtifact, DefaultDependencyArtifactDescriptor, MDArtifact}
|
||||
import core.module.descriptor.{DefaultDependencyDescriptor, DefaultModuleDescriptor, DependencyDescriptor, ModuleDescriptor}
|
||||
import core.module.descriptor.{DefaultExcludeRule, ExcludeRule}
|
||||
import core.module.id.{ArtifactId,ModuleId, ModuleRevisionId}
|
||||
import core.publish.PublishOptions
|
||||
import core.resolve.ResolveOptions
|
||||
import core.retrieve.RetrieveOptions
|
||||
import core.settings.IvySettings
|
||||
import plugins.matcher.{ExactPatternMatcher, PatternMatcher}
|
||||
import plugins.parser.ModuleDescriptorParser
|
||||
import plugins.parser.m2.{PomModuleDescriptorParser,PomModuleDescriptorWriter}
|
||||
import plugins.parser.xml.XmlModuleDescriptorParser
|
||||
import plugins.repository.{BasicResource, Resource}
|
||||
import plugins.repository.url.URLResource
|
||||
import plugins.resolver.{ChainResolver, DependencyResolver, IBiblioResolver}
|
||||
import plugins.resolver.{AbstractPatternsBasedResolver, AbstractSshBasedResolver, FileSystemResolver, SFTPResolver, SshResolver, URLResolver}
|
||||
import util.{Message, MessageLogger}
|
||||
|
||||
final class IvyScala(val scalaVersion: String, val configurations: Iterable[Configuration], val checkExplicit: Boolean, val filterImplicit: Boolean) extends NotNull
|
||||
final class IvyPaths(val projectDirectory: Path, val managedLibDirectory: Path, val cacheDirectory: Option[Path]) extends NotNull
|
||||
final class IvyFlags(val validate: Boolean, val addScalaTools: Boolean, val errorIfNoConfiguration: Boolean) extends NotNull
|
||||
final class IvyConfiguration(val paths: IvyPaths, val manager: Manager, val flags: IvyFlags, val ivyScala: Option[IvyScala], val log: Logger) extends NotNull
|
||||
final class UpdateConfiguration(val outputPattern: String, val synchronize: Boolean, val quiet: Boolean) extends NotNull
|
||||
object ManageDependencies
|
||||
{
|
||||
val DefaultIvyConfigFilename = "ivysettings.xml"
|
||||
val DefaultIvyFilename = "ivy.xml"
|
||||
val DefaultMavenFilename = "pom.xml"
|
||||
val ScalaOrganization = "org.scala-lang"
|
||||
val ScalaLibraryID = "scala-library"
|
||||
val ScalaCompilerID = "scala-compiler"
|
||||
|
||||
private def defaultIvyFile(project: Path) = project / DefaultIvyFilename
|
||||
private def defaultIvyConfiguration(project: Path) = project / DefaultIvyConfigFilename
|
||||
private def defaultPOM(project: Path) = project / DefaultMavenFilename
|
||||
|
||||
/** Configures Ivy using the provided configuration 'config' and calls 'doWithIvy'. This method takes care of setting up and cleaning up Ivy.*/
|
||||
private def withIvy(config: IvyConfiguration)(doWithIvy: (Ivy, ModuleDescriptor, String) => Option[String]) =
|
||||
withIvyValue(config)( (ivy, module, default) => doWithIvy(ivy, module, default).toLeft(()) ).left.toOption
|
||||
/** Configures Ivy using the provided configuration 'config' and calls 'doWithIvy'. This method takes care of setting up and cleaning up Ivy.*/
|
||||
private def withIvyValue[T](config: IvyConfiguration)(doWithIvy: (Ivy, ModuleDescriptor, String) => Either[String, T]) =
|
||||
{
|
||||
import config._
|
||||
val logger = new IvyLogger(log)
|
||||
Message.setDefaultLogger(logger)
|
||||
val ivy = Ivy.newInstance()
|
||||
ivy.getLoggerEngine.pushLogger(logger)
|
||||
|
||||
/** Parses the given Maven pom 'pomFile'.*/
|
||||
def readPom(pomFile: File) =
|
||||
Control.trap("Could not read pom: ", log)
|
||||
{ Right((PomModuleDescriptorParser.getInstance.parseDescriptor(ivy.getSettings, toURL(pomFile), flags.validate)), "compile") }
|
||||
/** Parses the given Ivy file 'ivyFile'.*/
|
||||
def readIvyFile(ivyFile: File) =
|
||||
Control.trap("Could not read Ivy file: ", log)
|
||||
{
|
||||
val url = toURL(ivyFile)
|
||||
val parser = new CustomXmlParser.CustomParser(ivy.getSettings)
|
||||
parser.setValidate(flags.validate)
|
||||
parser.setSource(url)
|
||||
parser.parse()
|
||||
Right((parser.getModuleDescriptor(), parser.getDefaultConf))
|
||||
}
|
||||
/** Parses the given in-memory Ivy file 'xml', using the existing 'moduleID' and specifying the given 'defaultConfiguration'. */
|
||||
def parseXMLDependencies(xml: scala.xml.NodeSeq, moduleID: DefaultModuleDescriptor, defaultConfiguration: String) =
|
||||
parseDependencies(xml.toString, moduleID, defaultConfiguration)
|
||||
/** Parses the given in-memory Ivy file 'xml', using the existing 'moduleID' and specifying the given 'defaultConfiguration'. */
|
||||
def parseDependencies(xml: String, moduleID: DefaultModuleDescriptor, defaultConfiguration: String): Either[String, CustomXmlParser.CustomParser] =
|
||||
Control.trap("Could not read dependencies: ", log)
|
||||
{
|
||||
val parser = new CustomXmlParser.CustomParser(ivy.getSettings)
|
||||
parser.setMd(moduleID)
|
||||
parser.setDefaultConf(defaultConfiguration)
|
||||
parser.setValidate(flags.validate)
|
||||
val resource = new ByteResource(xml.getBytes)
|
||||
parser.setInput(resource.openStream)
|
||||
parser.setResource(resource)
|
||||
parser.parse()
|
||||
Right(parser)
|
||||
}
|
||||
/** Configures Ivy using the specified Ivy configuration file. This method is used when the manager is explicitly requested to be MavenManager or
|
||||
* IvyManager. If a file is not specified, Ivy is configured with defaults and scala-tools releases is added as a repository.*/
|
||||
def configure(configFile: Option[Path])
|
||||
{
|
||||
configFile match
|
||||
{
|
||||
case Some(path) => ivy.configure(path.asFile)
|
||||
case None =>
|
||||
configureDefaults()
|
||||
scalaTools()
|
||||
}
|
||||
}
|
||||
/** Adds the scala-tools.org releases maven repository to the list of resolvers if configured to do so in IvyFlags.*/
|
||||
def scalaTools()
|
||||
{
|
||||
if(flags.addScalaTools)
|
||||
{
|
||||
log.debug("Added Scala Tools Releases repository.")
|
||||
addResolvers(ivy.getSettings, ScalaToolsReleases :: Nil, log)
|
||||
}
|
||||
}
|
||||
/** Configures Ivy using defaults. This is done when no ivy-settings.xml exists. */
|
||||
def configureDefaults()
|
||||
{
|
||||
ivy.configureDefault
|
||||
val settings = ivy.getSettings
|
||||
for(dir <- paths.cacheDirectory) settings.setDefaultCache(dir.asFile)
|
||||
settings.setBaseDir(paths.projectDirectory.asFile)
|
||||
configureCache(settings)
|
||||
}
|
||||
/** Called to configure Ivy when the configured dependency manager is SbtManager and inline configuration is specified or if the manager
|
||||
* is AutodetectManager. It will configure Ivy with an 'ivy-settings.xml' file if there is one, or configure the defaults and add scala-tools as
|
||||
* a repository otherwise.*/
|
||||
def autodetectConfiguration()
|
||||
{
|
||||
log.debug("Autodetecting configuration.")
|
||||
val defaultIvyConfigFile = defaultIvyConfiguration(paths.projectDirectory).asFile
|
||||
if(defaultIvyConfigFile.canRead)
|
||||
ivy.configure(defaultIvyConfigFile)
|
||||
else
|
||||
{
|
||||
configureDefaults()
|
||||
scalaTools()
|
||||
}
|
||||
}
|
||||
/** Called to determine dependencies when the dependency manager is SbtManager and no inline dependencies (Scala or XML) are defined
|
||||
* or if the manager is AutodetectManager. It will try to read from pom.xml first and then ivy.xml if pom.xml is not found. If neither is found,
|
||||
* Ivy is configured with defaults unless IvyFlags.errorIfNoConfiguration is true, in which case an error is generated.*/
|
||||
def autodetectDependencies(module: ModuleRevisionId) =
|
||||
{
|
||||
log.debug("Autodetecting dependencies.")
|
||||
val defaultPOMFile = defaultPOM(paths.projectDirectory).asFile
|
||||
if(defaultPOMFile.canRead)
|
||||
readPom(defaultPOMFile)
|
||||
else
|
||||
{
|
||||
val defaultIvy = defaultIvyFile(paths.projectDirectory).asFile
|
||||
if(defaultIvy.canRead)
|
||||
readIvyFile(defaultIvy)
|
||||
else if(flags.errorIfNoConfiguration)
|
||||
Left("No readable dependency configuration found. Need " + DefaultIvyFilename + " or " + DefaultMavenFilename)
|
||||
else
|
||||
{
|
||||
val defaultConf = ModuleDescriptor.DEFAULT_CONFIGURATION
|
||||
log.warn("No readable dependency configuration found, using defaults.")
|
||||
val moduleID = DefaultModuleDescriptor.newDefaultInstance(module)
|
||||
addMainArtifact(moduleID)
|
||||
addDefaultArtifact(defaultConf, moduleID)
|
||||
Right((moduleID, defaultConf))
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Creates an Ivy module descriptor according the manager configured. The default configuration for dependencies
|
||||
* is also returned.*/
|
||||
def moduleDescriptor: Either[String, (ModuleDescriptor, String)] =
|
||||
config.manager match
|
||||
{
|
||||
case mm: MavenManager =>
|
||||
{
|
||||
log.debug("Maven configuration explicitly requested.")
|
||||
configure(mm.configuration)
|
||||
readPom(mm.pom.asFile)
|
||||
}
|
||||
case im: IvyManager =>
|
||||
{
|
||||
log.debug("Ivy configuration explicitly requested.")
|
||||
configure(im.configuration)
|
||||
readIvyFile(im.dependencies.asFile)
|
||||
}
|
||||
case adm: AutoDetectManager =>
|
||||
{
|
||||
log.debug("No dependency manager explicitly specified.")
|
||||
autodetectConfiguration()
|
||||
autodetectDependencies(toID(adm.module))
|
||||
}
|
||||
case sm: SbtManager =>
|
||||
{
|
||||
import sm._
|
||||
if(resolvers.isEmpty && autodetectUnspecified)
|
||||
autodetectConfiguration()
|
||||
else
|
||||
{
|
||||
log.debug("Using inline repositories.")
|
||||
configureDefaults()
|
||||
val extra = if(flags.addScalaTools) ScalaToolsReleases :: resolvers.toList else resolvers
|
||||
addResolvers(ivy.getSettings, extra, log)
|
||||
}
|
||||
if(autodetect)
|
||||
autodetectDependencies(toID(module))
|
||||
else
|
||||
{
|
||||
val moduleID =
|
||||
{
|
||||
val mod = new DefaultModuleDescriptor(toID(module), "release", null, false)
|
||||
mod.setLastModified(System.currentTimeMillis)
|
||||
configurations.foreach(config => mod.addConfiguration(toIvyConfiguration(config)))
|
||||
mod
|
||||
}
|
||||
val defaultConf = defaultConfiguration getOrElse Configurations.config(ModuleDescriptor.DEFAULT_CONFIGURATION)
|
||||
log.debug("Using inline dependencies specified in Scala" + (if(dependenciesXML.isEmpty) "." else " and XML."))
|
||||
for(parser <- parseXMLDependencies(wrapped(module, dependenciesXML), moduleID, defaultConf.name).right) yield
|
||||
{
|
||||
addArtifacts(moduleID, artifacts)
|
||||
addDependencies(moduleID, dependencies, parser)
|
||||
addMainArtifact(moduleID)
|
||||
(moduleID, parser.getDefaultConf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Creates a full ivy file for 'module' using the 'dependencies' XML as the part after the <info>...</info> section. */
|
||||
def wrapped(module: ModuleID, dependencies: scala.xml.NodeSeq) =
|
||||
{
|
||||
import module._
|
||||
<ivy-module version="2.0">
|
||||
<info organisation={organization} module={name} revision={revision}/>
|
||||
{dependencies}
|
||||
</ivy-module>
|
||||
}
|
||||
/** Performs checks/adds filters on Scala dependencies (if enabled in IvyScala). */
|
||||
def checkModule(moduleAndConf: (ModuleDescriptor, String)): Either[String, (ModuleDescriptor, String)] =
|
||||
ivyScala match
|
||||
{
|
||||
case Some(check) =>
|
||||
val (module, conf) = moduleAndConf
|
||||
val explicitCheck =
|
||||
if(check.checkExplicit)
|
||||
checkDependencies(module, check.scalaVersion, check.configurations)
|
||||
else
|
||||
None
|
||||
explicitCheck match
|
||||
{
|
||||
case None =>
|
||||
if(check.filterImplicit)
|
||||
{
|
||||
val asDefault = toDefaultModuleDescriptor(module)
|
||||
excludeScalaJars(asDefault, check.configurations)
|
||||
Right( (asDefault, conf) )
|
||||
}
|
||||
else
|
||||
Right(moduleAndConf)
|
||||
case Some(err) => Left(err)
|
||||
}
|
||||
case None => Right(moduleAndConf)
|
||||
}
|
||||
|
||||
this.synchronized // Ivy is not thread-safe. In particular, it uses a static DocumentBuilder, which is not thread-safe
|
||||
{
|
||||
ivy.pushContext()
|
||||
try
|
||||
{
|
||||
moduleDescriptor.right.flatMap(checkModule).right.flatMap { mdAndConf =>
|
||||
doWithIvy(ivy, mdAndConf._1, mdAndConf._2)
|
||||
}
|
||||
}
|
||||
finally { ivy.popContext() }
|
||||
}
|
||||
}
|
||||
/** Checks the immediate dependencies of module for dependencies on scala jars and verifies that the version on the
|
||||
* dependencies matches scalaVersion. */
|
||||
private def checkDependencies(module: ModuleDescriptor, scalaVersion: String, configurations: Iterable[Configuration]): Option[String] =
|
||||
{
|
||||
val configSet = configurationSet(configurations)
|
||||
Control.lazyFold(module.getDependencies.toList)
|
||||
{ dep =>
|
||||
val id = dep.getDependencyRevisionId
|
||||
if(id.getOrganisation == ScalaOrganization && id.getRevision != scalaVersion && dep.getModuleConfigurations.exists(configSet.contains))
|
||||
Some("Different Scala version specified in dependency ("+ id.getRevision + ") than in project (" + scalaVersion + ").")
|
||||
else
|
||||
None
|
||||
}
|
||||
}
|
||||
private def configurationSet(configurations: Iterable[Configuration]) =
|
||||
HashSet(configurations.map(_.toString).toSeq : _*)
|
||||
/** Adds exclusions for the scala library and compiler jars so that they are not downloaded. This is
|
||||
* done because normally these jars are already on the classpath and cannot/should not be overridden. The version
|
||||
* of Scala to use is done by setting scala.version in the project definition. */
|
||||
private def excludeScalaJars(module: DefaultModuleDescriptor, configurations: Iterable[Configuration])
|
||||
{
|
||||
val configurationNames =
|
||||
{
|
||||
val names = module.getConfigurationsNames
|
||||
if(configurations.isEmpty)
|
||||
names
|
||||
else
|
||||
{
|
||||
import scala.collection.mutable.HashSet
|
||||
val configSet = configurationSet(configurations)
|
||||
configSet.intersect(HashSet(names : _*))
|
||||
configSet.toArray
|
||||
}
|
||||
}
|
||||
def excludeScalaJar(name: String)
|
||||
{ module.addExcludeRule(excludeRule(ScalaOrganization, name, configurationNames)) }
|
||||
excludeScalaJar(ScalaLibraryID)
|
||||
excludeScalaJar(ScalaCompilerID)
|
||||
}
|
||||
private def configureCache(settings: IvySettings)
|
||||
{
|
||||
settings.getDefaultRepositoryCacheManager match
|
||||
{
|
||||
case manager: DefaultRepositoryCacheManager =>
|
||||
manager.setUseOrigin(true)
|
||||
manager.setChangingMatcher(PatternMatcher.REGEXP);
|
||||
manager.setChangingPattern(".*-SNAPSHOT");
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
/** Creates an ExcludeRule that excludes artifacts with the given module organization and name for
|
||||
* the given configurations. */
|
||||
private def excludeRule(organization: String, name: String, configurationNames: Iterable[String]): ExcludeRule =
|
||||
{
|
||||
val artifact = new ArtifactId(ModuleId.newInstance(organization, name), "*", "*", "*")
|
||||
val rule = new DefaultExcludeRule(artifact, ExactPatternMatcher.INSTANCE, Collections.emptyMap[AnyRef,AnyRef])
|
||||
configurationNames.foreach(rule.addConfiguration)
|
||||
rule
|
||||
}
|
||||
/** Clears the Ivy cache, as configured by 'config'. */
|
||||
def cleanCache(config: IvyConfiguration) =
|
||||
{
|
||||
def doClean(ivy: Ivy, module: ModuleDescriptor, default: String) =
|
||||
Control.trapUnit("Could not clean cache: ", config.log)
|
||||
{ ivy.getSettings.getRepositoryCacheManagers.foreach(_.clean()); None }
|
||||
|
||||
withIvy(config)(doClean)
|
||||
}
|
||||
/** Creates a Maven pom from the given Ivy configuration*/
|
||||
def makePom(config: IvyConfiguration, extraDependencies: Iterable[ModuleID], configurations: Option[Iterable[Configuration]], output: File) =
|
||||
{
|
||||
def doMakePom(ivy: Ivy, md: ModuleDescriptor, default: String) =
|
||||
Control.trapUnit("Could not make pom: ", config.log)
|
||||
{
|
||||
val module = addLateDependencies(ivy, md, default, extraDependencies)
|
||||
val pomModule = keepConfigurations(module, configurations)
|
||||
PomModuleDescriptorWriter.write(pomModule, DefaultConfigurationMapping, output)
|
||||
config.log.info("Wrote " + output.getAbsolutePath)
|
||||
None
|
||||
}
|
||||
withIvy(config)(doMakePom)
|
||||
}
|
||||
private def addDefaultArtifact(defaultConf: String, moduleID: DefaultModuleDescriptor) =
|
||||
moduleID.addArtifact(defaultConf, new MDArtifact(moduleID, moduleID.getModuleRevisionId.getName, defaultType, defaultExtension))
|
||||
// todo: correct default configuration for extra dependencies
|
||||
private def addLateDependencies(ivy: Ivy, md: ModuleDescriptor, defaultConfiguration: String, extraDependencies: Iterable[ModuleID]) =
|
||||
{
|
||||
val module = toDefaultModuleDescriptor(md)
|
||||
val parser = new CustomXmlParser.CustomParser(ivy.getSettings)
|
||||
parser.setMd(module)
|
||||
val defaultConf = if(defaultConfiguration.contains("->")) defaultConfiguration else (defaultConfiguration + "->default")
|
||||
parser.setDefaultConf(defaultConf)
|
||||
addDependencies(module, extraDependencies, parser)
|
||||
module
|
||||
}
|
||||
private def getConfigurations(module: ModuleDescriptor, configurations: Option[Iterable[Configuration]]) =
|
||||
configurations match
|
||||
{
|
||||
case Some(confs) => confs.map(_.name).toList.toArray
|
||||
case None => module.getPublicConfigurationsNames
|
||||
}
|
||||
/** Retain dependencies only with the configurations given, or all public configurations of `module` if `configurations` is None.
|
||||
* This is currently only preserves the information required by makePom*/
|
||||
private def keepConfigurations(module: ModuleDescriptor, configurations: Option[Iterable[Configuration]]): ModuleDescriptor =
|
||||
{
|
||||
val keepConfigurations = getConfigurations(module, configurations)
|
||||
val keepSet = Set(keepConfigurations.toSeq : _*)
|
||||
def translate(dependency: DependencyDescriptor) =
|
||||
{
|
||||
val keep = dependency.getModuleConfigurations.filter(keepSet.contains)
|
||||
if(keep.isEmpty)
|
||||
None
|
||||
else // TODO: translate the dependency to contain only configurations to keep
|
||||
Some(dependency)
|
||||
}
|
||||
val newModule = new DefaultModuleDescriptor(module.getModuleRevisionId, "", null)
|
||||
newModule.setHomePage(module.getHomePage)
|
||||
for(dependency <- module.getDependencies; translated <- translate(dependency))
|
||||
newModule.addDependency(translated)
|
||||
newModule
|
||||
}
|
||||
private def addConfigurations(configurations: Iterable[String], to: { def setConfs(c: Array[String]): AnyRef }): Unit =
|
||||
to.setConfs(configurations.toList.toArray)
|
||||
|
||||
def deliver(ivyConfig: IvyConfiguration, updateConfig: UpdateConfiguration, status: String, deliverIvyPattern: String, extraDependencies: Iterable[ModuleID], configurations: Option[Iterable[Configuration]]) =
|
||||
{
|
||||
def doDeliver(ivy: Ivy, md: ModuleDescriptor, default: String) =
|
||||
Control.trapUnit("Could not deliver: ", ivyConfig.log)
|
||||
{
|
||||
val module = addLateDependencies(ivy, md, default, extraDependencies)
|
||||
resolve(ivy, updateConfig, module) orElse
|
||||
{
|
||||
val revID = module.getModuleRevisionId
|
||||
val options = DeliverOptions.newInstance(ivy.getSettings).setStatus(status)
|
||||
options.setConfs(getConfigurations(module, configurations))
|
||||
|
||||
ivy.deliver(revID, revID.getRevision, deliverIvyPattern, options)
|
||||
None
|
||||
}
|
||||
}
|
||||
withIvy(ivyConfig)(doDeliver)
|
||||
}
|
||||
// todo: map configurations, extra dependencies
|
||||
def publish(ivyConfig: IvyConfiguration, resolverName: String, srcArtifactPatterns: Iterable[String], deliveredIvyPattern: Option[String], configurations: Option[Iterable[Configuration]]) =
|
||||
{
|
||||
def doPublish(ivy: Ivy, md: ModuleDescriptor, default: String) =
|
||||
Control.trapUnit("Could not publish: ", ivyConfig.log)
|
||||
{
|
||||
val revID = md.getModuleRevisionId
|
||||
val patterns = new java.util.ArrayList[String]
|
||||
srcArtifactPatterns.foreach(pattern => patterns.add(pattern))
|
||||
val options = (new PublishOptions).setOverwrite(true)
|
||||
deliveredIvyPattern.foreach(options.setSrcIvyPattern)
|
||||
options.setConfs(getConfigurations(md, configurations))
|
||||
ivy.publish(revID, patterns, resolverName, options)
|
||||
None
|
||||
}
|
||||
withIvy(ivyConfig)(doPublish)
|
||||
}
|
||||
/** Resolves and retrieves dependencies. 'ivyConfig' is used to produce an Ivy file and configuration.
|
||||
* 'updateConfig' configures the actual resolution and retrieval process. */
|
||||
def update(ivyConfig: IvyConfiguration, updateConfig: UpdateConfiguration) =
|
||||
{
|
||||
def processModule(ivy: Ivy, module: ModuleDescriptor, default: String) =
|
||||
{
|
||||
import updateConfig._
|
||||
Control.trapUnit("Could not process dependencies: ", ivyConfig.log)
|
||||
{
|
||||
resolve(ivy, updateConfig, module) orElse
|
||||
{
|
||||
val retrieveOptions = new RetrieveOptions
|
||||
retrieveOptions.setSync(synchronize)
|
||||
val patternBase = ivyConfig.paths.managedLibDirectory.absolutePath
|
||||
val pattern =
|
||||
if(patternBase.endsWith(File.separator))
|
||||
patternBase + outputPattern
|
||||
else
|
||||
patternBase + File.separatorChar + outputPattern
|
||||
ivy.retrieve(module.getModuleRevisionId, pattern, retrieveOptions)
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
withIvy(ivyConfig)(processModule)
|
||||
}
|
||||
private def resolve(ivy: Ivy, updateConfig: UpdateConfiguration, module: ModuleDescriptor) =
|
||||
{
|
||||
import updateConfig._
|
||||
val resolveOptions = new ResolveOptions
|
||||
if(quiet)
|
||||
resolveOptions.setLog(LogOptions.LOG_DOWNLOAD_ONLY)
|
||||
val resolveReport = ivy.resolve(module, resolveOptions)
|
||||
if(resolveReport.hasError)
|
||||
Some(Set(resolveReport.getAllProblemMessages.toArray: _*).mkString(System.getProperty("line.separator")))
|
||||
else
|
||||
None
|
||||
}
|
||||
/** This method is used to add inline dependencies to the provided module. */
|
||||
private def addDependencies(moduleID: DefaultModuleDescriptor, dependencies: Iterable[ModuleID], parser: CustomXmlParser.CustomParser)
|
||||
{
|
||||
for(dependency <- dependencies)
|
||||
{
|
||||
val dependencyDescriptor = new DefaultDependencyDescriptor(moduleID, toID(dependency), false, dependency.isChanging, dependency.isTransitive)
|
||||
dependency.configurations match
|
||||
{
|
||||
case None => // The configuration for this dependency was not explicitly specified, so use the default
|
||||
parser.parseDepsConfs(parser.getDefaultConf, dependencyDescriptor)
|
||||
case Some(confs) => // The configuration mapping (looks like: test->default) was specified for this dependency
|
||||
parser.parseDepsConfs(confs, dependencyDescriptor)
|
||||
}
|
||||
for(artifact <- dependency.explicitArtifacts)
|
||||
{
|
||||
import artifact.{name, `type`, extension, url}
|
||||
val ivyArtifact = new DefaultDependencyArtifactDescriptor(dependencyDescriptor, name, `type`, extension, url.getOrElse(null), null)
|
||||
for(conf <- dependencyDescriptor.getModuleConfigurations)
|
||||
dependencyDescriptor.addDependencyArtifact(conf, ivyArtifact)
|
||||
}
|
||||
moduleID.addDependency(dependencyDescriptor)
|
||||
}
|
||||
}
|
||||
private def addArtifacts(moduleID: DefaultModuleDescriptor, artifacts: Iterable[Artifact])
|
||||
{
|
||||
val allConfigurations = moduleID.getPublicConfigurationsNames
|
||||
for(artifact <- artifacts)
|
||||
{
|
||||
val configurationStrings =
|
||||
{
|
||||
val artifactConfigurations = artifact.configurations
|
||||
if(artifactConfigurations.isEmpty)
|
||||
allConfigurations
|
||||
else
|
||||
artifactConfigurations.map(_.name)
|
||||
}
|
||||
val ivyArtifact = toIvyArtifact(moduleID, artifact, configurationStrings)
|
||||
configurationStrings.foreach(configuration => moduleID.addArtifact(configuration, ivyArtifact))
|
||||
}
|
||||
}
|
||||
private def toURL(file: File) = file.toURI.toURL
|
||||
/** Adds the ivy.xml main artifact. */
|
||||
private def addMainArtifact(moduleID: DefaultModuleDescriptor)
|
||||
{
|
||||
val artifact = DefaultArtifact.newIvyArtifact(moduleID.getResolvedModuleRevisionId, moduleID.getPublicationDate)
|
||||
moduleID.setModuleArtifact(artifact)
|
||||
moduleID.check()
|
||||
}
|
||||
/** Sets the resolvers for 'settings' to 'resolvers'. This is done by creating a new chain and making it the default. */
|
||||
private def addResolvers(settings: IvySettings, resolvers: Iterable[Resolver], log: Logger)
|
||||
{
|
||||
val newDefault = new ChainResolver
|
||||
newDefault.setName("redefined-public")
|
||||
resolvers.foreach(r => newDefault.add(ConvertResolver(r)))
|
||||
newDefault.add(settings.getDefaultResolver)
|
||||
settings.addResolver(newDefault)
|
||||
settings.setDefaultResolver(newDefault.getName)
|
||||
if(log.atLevel(Level.Debug))
|
||||
{
|
||||
log.debug("Using extra repositories:")
|
||||
resolvers.foreach(r => log.debug("\t" + r.toString))
|
||||
}
|
||||
}
|
||||
private def toIvyConfiguration(configuration: Configuration) =
|
||||
{
|
||||
import org.apache.ivy.core.module.descriptor.{Configuration => IvyConfig}
|
||||
import IvyConfig.Visibility._
|
||||
import configuration._
|
||||
new IvyConfig(name, if(isPublic) PUBLIC else PRIVATE, description, extendsConfigs.map(_.name).toArray, transitive, null)
|
||||
}
|
||||
/** Converts the given sbt module id into an Ivy ModuleRevisionId.*/
|
||||
private def toID(m: ModuleID) =
|
||||
{
|
||||
import m._
|
||||
ModuleRevisionId.newInstance(organization, name, revision)
|
||||
}
|
||||
private def toIvyArtifact(moduleID: ModuleDescriptor, a: Artifact, configurations: Iterable[String]): MDArtifact =
|
||||
{
|
||||
val artifact = new MDArtifact(moduleID, a.name, a.`type`, a.extension)
|
||||
configurations.foreach(artifact.addConfiguration)
|
||||
artifact
|
||||
}
|
||||
/** An implementation of Ivy's Resource class that provides the Ivy file from a byte array. This is used to support
|
||||
* inline Ivy file XML.*/
|
||||
private class ByteResource(bytes: Array[Byte]) extends
|
||||
BasicResource("Inline XML dependencies", true, bytes.length, System.currentTimeMillis, true)
|
||||
{
|
||||
override def openStream = new java.io.ByteArrayInputStream(bytes)
|
||||
}
|
||||
/** Subclasses the default Ivy file parser in order to provide access to protected methods.*/
|
||||
private object CustomXmlParser extends XmlModuleDescriptorParser with NotNull
|
||||
{
|
||||
import XmlModuleDescriptorParser.Parser
|
||||
class CustomParser(settings: IvySettings) extends Parser(CustomXmlParser, settings) with NotNull
|
||||
{
|
||||
def setSource(url: URL) =
|
||||
{
|
||||
super.setResource(new URLResource(url))
|
||||
super.setInput(url)
|
||||
}
|
||||
/** Overridden because the super implementation overwrites the module descriptor.*/
|
||||
override def setResource(res: Resource) {}
|
||||
override def setMd(md: DefaultModuleDescriptor) = super.setMd(md)
|
||||
override def parseDepsConfs(confs: String, dd: DefaultDependencyDescriptor) = super.parseDepsConfs(confs, dd)
|
||||
override def getDefaultConf = super.getDefaultConf
|
||||
override def setDefaultConf(conf: String) = super.setDefaultConf(conf)
|
||||
}
|
||||
}
|
||||
/** This code converts the given ModuleDescriptor to a DefaultModuleDescriptor by casting or generating an error.
|
||||
* Ivy always produces a DefaultModuleDescriptor, so this should be reasonable. */
|
||||
private def toDefaultModuleDescriptor(md: ModuleDescriptor) =
|
||||
md match
|
||||
{
|
||||
case dmd: DefaultModuleDescriptor => dmd
|
||||
case _ => error("Unknown ModuleDescriptor type.")
|
||||
}
|
||||
}
|
||||
|
||||
private object ConvertResolver
|
||||
{
|
||||
/** Converts the given sbt resolver into an Ivy resolver..*/
|
||||
def apply(r: Resolver) =
|
||||
{
|
||||
r match
|
||||
{
|
||||
case repo: MavenRepository =>
|
||||
{
|
||||
val resolver = new IBiblioResolver
|
||||
initializeMavenStyle(resolver, repo.name, repo.root)
|
||||
resolver
|
||||
}
|
||||
case JavaNet1Repository =>
|
||||
{
|
||||
// Thanks to Matthias Pfau for posting how to use the Maven 1 repository on java.net with Ivy:
|
||||
// http://www.nabble.com/Using-gradle-Ivy-with-special-maven-repositories-td23775489.html
|
||||
val resolver = new IBiblioResolver { override def convertM2IdForResourceSearch(mrid: ModuleRevisionId) = mrid }
|
||||
initializeMavenStyle(resolver, JavaNet1Repository.name, "http://download.java.net/maven/1/")
|
||||
resolver.setPattern("[organisation]/[ext]s/[module]-[revision](-[classifier]).[ext]")
|
||||
resolver
|
||||
}
|
||||
case repo: SshRepository =>
|
||||
{
|
||||
val resolver = new SshResolver
|
||||
initializeSSHResolver(resolver, repo)
|
||||
repo.publishPermissions.foreach(perm => resolver.setPublishPermissions(perm))
|
||||
resolver
|
||||
}
|
||||
case repo: SftpRepository =>
|
||||
{
|
||||
val resolver = new SFTPResolver
|
||||
initializeSSHResolver(resolver, repo)
|
||||
resolver
|
||||
}
|
||||
case repo: FileRepository =>
|
||||
{
|
||||
val resolver = new FileSystemResolver
|
||||
resolver.setName(repo.name)
|
||||
initializePatterns(resolver, repo.patterns)
|
||||
import repo.configuration.{isLocal, isTransactional}
|
||||
resolver.setLocal(isLocal)
|
||||
isTransactional.foreach(value => resolver.setTransactional(value.toString))
|
||||
resolver
|
||||
}
|
||||
case repo: URLRepository =>
|
||||
{
|
||||
val resolver = new URLResolver
|
||||
resolver.setName(repo.name)
|
||||
initializePatterns(resolver, repo.patterns)
|
||||
resolver
|
||||
}
|
||||
}
|
||||
}
|
||||
private def initializeMavenStyle(resolver: IBiblioResolver, name: String, root: String)
|
||||
{
|
||||
resolver.setName(name)
|
||||
resolver.setM2compatible(true)
|
||||
resolver.setRoot(root)
|
||||
}
|
||||
private def initializeSSHResolver(resolver: AbstractSshBasedResolver, repo: SshBasedRepository)
|
||||
{
|
||||
resolver.setName(repo.name)
|
||||
resolver.setPassfile(null)
|
||||
initializePatterns(resolver, repo.patterns)
|
||||
initializeConnection(resolver, repo.connection)
|
||||
}
|
||||
private def initializeConnection(resolver: AbstractSshBasedResolver, connection: RepositoryHelpers.SshConnection)
|
||||
{
|
||||
import resolver._
|
||||
import connection._
|
||||
hostname.foreach(setHost)
|
||||
port.foreach(setPort)
|
||||
authentication foreach
|
||||
{
|
||||
case RepositoryHelpers.PasswordAuthentication(user, password) =>
|
||||
setUser(user)
|
||||
setUserPassword(password)
|
||||
case RepositoryHelpers.KeyFileAuthentication(file, password) =>
|
||||
setKeyFile(file)
|
||||
setKeyFilePassword(password)
|
||||
}
|
||||
}
|
||||
private def initializePatterns(resolver: AbstractPatternsBasedResolver, patterns: RepositoryHelpers.Patterns)
|
||||
{
|
||||
resolver.setM2compatible(patterns.isMavenCompatible)
|
||||
patterns.ivyPatterns.foreach(resolver.addIvyPattern)
|
||||
patterns.artifactPatterns.foreach(resolver.addArtifactPattern)
|
||||
}
|
||||
}
|
||||
|
||||
private object DefaultConfigurationMapping extends PomModuleDescriptorWriter.ConfigurationScopeMapping(new java.util.HashMap)
|
||||
{
|
||||
override def getScope(confs: Array[String]) =
|
||||
{
|
||||
Configurations.defaultMavenConfigurations.find(conf => confs.contains(conf.name)) match
|
||||
{
|
||||
case Some(conf) => conf.name
|
||||
case None =>
|
||||
if(confs.isEmpty || confs(0) == Configurations.Default.name)
|
||||
null
|
||||
else
|
||||
confs(0)
|
||||
}
|
||||
}
|
||||
override def isOptional(confs: Array[String]) = confs.isEmpty || (confs.length == 1 && confs(0) == Configurations.Optional.name)
|
||||
}
|
||||
|
||||
/** Interface between Ivy logging and sbt logging. */
|
||||
private final class IvyLogger(log: Logger) extends MessageLogger
|
||||
{
|
||||
private var progressEnabled = false
|
||||
|
||||
def log(msg: String, level: Int)
|
||||
{
|
||||
import Message.{MSG_DEBUG, MSG_VERBOSE, MSG_INFO, MSG_WARN, MSG_ERR}
|
||||
level match
|
||||
{
|
||||
case MSG_DEBUG | MSG_VERBOSE => debug(msg)
|
||||
case MSG_INFO => info(msg)
|
||||
case MSG_WARN => warn(msg)
|
||||
case MSG_ERR => error(msg)
|
||||
}
|
||||
}
|
||||
def rawlog(msg: String, level: Int)
|
||||
{
|
||||
log(msg, level)
|
||||
}
|
||||
import Level.{Debug, Info, Warn, Error}
|
||||
def debug(msg: String) = logImpl(msg, Debug)
|
||||
def verbose(msg: String) = debug(msg)
|
||||
def deprecated(msg: String) = warn(msg)
|
||||
def info(msg: String) = logImpl(msg, Info)
|
||||
def rawinfo(msg: String) = info(msg)
|
||||
def warn(msg: String) = logImpl(msg, Warn)
|
||||
def error(msg: String) = logImpl(msg, Error)
|
||||
|
||||
private def logImpl(msg: String, level: Level.Value) = log.log(level, msg)
|
||||
|
||||
private def emptyList = java.util.Collections.emptyList[T forSome { type T}]
|
||||
def getProblems = emptyList
|
||||
def getWarns = emptyList
|
||||
def getErrors = emptyList
|
||||
|
||||
def clearProblems = ()
|
||||
def sumupProblems = ()
|
||||
def progress = ()
|
||||
def endProgress = ()
|
||||
|
||||
def endProgress(msg: String) = info(msg)
|
||||
def isShowProgress = false
|
||||
def setShowProgress(progress: Boolean) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,376 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import java.io.File
|
||||
import java.net.{URI, URL}
|
||||
import scala.xml.NodeSeq
|
||||
import org.apache.ivy.plugins.resolver.IBiblioResolver
|
||||
import org.apache.ivy.util.url.CredentialsStore
|
||||
|
||||
sealed abstract class Manager extends NotNull
|
||||
/** This explicitly requests auto detection as a dependency manager. It will first check for a 'pom.xml' file and if that does not exist, an 'ivy.xml' file.
|
||||
* Ivy is configured using the detected file or uses defaults.*/
|
||||
final class AutoDetectManager(val module: ModuleID) extends Manager
|
||||
/** This explicitly requests that the Maven pom 'pom' be used to determine dependencies. An Ivy configuration file to use may be specified in
|
||||
* 'configuration', since Ivy currently cannot extract Maven repositories from a pom file. Otherwise, defaults are used.*/
|
||||
final class MavenManager(val configuration: Option[Path], val pom: Path) extends Manager
|
||||
/** This explicitly requests that the Ivy file 'dependencies' be used to determine dependencies. An Ivy configuration file to use may be specified in
|
||||
* 'configuration'. Otherwise, defaults are used.*/
|
||||
final class IvyManager(val configuration: Option[Path], val dependencies: Path) extends Manager
|
||||
/** This manager directly specifies the dependencies, resolvers, and configurations through sbt wrapper classes and through an in-memory
|
||||
* Ivy XML file. */
|
||||
sealed trait SbtManager extends Manager
|
||||
{
|
||||
def autodetect: Boolean
|
||||
def module: ModuleID
|
||||
def resolvers: Iterable[Resolver]
|
||||
def dependencies: Iterable[ModuleID]
|
||||
def autodetectUnspecified: Boolean
|
||||
def dependenciesXML: NodeSeq
|
||||
def configurations: Iterable[Configuration]
|
||||
def defaultConfiguration: Option[Configuration]
|
||||
def artifacts: Iterable[Artifact]
|
||||
}
|
||||
final class SimpleManager private[sbt] (val dependenciesXML: NodeSeq, val autodetectUnspecified: Boolean,
|
||||
val module: ModuleID, val resolvers: Iterable[Resolver], explicitConfigurations: Iterable[Configuration],
|
||||
val defaultConfiguration: Option[Configuration], val artifacts: Iterable[Artifact], val dependencies: ModuleID*) extends SbtManager
|
||||
{
|
||||
def autodetect = dependencies.isEmpty && dependenciesXML.isEmpty && artifacts.isEmpty && explicitConfigurations.isEmpty && autodetectUnspecified
|
||||
def configurations =
|
||||
if(explicitConfigurations.isEmpty && !autodetect)
|
||||
{
|
||||
defaultConfiguration match
|
||||
{
|
||||
case Some(Configurations.DefaultIvyConfiguration) => Configurations.Default :: Nil
|
||||
case Some(Configurations.DefaultMavenConfiguration) => Configurations.defaultMavenConfigurations
|
||||
case _ => Nil
|
||||
}
|
||||
}
|
||||
else
|
||||
explicitConfigurations
|
||||
}
|
||||
|
||||
final case class ModuleID(organization: String, name: String, revision: String, configurations: Option[String], isChanging: Boolean, isTransitive: Boolean, explicitArtifacts: Seq[Artifact]) extends NotNull
|
||||
{
|
||||
override def toString = organization + ":" + name + ":" + revision
|
||||
// () required for chaining
|
||||
def notTransitive() = intransitive()
|
||||
def intransitive() = ModuleID(organization, name, revision, configurations, isChanging, false, explicitArtifacts)
|
||||
def changing() = ModuleID(organization, name, revision, configurations, true, isTransitive, explicitArtifacts)
|
||||
def from(url: String) = artifacts(Artifact(name, new URL(url)))
|
||||
def artifacts(newArtifacts: Artifact*) = ModuleID(organization, name, revision, configurations, isChanging, isTransitive, newArtifacts ++ explicitArtifacts)
|
||||
}
|
||||
object ModuleID
|
||||
{
|
||||
def apply(organization: String, name: String, revision: String): ModuleID = ModuleID(organization, name, revision, None)
|
||||
def apply(organization: String, name: String, revision: String, configurations: Option[String]): ModuleID =
|
||||
ModuleID(organization, name, revision, configurations, false, true)
|
||||
def apply(organization: String, name: String, revision: String, configurations: Option[String], isChanging: Boolean, isTransitive: Boolean): ModuleID =
|
||||
ModuleID(organization, name, revision, configurations, isChanging, isTransitive, Nil)
|
||||
}
|
||||
sealed trait Resolver extends NotNull
|
||||
{
|
||||
def name: String
|
||||
}
|
||||
sealed case class MavenRepository(name: String, root: String) extends Resolver
|
||||
{
|
||||
override def toString = name + ": " + root
|
||||
}
|
||||
|
||||
object RepositoryHelpers
|
||||
{
|
||||
final case class Patterns(ivyPatterns: Seq[String], artifactPatterns: Seq[String], isMavenCompatible: Boolean) extends NotNull
|
||||
{
|
||||
private[sbt] def mavenStyle(): Patterns = Patterns(ivyPatterns, artifactPatterns, true)
|
||||
private[sbt] def withIvys(patterns: Seq[String]): Patterns = Patterns(patterns ++ ivyPatterns, artifactPatterns, isMavenCompatible)
|
||||
private[sbt] def withArtifacts(patterns: Seq[String]): Patterns = Patterns(ivyPatterns, patterns ++ artifactPatterns, isMavenCompatible)
|
||||
}
|
||||
final case class SshConnection(authentication: Option[SshAuthentication], hostname: Option[String], port: Option[Int]) extends NotNull
|
||||
{
|
||||
def copy(authentication: Option[SshAuthentication]) = SshConnection(authentication, hostname, port)
|
||||
}
|
||||
/** Configuration specific to an Ivy filesystem resolver. */
|
||||
final case class FileConfiguration(isLocal: Boolean, isTransactional: Option[Boolean]) extends NotNull
|
||||
{
|
||||
def transactional() = FileConfiguration(isLocal, Some(true))
|
||||
def nontransactional() = FileConfiguration(isLocal, Some(false))
|
||||
def nonlocal() = FileConfiguration(false, isTransactional)
|
||||
}
|
||||
sealed trait SshAuthentication extends NotNull
|
||||
final case class PasswordAuthentication(user: String, password: String) extends SshAuthentication
|
||||
final case class KeyFileAuthentication(keyfile: File, password: String) extends SshAuthentication
|
||||
}
|
||||
import RepositoryHelpers.{Patterns, SshConnection, FileConfiguration}
|
||||
import RepositoryHelpers.{KeyFileAuthentication, PasswordAuthentication, SshAuthentication}
|
||||
|
||||
/** sbt interface to an Ivy repository based on patterns, which is most Ivy repositories.*/
|
||||
sealed abstract class PatternsBasedRepository extends Resolver
|
||||
{
|
||||
type RepositoryType <: PatternsBasedRepository
|
||||
/** Should be implemented to create a new copy of this repository but with `patterns` as given.*/
|
||||
protected def copy(patterns: Patterns): RepositoryType
|
||||
|
||||
/** The object representing the configured patterns for this repository. */
|
||||
def patterns: Patterns
|
||||
|
||||
/** Enables maven 2 compatibility for this repository. */
|
||||
def mavenStyle() = copy(patterns.mavenStyle())
|
||||
/** Adds the given patterns for resolving/publishing Ivy files.*/
|
||||
def ivys(ivyPatterns: String*): RepositoryType = copy(patterns.withIvys(ivyPatterns))
|
||||
/** Adds the given patterns for resolving/publishing artifacts.*/
|
||||
def artifacts(artifactPatterns: String*): RepositoryType = copy(patterns.withArtifacts(artifactPatterns))
|
||||
}
|
||||
/** sbt interface for an Ivy filesystem repository. More convenient construction is done using Resolver.file. */
|
||||
final case class FileRepository(name: String, configuration: FileConfiguration, patterns: Patterns) extends PatternsBasedRepository
|
||||
{
|
||||
type RepositoryType = FileRepository
|
||||
protected def copy(patterns: Patterns): FileRepository = FileRepository(name, configuration, patterns)
|
||||
private def copy(configuration: FileConfiguration) = FileRepository(name, configuration, patterns)
|
||||
def transactional() = copy(configuration.transactional())
|
||||
def nonlocal() = copy(configuration.nonlocal())
|
||||
}
|
||||
final case class URLRepository(name: String, patterns: Patterns) extends PatternsBasedRepository
|
||||
{
|
||||
type RepositoryType = URLRepository
|
||||
protected def copy(patterns: Patterns): URLRepository = URLRepository(name, patterns)
|
||||
}
|
||||
/** sbt interface for an Ivy ssh-based repository (ssh and sftp). Requires the Jsch library.. */
|
||||
sealed abstract class SshBasedRepository extends PatternsBasedRepository
|
||||
{
|
||||
type RepositoryType <: SshBasedRepository
|
||||
protected def copy(connection: SshConnection): RepositoryType
|
||||
private def copy(authentication: SshAuthentication): RepositoryType = copy(connection.copy(Some(authentication)))
|
||||
|
||||
/** The object representing the configured ssh connection for this repository. */
|
||||
def connection: SshConnection
|
||||
|
||||
/** Configures this to use the specified user name and password when connecting to the remote repository. */
|
||||
def as(user: String, password: String): RepositoryType = copy(new PasswordAuthentication(user, password))
|
||||
/** Configures this to use the specified keyfile and password for the keyfile when connecting to the remote repository. */
|
||||
def as(keyfile: File, password: String): RepositoryType = copy(new KeyFileAuthentication(keyfile, password))
|
||||
}
|
||||
/** sbt interface for an Ivy repository over ssh. More convenient construction is done using Resolver.ssh. */
|
||||
final case class SshRepository(name: String, connection: SshConnection, patterns: Patterns, publishPermissions: Option[String]) extends SshBasedRepository
|
||||
{
|
||||
type RepositoryType = SshRepository
|
||||
protected def copy(patterns: Patterns): SshRepository = SshRepository(name, connection, patterns, publishPermissions)
|
||||
protected def copy(connection: SshConnection): SshRepository = SshRepository(name, connection, patterns, publishPermissions)
|
||||
/** Defines the permissions to set when publishing to this repository. */
|
||||
def withPermissions(publishPermissions: String): SshRepository = withPermissions(Some(publishPermissions))
|
||||
def withPermissions(publishPermissions: Option[String]): SshRepository = SshRepository(name, connection, patterns, publishPermissions)
|
||||
}
|
||||
/** sbt interface for an Ivy repository over sftp. More convenient construction is done using Resolver.sftp. */
|
||||
final case class SftpRepository(name: String, connection: SshConnection, patterns: Patterns) extends SshBasedRepository
|
||||
{
|
||||
type RepositoryType = SftpRepository
|
||||
protected def copy(patterns: Patterns): SftpRepository = SftpRepository(name, connection, patterns)
|
||||
protected def copy(connection: SshConnection): SftpRepository = SftpRepository(name, connection, patterns)
|
||||
}
|
||||
|
||||
import Resolver._
|
||||
object ScalaToolsReleases extends MavenRepository(ScalaToolsReleasesName, ScalaToolsReleasesRoot)
|
||||
object ScalaToolsSnapshots extends MavenRepository(ScalaToolsSnapshotsName, ScalaToolsSnapshotsRoot)
|
||||
object DefaultMavenRepository extends MavenRepository("Maven2 Repository", IBiblioResolver.DEFAULT_M2_ROOT)
|
||||
object JavaNet1Repository extends Resolver
|
||||
{
|
||||
def name = "java.net Maven1 Repository"
|
||||
}
|
||||
|
||||
object Resolver
|
||||
{
|
||||
val ScalaToolsReleasesName = "Scala-Tools Maven2 Repository"
|
||||
val ScalaToolsSnapshotsName = "Scala-Tools Maven2 Snapshots Repository"
|
||||
val ScalaToolsReleasesRoot = "http://scala-tools.org/repo-releases"
|
||||
val ScalaToolsSnapshotsRoot = "http://scala-tools.org/repo-snapshots"
|
||||
|
||||
/** A base class for defining factories for interfaces to Ivy repositories that require a hostname , port, and patterns. */
|
||||
sealed abstract class Define[RepositoryType <: SshBasedRepository] extends NotNull
|
||||
{
|
||||
/** Subclasses should implement this method to */
|
||||
protected def construct(name: String, connection: SshConnection, patterns: Patterns): RepositoryType
|
||||
/** Constructs this repository type with the given `name`. `basePatterns` are the initial patterns to use. A ManagedProject
|
||||
* has an implicit defining these initial patterns based on a setting for either Maven or Ivy style patterns.*/
|
||||
def apply(name: String)(implicit basePatterns: Patterns): RepositoryType =
|
||||
apply(name, None, None, None)
|
||||
/** Constructs this repository type with the given `name` and `hostname`. `basePatterns` are the initial patterns to use.
|
||||
* A ManagedProject has an implicit defining these initial patterns based on a setting for either Maven or Ivy style patterns.*/
|
||||
def apply(name: String, hostname: String)(implicit basePatterns: Patterns): RepositoryType =
|
||||
apply(name, Some(hostname), None, None)
|
||||
/** Constructs this repository type with the given `name`, `hostname`, and the `basePath` against which the initial
|
||||
* patterns will be resolved. `basePatterns` are the initial patterns to use.
|
||||
* A ManagedProject has an implicit defining these initial patterns based on a setting for either Maven or Ivy style patterns.*/
|
||||
def apply(name: String, hostname: String, basePath: String)(implicit basePatterns: Patterns): RepositoryType =
|
||||
apply(name, Some(hostname), None, Some(basePath))
|
||||
/** Constructs this repository type with the given `name`, `hostname`, and `port`. `basePatterns` are the initial patterns to use.
|
||||
* A ManagedProject has an implicit defining these initial patterns based on a setting for either Maven or Ivy style patterns.*/
|
||||
def apply(name: String, hostname: String, port: Int)(implicit basePatterns: Patterns): RepositoryType =
|
||||
apply(name, Some(hostname), Some(port), None)
|
||||
/** Constructs this repository type with the given `name`, `hostname`, `port`, and the `basePath` against which the initial
|
||||
* patterns will be resolved. `basePatterns` are the initial patterns to use.
|
||||
* A ManagedProject has an implicit defining these initial patterns based on a setting for either Maven or Ivy style patterns.*/
|
||||
def apply(name: String, hostname: String, port: Int, basePath: String)(implicit basePatterns: Patterns): RepositoryType =
|
||||
apply(name, Some(hostname), Some(port), Some(basePath))
|
||||
/** Constructs this repository type with the given `name`, `hostname`, `port`, and the `basePath` against which the initial
|
||||
* patterns will be resolved. `basePatterns` are the initial patterns to use. All but the `name` are optional (use None).
|
||||
* A ManagedProject has an implicit defining these initial patterns based on a setting for either Maven or Ivy style patterns.*/
|
||||
def apply(name: String, hostname: Option[String], port: Option[Int], basePath: Option[String])(implicit basePatterns: Patterns): RepositoryType =
|
||||
construct(name, SshConnection(None, hostname, port), resolvePatterns(basePath, basePatterns))
|
||||
}
|
||||
/** A factory to construct an interface to an Ivy SSH resolver.*/
|
||||
object ssh extends Define[SshRepository]
|
||||
{
|
||||
protected def construct(name: String, connection: SshConnection, patterns: Patterns) = SshRepository(name, connection, patterns, None)
|
||||
}
|
||||
/** A factory to construct an interface to an Ivy SFTP resolver.*/
|
||||
object sftp extends Define[SftpRepository]
|
||||
{
|
||||
protected def construct(name: String, connection: SshConnection, patterns: Patterns) = SftpRepository(name, connection, patterns)
|
||||
}
|
||||
/** A factory to construct an interface to an Ivy filesytem resolver. */
|
||||
object file
|
||||
{
|
||||
/** Constructs a file resolver with the given name. The patterns to use must be explicitly specified
|
||||
* using the `ivys` or `artifacts` methods on the constructed resolver object.*/
|
||||
def apply(name: String): FileRepository = FileRepository(name, defaultFileConfiguration, ivyStylePatterns)
|
||||
/** Constructs a file resolver with the given name and base directory. */
|
||||
def apply(name: String, baseDirectory: File)(implicit basePatterns: Patterns): FileRepository =
|
||||
baseRepository(baseDirectory.toURI)(FileRepository(name, defaultFileConfiguration, _))
|
||||
}
|
||||
object url
|
||||
{
|
||||
/** Constructs a URL resolver with the given name. The patterns to use must be explicitly specified
|
||||
* using the `ivys` or `artifacts` methods on the constructed resolver object.*/
|
||||
def apply(name: String): URLRepository = URLRepository(name, ivyStylePatterns)
|
||||
/** Constructs a file resolver with the given name and base directory. */
|
||||
def apply(name: String, baseURL: URL)(implicit basePatterns: Patterns): URLRepository =
|
||||
baseRepository(baseURL.toURI)(URLRepository(name, _))
|
||||
}
|
||||
private def baseRepository[T](baseURI: java.net.URI)(construct: Patterns => T)(implicit basePatterns: Patterns): T =
|
||||
construct(resolvePatterns(baseURI.normalize, basePatterns))
|
||||
|
||||
/** If `base` is None, `patterns` is returned unchanged.
|
||||
* Otherwise, the ivy file and artifact patterns in `patterns` are resolved against the given base. */
|
||||
private def resolvePatterns(base: Option[String], patterns: Patterns): Patterns =
|
||||
base match
|
||||
{
|
||||
case Some(path) => resolvePatterns(pathURI(path), patterns)
|
||||
case None => patterns
|
||||
}
|
||||
/** Resolves the ivy file and artifact patterns in `patterns` against the given base. */
|
||||
private def resolvePatterns(base: URI, basePatterns: Patterns): Patterns =
|
||||
{
|
||||
def resolve(pattern: String) = base.resolve(pathURI(pattern)).getPath
|
||||
def resolveAll(patterns: Seq[String]) = patterns.map(resolve)
|
||||
Patterns(resolveAll(basePatterns.ivyPatterns), resolveAll(basePatterns.artifactPatterns), basePatterns.isMavenCompatible)
|
||||
}
|
||||
/** Constructs a `URI` with the path component set to `path` and the other components set to null.*/
|
||||
private def pathURI(path: String) = new URI(null, null, path, null)
|
||||
|
||||
def defaultFileConfiguration = FileConfiguration(true, None)
|
||||
def mavenStylePatterns = Patterns(Nil, mavenStyleBasePattern :: Nil, true)
|
||||
def ivyStylePatterns = Patterns(Nil, Nil, false)
|
||||
|
||||
def defaultPatterns = mavenStylePatterns
|
||||
def mavenStyleBasePattern = "[organisation]/[module]/[revision]/[artifact]-[revision](-[classifier]).[ext]"
|
||||
}
|
||||
|
||||
object Configurations
|
||||
{
|
||||
def config(name: String) = new Configuration(name)
|
||||
def defaultMavenConfigurations = Compile :: Runtime :: Test :: Provided :: System :: Optional :: Sources :: Javadoc :: Nil
|
||||
|
||||
lazy val Default = config("default")
|
||||
lazy val Compile = config("compile")
|
||||
lazy val IntegrationTest = config("it") hide
|
||||
lazy val Provided = config("provided")
|
||||
lazy val Javadoc = config("javadoc")
|
||||
lazy val Runtime = config("runtime")
|
||||
lazy val Test = config("test") hide
|
||||
lazy val Sources = config("sources")
|
||||
lazy val System = config("system")
|
||||
lazy val Optional = config("optional")
|
||||
|
||||
lazy val CompilerPlugin = config("plugin") hide
|
||||
|
||||
private[sbt] val DefaultMavenConfiguration = defaultConfiguration(true)
|
||||
private[sbt] val DefaultIvyConfiguration = defaultConfiguration(false)
|
||||
private[sbt] def DefaultConfiguration(mavenStyle: Boolean) = if(mavenStyle) DefaultMavenConfiguration else DefaultIvyConfiguration
|
||||
private[sbt] def defaultConfiguration(mavenStyle: Boolean) =
|
||||
{
|
||||
val base = if(mavenStyle) Configurations.Compile else Configurations.Default
|
||||
config(base.name + "->default(compile)")
|
||||
}
|
||||
|
||||
private[sbt] def removeDuplicates(configs: Iterable[Configuration]) = Set(scala.collection.mutable.Map(configs.map(config => (config.name, config)).toSeq: _*).values.toList: _*)
|
||||
}
|
||||
/** Represents an Ivy configuration. */
|
||||
final case class Configuration(name: String, description: String, isPublic: Boolean, extendsConfigs: List[Configuration], transitive: Boolean) extends NotNull
|
||||
{
|
||||
require(name != null && !name.isEmpty)
|
||||
require(description != null)
|
||||
def this(name: String) = this(name, "", true, Nil, true)
|
||||
def describedAs(newDescription: String) = Configuration(name, newDescription, isPublic, extendsConfigs, transitive)
|
||||
def extend(configs: Configuration*) = Configuration(name, description, isPublic, configs.toList ::: extendsConfigs, transitive)
|
||||
def notTransitive = intransitive
|
||||
def intransitive = Configuration(name, description, isPublic, extendsConfigs, false)
|
||||
def hide = Configuration(name, description, false, extendsConfigs, transitive)
|
||||
override def toString = name
|
||||
}
|
||||
|
||||
final case class Artifact(name: String, `type`: String, extension: String, configurations: Iterable[Configuration], url: Option[URL]) extends NotNull
|
||||
object Artifact
|
||||
{
|
||||
def apply(name: String): Artifact = Artifact(name, defaultType, defaultExtension, Nil, None)
|
||||
def apply(name: String, `type`: String, extension: String): Artifact = Artifact(name, `type`, extension, Nil, None)
|
||||
def apply(name: String, url: URL): Artifact =Artifact(name, extract(url, defaultType), extract(url, defaultExtension), Nil, Some(url))
|
||||
val defaultExtension = "jar"
|
||||
val defaultType = "jar"
|
||||
private[this] def extract(url: URL, default: String) =
|
||||
{
|
||||
val s = url.toString
|
||||
val i = s.lastIndexOf('.')
|
||||
if(i >= 0)
|
||||
s.substring(i+1)
|
||||
else
|
||||
default
|
||||
}
|
||||
}
|
||||
|
||||
object Credentials
|
||||
{
|
||||
/** Add the provided credentials to Ivy's credentials cache.*/
|
||||
def add(realm: String, host: String, userName: String, passwd: String): Unit =
|
||||
CredentialsStore.INSTANCE.addCredentials(realm, host, userName, passwd)
|
||||
/** Load credentials from the given file into Ivy's credentials cache.*/
|
||||
def apply(file: String, log: Logger): Unit = apply(Path.fromFile(file), log)
|
||||
/** Load credentials from the given file into Ivy's credentials cache.*/
|
||||
def apply(file: File, log: Logger): Unit = apply(Path.fromFile(file), log)
|
||||
/** Load credentials from the given file into Ivy's credentials cache.*/
|
||||
def apply(path: Path, log: Logger)
|
||||
{
|
||||
val msg =
|
||||
if(path.exists)
|
||||
{
|
||||
val properties = new scala.collection.mutable.HashMap[String, String]
|
||||
def get(keys: List[String]) = keys.flatMap(properties.get).firstOption.toRight(keys.head + " not specified in credentials file: " + path)
|
||||
|
||||
impl.MapUtilities.read(properties, path, log) orElse
|
||||
{
|
||||
List.separate( List(RealmKeys, HostKeys, UserKeys, PasswordKeys).map(get) ) match
|
||||
{
|
||||
case (Nil, List(realm, host, user, pass)) => add(realm, host, user, pass); None
|
||||
case (errors, _) => Some(errors.mkString("\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
Some("Credentials file " + path + " does not exist")
|
||||
msg.foreach(x => log.warn(x))
|
||||
}
|
||||
private[this] val RealmKeys = List("realm")
|
||||
private[this] val HostKeys = List("host", "hostname")
|
||||
private[this] val UserKeys = List("user", "user.name", "username")
|
||||
private[this] val PasswordKeys = List("password", "pwd", "pass", "passwd")
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
object ModuleUtilities
|
||||
{
|
||||
def getObject(className: String, loader: ClassLoader) =
|
||||
{
|
||||
val obj = Class.forName(className + "$", true, loader)
|
||||
val singletonField = obj.getField("MODULE$")
|
||||
singletonField.get(null)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import java.io.File
|
||||
import java.util.regex.Pattern
|
||||
|
||||
trait FileFilter extends java.io.FileFilter with NotNull
|
||||
{
|
||||
def || (filter: FileFilter): FileFilter = new SimpleFileFilter( file => accept(file) || filter.accept(file) )
|
||||
def && (filter: FileFilter): FileFilter = new SimpleFileFilter( file => accept(file) && filter.accept(file) )
|
||||
def -- (filter: FileFilter): FileFilter = new SimpleFileFilter( file => accept(file) && !filter.accept(file) )
|
||||
def unary_- : FileFilter = new SimpleFileFilter( file => !accept(file) )
|
||||
}
|
||||
trait NameFilter extends FileFilter with NotNull
|
||||
{
|
||||
def accept(name: String): Boolean
|
||||
final def accept(file: File): Boolean = accept(file.getName)
|
||||
def | (filter: NameFilter): NameFilter = new SimpleFilter( name => accept(name) || filter.accept(name) )
|
||||
def & (filter: NameFilter): NameFilter = new SimpleFilter( name => accept(name) && filter.accept(name) )
|
||||
def - (filter: NameFilter): NameFilter = new SimpleFilter( name => accept(name) && !filter.accept(name) )
|
||||
override def unary_- : NameFilter = new SimpleFilter( name => !accept(name) )
|
||||
}
|
||||
object HiddenFileFilter extends FileFilter {
|
||||
def accept(file: File) = file.isHidden && file.getName != "."
|
||||
}
|
||||
object ExistsFileFilter extends FileFilter {
|
||||
def accept(file: File) = file.exists
|
||||
}
|
||||
object DirectoryFilter extends FileFilter {
|
||||
def accept(file: File) = file.isDirectory
|
||||
}
|
||||
class SimpleFileFilter(val acceptFunction: File => Boolean) extends FileFilter
|
||||
{
|
||||
def accept(file: File) = acceptFunction(file)
|
||||
}
|
||||
class ExactFilter(val matchName: String) extends NameFilter
|
||||
{
|
||||
def accept(name: String) = matchName == name
|
||||
}
|
||||
class SimpleFilter(val acceptFunction: String => Boolean) extends NameFilter
|
||||
{
|
||||
def accept(name: String) = acceptFunction(name)
|
||||
}
|
||||
class PatternFilter(val pattern: Pattern) extends NameFilter
|
||||
{
|
||||
def accept(name: String) = pattern.matcher(name).matches
|
||||
}
|
||||
object AllPassFilter extends NameFilter
|
||||
{
|
||||
def accept(name: String) = true
|
||||
}
|
||||
object NothingFilter extends NameFilter
|
||||
{
|
||||
def accept(name: String) = false
|
||||
}
|
||||
|
||||
object GlobFilter
|
||||
{
|
||||
def apply(expression: String): NameFilter =
|
||||
{
|
||||
require(!expression.exists(java.lang.Character.isISOControl), "Control characters not allowed in filter expression.")
|
||||
if(expression == "*")
|
||||
AllPassFilter
|
||||
else if(expression.indexOf('*') < 0) // includes case where expression is empty
|
||||
new ExactFilter(expression)
|
||||
else
|
||||
new PatternFilter(Pattern.compile(expression.split("\\*", -1).map(quote).mkString(".*")))
|
||||
}
|
||||
private def quote(s: String) = if(s.isEmpty) "" else Pattern.quote(s.replaceAll("\n", """\n"""))
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import java.io.{File, FileOutputStream}
|
||||
import java.util.jar.{JarEntry, JarFile, JarOutputStream, Pack200}
|
||||
import scala.collection.Map
|
||||
import FileUtilities._
|
||||
|
||||
object Pack
|
||||
{
|
||||
def pack(jarPath: Path, out: Path, log: Logger): Option[String] = pack(jarPath, out, defaultPackerOptions, log)
|
||||
def pack(jarPath: Path, out: Path, options: Map[String, String], log: Logger): Option[String] =
|
||||
{
|
||||
val packer = Pack200.newPacker
|
||||
val properties = new wrap.MutableMapWrapper(packer.properties)
|
||||
properties ++= options
|
||||
|
||||
OpenResource.jarFile(false).ioOption(jarPath.asFile, "applying pack200 compression to jar", log) { f =>
|
||||
writeStream(out.asFile, log) { stream =>
|
||||
packer.pack(f, stream)
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
def unpack(packedPath: Path, toJarPath: Path, log: Logger): Option[String] =
|
||||
{
|
||||
val unpacker = Pack200.newUnpacker
|
||||
writeStream(toJarPath.asFile, log) { fileStream =>
|
||||
val jarOut = new JarOutputStream(fileStream)
|
||||
Control.trapUnitAndFinally("Error unpacking '" + packedPath + "': ", log)
|
||||
{ unpacker.unpack(packedPath.asFile, jarOut); None }
|
||||
{ jarOut.close() }
|
||||
}
|
||||
}
|
||||
def defaultPackerOptions: Map[String, String] = scala.collection.immutable.Map()
|
||||
}
|
||||
|
||||
import java.net.URL
|
||||
/** This is somewhat of a mess and is not entirely correct. jarsigner doesn't work properly
|
||||
* on scalaz and it is difficult to determine whether a jar is both signed and valid. */
|
||||
object SignJar
|
||||
{
|
||||
final class SignOption private[SignJar](val toList: List[String], val signOnly: Boolean) extends NotNull
|
||||
{
|
||||
override def toString = toList.mkString(" ")
|
||||
}
|
||||
def keyStore(url: URL) = new SignOption("-keystore" :: url.toExternalForm :: Nil, true)
|
||||
def signedJar(p: Path) = new SignOption("-signedjar" :: p.asFile.getAbsolutePath :: Nil, true)
|
||||
def verbose = new SignOption("-verbose" :: Nil, false)
|
||||
def sigFile(name: String) = new SignOption("-sigfile" :: name :: Nil, true)
|
||||
def storeType(t: String) = new SignOption("-storetype" :: t :: Nil, false)
|
||||
def provider(p: String) = new SignOption("-provider" :: p :: Nil, false)
|
||||
def providerName(p: String) = new SignOption("-providerName" :: p :: Nil, false)
|
||||
def storePassword(p: String) = new SignOption("-storepass" :: p :: Nil, true)
|
||||
def keyPassword(p: String) = new SignOption("-keypass" :: p :: Nil, true)
|
||||
|
||||
private def VerifyOption = "-verify"
|
||||
|
||||
/** Uses jarsigner to sign the given jar. */
|
||||
def sign(jarPath: Path, alias: String, options: Seq[SignOption], log: Logger): Option[String] =
|
||||
{
|
||||
require(!alias.trim.isEmpty, "Alias cannot be empty")
|
||||
val arguments = options.toList.flatMap(_.toList) ::: jarPath.asFile.getAbsolutePath :: alias :: Nil
|
||||
execute("Signed " + jarPath, "signing", arguments, log)
|
||||
}
|
||||
/** Uses jarsigner to verify the given jar.*/
|
||||
def verify(jarPath: Path, options: Seq[SignOption], log: Logger): Option[String] =
|
||||
{
|
||||
val arguments = options.filter(!_.signOnly).toList.flatMap(_.toList) ::: VerifyOption :: jarPath.asFile.getAbsolutePath :: Nil
|
||||
execute("Verified " + jarPath, "verifying", arguments, log)
|
||||
}
|
||||
private def execute(successMessage: String, action: String, arguments: List[String], log: Logger): Option[String] =
|
||||
{
|
||||
val exitCode = Process(CommandName, arguments) ! log
|
||||
if(exitCode == 0)
|
||||
{
|
||||
log.debug(successMessage)
|
||||
None
|
||||
}
|
||||
else
|
||||
Some("Error " + action + " jar (exit code was " + exitCode + ".)")
|
||||
}
|
||||
|
||||
private val CommandName = "jarsigner"
|
||||
}
|
||||
|
|
@ -0,0 +1,464 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
/** This file provides the parallel execution engine of sbt. It is a fairly general module, with pluggable Schedulers and Strategies.
|
||||
*
|
||||
* There are three main componenets to the engine: Distributors, Schedulers, and Strategies.
|
||||
*
|
||||
* A Scheduler provides work that is ready to execute. The main type of Scheduler in sbt is a scheduler
|
||||
* of nodes in a directed, acyclic graph.. This type of scheduler provides work when its
|
||||
* dependencies have finished executing successfully. Another type of scheduler is a MultiScheduler, which draws work
|
||||
* from sub-schedulers.
|
||||
*
|
||||
* A Strategy is used by a Scheduler to select the work to process from the work that is ready. It is notified as work
|
||||
* becomes ready. It is requested to select work to process from the work that is ready. The main Strategy in sbt is the
|
||||
* OrderedStrategy, which prioritizes work according to some ordering defined by its constructor. The primary ordering
|
||||
* used in sbt is based on the longest length of the processing path that includes the node being ordered.
|
||||
*
|
||||
* A Distributor uses a Scheduler to obtain work according up to the maximum work allowed to run at once. It runs each
|
||||
* unit of work in its own Thread.
|
||||
**/
|
||||
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import scala.collection.{immutable, mutable}
|
||||
import immutable.TreeSet
|
||||
|
||||
/** Interface to the Distributor/Scheduler system for running tasks with dependencies described by a directed acyclic graph.*/
|
||||
object ParallelRunner
|
||||
{
|
||||
/** Executes work for nodes in an acyclic directed graph with root node `node`. The name of a node is provided
|
||||
* by the `name` function, the work to perform for a node by `action`, and the logger to use for a node by `log`.
|
||||
* The maximum number of tasks to execute simultaneously is `maximumTasks`. */
|
||||
def run[D <: Dag[D]](node: D, name: D => String, action: D => Option[String], maximumTasks: Int, log: D => Logger): List[WorkFailure[D]] =
|
||||
{
|
||||
val info = DagInfo(node)
|
||||
// Create a strategy that gives each node a uniform self cost and uses the maximum cost to execute it and the nodes that depend on it
|
||||
// to determine which node to run. The self cost could be modified to include more information about a node, such as the size of input files
|
||||
val strategy = defaultStrategy(info)
|
||||
val jobScheduler = CompoundScheduler(new DagScheduler(info, strategy), strategy)
|
||||
val distributor = new Distributor(jobScheduler, action, maximumTasks, log)
|
||||
val result = distributor.run().toList
|
||||
for( WorkFailure(work, message) <- result ) yield WorkFailure(work, "Error running " + name(work) + ": " + message)
|
||||
}
|
||||
def dagScheduler[D <: Dag[D]](node: D) =
|
||||
{
|
||||
val info = DagInfo(node)
|
||||
new DagScheduler(info, defaultStrategy(info))
|
||||
}
|
||||
private def defaultStrategy[D <: Dag[D]](info: DagInfo[D]) = MaxPathStrategy((d: D) => 1, info)
|
||||
}
|
||||
/** Requests work from `scheduler` and processes it using `doWork`. This class limits the amount of work processing at any given time
|
||||
* to `workers`.*/
|
||||
final class Distributor[D](scheduler: Scheduler[D], doWork: D => Option[String], workers: Int, log: D => Logger) extends NotNull
|
||||
{
|
||||
require(workers > 0)
|
||||
final def run(): Iterable[WorkFailure[D]] = (new Run).run()
|
||||
|
||||
private final class Run extends NotNull
|
||||
{
|
||||
private[this] val schedule = scheduler.run
|
||||
/** The number of threads currently running. */
|
||||
private[this] var running = 0
|
||||
/** Pending notifications of completed work. */
|
||||
private[this] val complete = new java.util.concurrent.LinkedBlockingQueue[Done]
|
||||
|
||||
private[Distributor] def run(): Iterable[WorkFailure[D]] =
|
||||
{
|
||||
next()
|
||||
if(isIdle && !schedule.hasPending) // test if all work is complete
|
||||
schedule.failures
|
||||
else
|
||||
{
|
||||
waitForCompletedWork() // wait for some work to complete
|
||||
run() // continue
|
||||
}
|
||||
}
|
||||
// true if the maximum number of worker threads are currently running
|
||||
private def atMaximum = running == workers
|
||||
private def availableWorkers = workers - running
|
||||
// true if no worker threads are currently running
|
||||
private def isIdle = running == 0
|
||||
// process more work
|
||||
private def next()
|
||||
{
|
||||
// if the maximum threads are being used, do nothing
|
||||
// if all work is complete or the scheduler is waiting for current work to complete, do nothing
|
||||
if(!atMaximum && schedule.hasPending)
|
||||
{
|
||||
val nextWork = schedule.next(availableWorkers)
|
||||
val nextSize = nextWork.size
|
||||
assume(nextSize <= availableWorkers, "Scheduler provided more work (" + nextSize + ") than allowed (" + availableWorkers + ")")
|
||||
assume(nextSize > 0 || !isIdle, "Distributor idle and the scheduler indicated work pending, but provided no work.")
|
||||
nextWork.foreach(process)
|
||||
}
|
||||
}
|
||||
// wait on the blocking queue `complete` until some work finishes and notify the scheduler
|
||||
private def waitForCompletedWork()
|
||||
{
|
||||
require(running > 0)
|
||||
val done = complete.take()
|
||||
running -= 1
|
||||
schedule.complete(done.data, done.result)
|
||||
}
|
||||
private def process(data: D)
|
||||
{
|
||||
require(running + 1 <= workers)
|
||||
running += 1
|
||||
new Worker(data).start()
|
||||
}
|
||||
private class Worker(data: D) extends Thread with NotNull
|
||||
{
|
||||
override def run()
|
||||
{
|
||||
val result = Control.trapUnit("", log(data))(doWork(data))
|
||||
complete.put( new Done(result, data) )
|
||||
}
|
||||
}
|
||||
}
|
||||
private final class Done(val result: Option[String], val data: D) extends NotNull
|
||||
}
|
||||
final case class WorkFailure[D](work: D, message: String) extends NotNull
|
||||
{
|
||||
override def toString = message
|
||||
}
|
||||
/** Schedules work of type D. A Scheduler determines what work is ready to be processed.
|
||||
* A Scheduler is itself immutable. It creates a mutable object for each scheduler run.*/
|
||||
trait Scheduler[D] extends NotNull
|
||||
{
|
||||
/** Starts a new run. The returned object is a new Run, representing a single scheduler run. All state for the run
|
||||
* is encapsulated in this object.*/
|
||||
def run: Run
|
||||
trait Run extends NotNull
|
||||
{
|
||||
/** Notifies this scheduler that work has completed with the given result (Some with the error message or None if the work succeeded).*/
|
||||
def complete(d: D, result: Option[String]): Unit
|
||||
/** Returns true if there is any more work to be done, although remaining work can be blocked
|
||||
* waiting for currently running work to complete.*/
|
||||
def hasPending: Boolean
|
||||
/**Returns true if this scheduler has no more work to be done, ever.*/
|
||||
def isComplete: Boolean
|
||||
/** Returns up to 'max' units of work. `max` is always positive. The returned sequence cannot be empty if there is
|
||||
* no work currently being processed.*/
|
||||
def next(max: Int): Seq[D]
|
||||
/** A list of failures that occurred to this point, as reported to the `complete` method. */
|
||||
def failures: Iterable[WorkFailure[D]]
|
||||
}
|
||||
}
|
||||
/** A Strategy selects the work to process from work that is ready to be processed.*/
|
||||
private trait ScheduleStrategy[D] extends NotNull
|
||||
{
|
||||
/** Starts a new run. The returned object is a new Run, representing a single strategy run. All state for the run
|
||||
* is handled through this object and is encapsulated in this object.*/
|
||||
def run: Run
|
||||
trait Run extends NotNull
|
||||
{
|
||||
/** Adds the given work to the list of work that is ready to run.*/
|
||||
def workReady(dep: D): Unit
|
||||
/** Returns true if there is work ready to be run. */
|
||||
def hasReady: Boolean
|
||||
/** Provides up to `max` units of work. `max` is always positive and this method is not called
|
||||
* if hasReady is false. The returned list cannot be empty is there is work ready to be run.*/
|
||||
def next(max: Int): List[D]
|
||||
}
|
||||
}
|
||||
|
||||
/** A scheduler for nodes of a directed-acyclic graph. It requires the root of the graph
|
||||
* and a strategy to select which available nodes to run on limited resources.*/
|
||||
private[sbt] final class DagScheduler[D <: Dag[D]](info: DagInfo[D], strategy: ScheduleStrategy[D]) extends Scheduler[D]
|
||||
{
|
||||
def run: Run = new Run
|
||||
{
|
||||
val infoRun = info.run
|
||||
val strategyRun = strategy.run
|
||||
|
||||
// find nodes that are ready to be run (no dependencies)
|
||||
{
|
||||
val startReady = for( (key, value) <- infoRun.remainingDepsRun if(value.isEmpty)) yield key
|
||||
infoRun.remainingDepsRun --= startReady
|
||||
startReady.foreach(strategyRun.workReady)
|
||||
}
|
||||
|
||||
val failures = new mutable.ListBuffer[WorkFailure[D]]
|
||||
def next(max: Int) = strategyRun.next(max)
|
||||
def complete(work: D, result: Option[String])
|
||||
{
|
||||
result match
|
||||
{
|
||||
case None => infoRun.complete(work, strategyRun.workReady)
|
||||
case Some(errorMessage) =>
|
||||
infoRun.clear(work)
|
||||
failures += WorkFailure(work, errorMessage)
|
||||
}
|
||||
}
|
||||
def isComplete = !strategyRun.hasReady && infoRun.reverseDepsRun.isEmpty
|
||||
// the strategy might not have any work ready if the remaining work needs currently executing work to finish first
|
||||
def hasPending = strategyRun.hasReady || !infoRun.remainingDepsRun.isEmpty
|
||||
}
|
||||
}
|
||||
private object MaxPathStrategy
|
||||
{
|
||||
def apply[D <: Dag[D]](selfCost: D => Int, info: DagInfo[D]): ScheduleStrategy[D] =
|
||||
{
|
||||
val cost = // compute the cost of the longest execution path ending at each node
|
||||
{
|
||||
val cost = new mutable.HashMap[D, Int]
|
||||
def computeCost(work: D): Int = info.reverseDeps.getOrElse(work, immutable.Set.empty[D]).foldLeft(0)(_ max getCost(_)) + selfCost(work)
|
||||
def getCost(work: D): Int = cost.getOrElseUpdate(work, computeCost(work))
|
||||
info.remainingDeps.keys.foreach(getCost)
|
||||
wrap.Wrappers.readOnly(cost)
|
||||
}
|
||||
// create a function to compare units of work. This is not as simple as cost(a) compare cost(b) because it cannot return 0 for
|
||||
// unequal nodes (at least for the Ordered comparison)
|
||||
|
||||
// 2.8.0 uses Ordering
|
||||
implicit val compareOrdering: Ordering[D] =
|
||||
new Ordering[D]
|
||||
{
|
||||
def compare(a: D, b: D) =
|
||||
{
|
||||
val base = cost(a) compare cost(b)
|
||||
if(base == 0)
|
||||
a.hashCode compare b.hashCode // this is required because TreeSet interprets 0 as equal
|
||||
else
|
||||
base
|
||||
}
|
||||
}
|
||||
// 2.7.x uses an implicit view to Ordered
|
||||
implicit val compare =
|
||||
(a: D) => new Ordered[D] {
|
||||
def compare(b: D) = compareOrdering.compare(a, b)
|
||||
}
|
||||
new OrderedStrategy(new TreeSet())
|
||||
}
|
||||
}
|
||||
/** A strategy that adds work to a tree and selects the last key as the next work to be done. */
|
||||
private class OrderedStrategy[D](ready: TreeSet[D]) extends ScheduleStrategy[D]
|
||||
{
|
||||
def run = new Run
|
||||
{
|
||||
private[this] var readyRun = ready
|
||||
def next(max: Int): List[D] = nextImpl(max, Nil)
|
||||
private[this] def nextImpl(remaining: Int, accumulated: List[D]): List[D] =
|
||||
{
|
||||
if(remaining <= 0 || readyRun.isEmpty)
|
||||
accumulated
|
||||
else
|
||||
{
|
||||
val next = readyRun.lastKey
|
||||
readyRun -= next
|
||||
nextImpl(remaining - 1, next :: accumulated)
|
||||
}
|
||||
}
|
||||
def workReady(dep: D) { readyRun += dep }
|
||||
def hasReady = !readyRun.isEmpty
|
||||
}
|
||||
}
|
||||
/** A class that represents state for a DagScheduler and that MaxPathStrategy uses to initialize an OrderedStrategy. */
|
||||
private final class DagInfo[D <: Dag[D]](val remainingDeps: immutable.Map[D, immutable.Set[D]],
|
||||
val reverseDeps: immutable.Map[D, immutable.Set[D]]) extends NotNull
|
||||
{
|
||||
def run = new Run
|
||||
final class Run extends NotNull
|
||||
{
|
||||
val remainingDepsRun = DagInfo.mutableMap(remainingDeps)
|
||||
val reverseDepsRun = DagInfo.mutableMap(reverseDeps)
|
||||
/** Called when work does not complete successfully and so all work that (transitively) depends on the work
|
||||
* must be removed from the maps. */
|
||||
def clear(work: D)
|
||||
{
|
||||
remainingDepsRun -= work
|
||||
foreachReverseDep(work)(clear)
|
||||
}
|
||||
/** Called when work completes properly. `initial` and `ready` are used for a fold over
|
||||
* the work that is now ready to go (becaues it was only waiting for `work` to complete).*/
|
||||
def complete(work: D, ready: D => Unit)
|
||||
{
|
||||
def completed(dependsOnCompleted: D)
|
||||
{
|
||||
for(remainingDependencies <- remainingDepsRun.get(dependsOnCompleted))
|
||||
{
|
||||
remainingDependencies -= work
|
||||
if(remainingDependencies.isEmpty)
|
||||
{
|
||||
remainingDepsRun -= dependsOnCompleted
|
||||
ready(dependsOnCompleted)
|
||||
}
|
||||
}
|
||||
}
|
||||
foreachReverseDep(work)(completed)
|
||||
}
|
||||
private def foreachReverseDep(work: D)(f: D => Unit) { reverseDepsRun.removeKey(work).foreach(_.foreach(f)) }
|
||||
}
|
||||
}
|
||||
/** Constructs forward and reverse dependency map for the given Dag root node. */
|
||||
private object DagInfo
|
||||
{
|
||||
/** Constructs the reverse dependency map from the given Dag and
|
||||
* puts the forward dependencies into a map */
|
||||
def apply[D <: Dag[D]](root: D): DagInfo[D] =
|
||||
{
|
||||
val remainingDeps = new mutable.HashMap[D, immutable.Set[D]]
|
||||
val reverseDeps = new mutable.HashMap[D, mutable.Set[D]]
|
||||
def visitIfUnvisited(node: D): Unit = remainingDeps.getOrElseUpdate(node, processDependencies(node))
|
||||
def processDependencies(node: D): Set[D] =
|
||||
{
|
||||
val workDependencies = node.dependencies
|
||||
workDependencies.foreach(visitIfUnvisited)
|
||||
for(dep <- workDependencies)
|
||||
reverseDeps.getOrElseUpdate(dep, new mutable.HashSet[D]) += node
|
||||
immutable.HashSet(workDependencies.toSeq: _*)
|
||||
}
|
||||
visitIfUnvisited(root)
|
||||
new DagInfo(immutable.HashMap(remainingDeps.toSeq : _*), immute(reverseDeps) )
|
||||
}
|
||||
/** Convert a mutable Map with mutable Sets for values to an immutable Map with immutable Sets for values. */
|
||||
private def immute[D](map: mutable.Map[D, mutable.Set[D]]): immutable.Map[D, immutable.Set[D]] =
|
||||
{
|
||||
val immutedSets = map.map { case (key, value) =>(key, immutable.HashSet(value.toSeq : _*)) }
|
||||
immutable.HashMap(immutedSets.toSeq :_*)
|
||||
}
|
||||
/** Convert an immutable Map with immutable Sets for values to a mutable Map with mutable Sets for values. */
|
||||
private def mutableMap[D](map: immutable.Map[D, immutable.Set[D]]): mutable.Map[D, mutable.Set[D]] =
|
||||
{
|
||||
val mutableSets = map.map { case (key, value) =>(key, mutable.HashSet(value.toSeq : _*)) }
|
||||
mutable.HashMap(mutableSets.toSeq :_*)
|
||||
}
|
||||
}
|
||||
/** A scheduler that can get work from sub-schedulers. The `schedulers` argument to the constructor
|
||||
* is a sequence of the initial schedulers and the key to provide to a client that uses the 'detailedComplete'
|
||||
* method when the scheduler completes its work.*/
|
||||
private final class MultiScheduler[D, T](schedulers: (Scheduler[D], T)*) extends Scheduler[D]
|
||||
{
|
||||
/** Returns a Run instance that represents a scheduler run.*/
|
||||
def run = new MultiRun
|
||||
final class MultiRun extends Run
|
||||
{
|
||||
val owners = new mutable.HashMap[D, Scheduler[D]#Run]
|
||||
val failures = new mutable.ListBuffer[WorkFailure[D]]
|
||||
val schedules = mutable.HashMap[Scheduler[D]#Run, T](schedulers.map { case (scheduler, completeKey) => (scheduler.run, completeKey)} : _*)
|
||||
def +=(schedule: Scheduler[D]#Run, completeKey: T) { schedules(schedule) = completeKey }
|
||||
|
||||
def isComplete = schedules.keys.forall(_.isComplete)
|
||||
def hasPending = schedules.keys.exists(_.hasPending)
|
||||
def next(max: Int) = nextImpl(max, schedules.keys.toList, Nil)
|
||||
|
||||
private def nextImpl(max: Int, remaining: List[Scheduler[D]#Run], accumulatedWork: List[D]): Seq[D] =
|
||||
{
|
||||
if(max == 0 || remaining.isEmpty)
|
||||
accumulatedWork
|
||||
else
|
||||
{
|
||||
val currentSchedule = remaining.head
|
||||
if(currentSchedule.hasPending)
|
||||
{
|
||||
val newWork = currentSchedule.next(max).toList
|
||||
newWork.foreach(work => owners.put(work, currentSchedule))
|
||||
nextImpl(max - newWork.size, remaining.tail, newWork ::: accumulatedWork)
|
||||
}
|
||||
else
|
||||
nextImpl(max, remaining.tail, accumulatedWork)
|
||||
}
|
||||
}
|
||||
|
||||
def complete(work: D, result: Option[String]) { detailedComplete(work, result) }
|
||||
def detailedComplete(work: D, result: Option[String]) =
|
||||
{
|
||||
def complete(forOwner: Scheduler[D]#Run) =
|
||||
{
|
||||
forOwner.complete(work, result)
|
||||
if(forOwner.isComplete)
|
||||
{
|
||||
failures ++= forOwner.failures
|
||||
Some(forOwner, schedules.removeKey(forOwner).get)
|
||||
}
|
||||
else
|
||||
None
|
||||
}
|
||||
owners.removeKey(work).flatMap(complete)
|
||||
}
|
||||
}
|
||||
}
|
||||
/** This scheduler allows a unit of work to provide nested work.
|
||||
*
|
||||
* When a unit of work that implements CompoundWork is returned for processing by `multi`, this scheduler will request the work's
|
||||
* nested scheduler that represents the nested work to be done. The new scheduler will be added to `multi`. When the new scheduler
|
||||
* is finished providing work, a final scheduler is run.*/
|
||||
private final class CompoundScheduler[D](multi: MultiScheduler[D, Option[FinalWork[D]]], finalWorkStrategy: ScheduleStrategy[D]) extends Scheduler[D]
|
||||
{
|
||||
def run: Run = new Run
|
||||
{
|
||||
val multiRun = multi.run
|
||||
val strategyRun = finalWorkStrategy.run
|
||||
|
||||
def isComplete = multiRun.isComplete && !strategyRun.hasReady
|
||||
def hasPending = strategyRun.hasReady || multiRun.hasPending || multiRun.schedules.values.exists(_.isDefined)
|
||||
def complete(work: D, result: Option[String]) =
|
||||
{
|
||||
for( (scheduler, Some(finalWorkTodo)) <- multiRun.detailedComplete(work, result) )
|
||||
{
|
||||
multiRun += (finalWorkTodo.doFinally.run, None)
|
||||
if(scheduler.failures.isEmpty)
|
||||
strategyRun workReady finalWorkTodo.compound
|
||||
else
|
||||
multiRun.complete(finalWorkTodo.compound, Some("One or more subtasks failed"))
|
||||
}
|
||||
}
|
||||
def failures = multiRun.failures
|
||||
def next(max: Int) = nextImpl(max, Nil)
|
||||
private def nextImpl(max: Int, processedNextWork: List[D]): Seq[D] =
|
||||
{
|
||||
if(max > 0)
|
||||
{
|
||||
if(strategyRun.hasReady)
|
||||
{
|
||||
val newWork = strategyRun.next(max)
|
||||
nextImpl(max - newWork.size, newWork ::: processedNextWork)
|
||||
}
|
||||
else if(multiRun.hasPending)
|
||||
{
|
||||
val multiWork = multiRun.next(max)
|
||||
if(multiWork.isEmpty)
|
||||
processedNextWork
|
||||
else
|
||||
{
|
||||
val expandedWork = (processedNextWork /: multiWork)(expand)
|
||||
val remaining = max - (expandedWork.size - processedNextWork.size)
|
||||
nextImpl(remaining, expandedWork)
|
||||
}
|
||||
}
|
||||
else
|
||||
processedNextWork
|
||||
}
|
||||
else
|
||||
processedNextWork
|
||||
}
|
||||
private def expand(accumulate: List[D], work: D): List[D] =
|
||||
{
|
||||
work match
|
||||
{
|
||||
case c: CompoundWork[D] =>
|
||||
val subWork = c.work
|
||||
addFinal(subWork.scheduler, new FinalWork(work, subWork.doFinally))
|
||||
accumulate
|
||||
case _ => work :: accumulate
|
||||
}
|
||||
}
|
||||
private def addFinal(schedule: Scheduler[D], work: FinalWork[D]) { multiRun += (schedule.run, Some(work)) }
|
||||
}
|
||||
}
|
||||
private object CompoundScheduler
|
||||
{
|
||||
def apply[D](scheduler: Scheduler[D], strategy: ScheduleStrategy[D]) : Scheduler[D] =
|
||||
new CompoundScheduler(new MultiScheduler[D, Option[FinalWork[D]]]( (scheduler, None) ), strategy)
|
||||
}
|
||||
private final class FinalWork[D](val compound: D, val doFinally: Scheduler[D]) extends NotNull
|
||||
/** This represents nested work. The work provided by `scheduler` is processed first. The work provided by `doFinally` is processed
|
||||
* after `scheduler` completes regardless of the success of `scheduler`.*/
|
||||
final class SubWork[D](val scheduler: Scheduler[D], val doFinally: Scheduler[D]) extends NotNull
|
||||
/** Work that implements this interface provides nested work to be done before this work is processed.*/
|
||||
trait CompoundWork[D] extends NotNull
|
||||
{
|
||||
def work: SubWork[D]
|
||||
}
|
||||
|
|
@ -0,0 +1,336 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import Path._
|
||||
import FileUtilities.wrapNull
|
||||
import java.io.File
|
||||
import scala.collection.mutable.{Set, HashSet}
|
||||
|
||||
/** A Path represents a file in a project.
|
||||
* @see sbt.PathFinder*/
|
||||
sealed abstract class Path extends PathFinder with NotNull
|
||||
{
|
||||
/** Creates a base directory for this path. This is used by copy and zip functions
|
||||
* to determine the relative path that should be used in the destination. For example,
|
||||
* if the following path is specified to be copied to directory 'd',
|
||||
*
|
||||
* <code>((a / b) ##) / x / y</code>
|
||||
*
|
||||
* the copied path would be
|
||||
*
|
||||
* <code>d / x / y</code>
|
||||
*
|
||||
* The <code>relativePath</code> method is used to return the relative path to the base directory. */
|
||||
override def ## : Path = new BaseDirectory(this)
|
||||
private[sbt] def addTo(pathSet: Set[Path])
|
||||
{
|
||||
if(asFile.exists)
|
||||
pathSet += this
|
||||
}
|
||||
override def / (component: String): Path = if(component == ".") this else new RelativePath(this, component)
|
||||
/** True if and only if the file represented by this path exists.*/
|
||||
def exists = asFile.exists
|
||||
/** True if and only if the file represented by this path is a directory.*/
|
||||
def isDirectory = asFile.isDirectory
|
||||
/** The last modified time of the file represented by this path.*/
|
||||
def lastModified = asFile.lastModified
|
||||
/* True if and only if file that this path represents exists and the file represented by the path 'p'
|
||||
* does not exist or was modified before the file for this path.*/
|
||||
def newerThan(p: Path): Boolean = exists && (!p.exists || lastModified > p.lastModified)
|
||||
/* True if and only if file that this path represents does not exist or the file represented by the path 'p'
|
||||
* exists and was modified after the file for this path.*/
|
||||
def olderThan(p: Path): Boolean = p newerThan this
|
||||
/** The file represented by this path.*/
|
||||
def asFile: File
|
||||
/** The file represented by this path converted to a <code>URL</code>.*/
|
||||
def asURL = asFile.toURI.toURL
|
||||
/** The string representation of this path relative to the base directory. The project directory is the
|
||||
* default base directory if one is not specified explicitly using the <code>##</code> operator.*/
|
||||
lazy val relativePath: String = relativePathString(sep.toString)
|
||||
def relativePathString(separator: String): String
|
||||
final def projectRelativePath: String = projectRelativePathString(sep.toString)
|
||||
def projectRelativePathString(separator: String): String
|
||||
def absolutePath: String = asFile.getAbsolutePath
|
||||
private[sbt] def prependTo(s: String): String
|
||||
|
||||
/** Equality of Paths is defined in terms of the underlying <code>File</code>.*/
|
||||
override final def equals(other: Any) =
|
||||
other match
|
||||
{
|
||||
case op: Path => asFile == op.asFile
|
||||
case _ => false
|
||||
}
|
||||
/** The hash code of a Path is that of the underlying <code>File</code>.*/
|
||||
override final def hashCode = asFile.hashCode
|
||||
}
|
||||
private final class BaseDirectory(private[sbt] val path: Path) extends Path
|
||||
{
|
||||
override def ## : Path = this
|
||||
override def toString = path.toString
|
||||
def asFile = path.asFile
|
||||
def relativePathString(separator: String) = ""
|
||||
def projectRelativePathString(separator: String) = path.projectRelativePathString(separator)
|
||||
private[sbt] def prependTo(s: String) = "." + sep + s
|
||||
}
|
||||
private[sbt] final class FilePath(file: File) extends Path
|
||||
{
|
||||
lazy val asFile = absolute(file)
|
||||
override def toString = absolutePath
|
||||
def relativePathString(separator: String) = asFile.getName
|
||||
def projectRelativePathString(separator: String) = relativePathString(separator)
|
||||
private[sbt] def prependTo(s: String) = absolutePath + sep + s
|
||||
}
|
||||
private[sbt] final class ProjectDirectory(file: File) extends Path
|
||||
{
|
||||
lazy val asFile = absolute(file)
|
||||
override def toString = "."
|
||||
def relativePathString(separator: String) = ""
|
||||
def projectRelativePathString(separator: String) = ""
|
||||
private[sbt] def prependTo(s: String) = "." + sep + s
|
||||
}
|
||||
private[sbt] final class RelativePath(val parentPath: Path, val component: String) extends Path
|
||||
{
|
||||
checkComponent(component)
|
||||
override def toString = parentPath prependTo component
|
||||
lazy val asFile = new File(parentPath.asFile, component)
|
||||
private[sbt] def prependTo(s: String) = parentPath prependTo (component + sep + s)
|
||||
def relativePathString(separator: String) = relative(parentPath.relativePathString(separator), separator)
|
||||
def projectRelativePathString(separator: String) = relative(parentPath.projectRelativePathString(separator), separator)
|
||||
private def relative(parentRelative: String, separator: String) =
|
||||
{
|
||||
if(parentRelative.isEmpty)
|
||||
component
|
||||
else
|
||||
parentRelative + separator + component
|
||||
}
|
||||
}
|
||||
object Path
|
||||
{
|
||||
import java.io.File
|
||||
import File.pathSeparator
|
||||
|
||||
def fileProperty(name: String) = Path.fromFile(System.getProperty(name))
|
||||
def userHome = fileProperty("user.home")
|
||||
|
||||
def absolute(file: File) = new File(file.toURI.normalize).getAbsoluteFile
|
||||
/** Constructs a String representation of <code>Path</code>s. The absolute path String of each <code>Path</code> is
|
||||
* separated by the platform's path separator.*/
|
||||
def makeString(paths: Iterable[Path]): String = paths.map(_.absolutePath).mkString(pathSeparator)
|
||||
|
||||
/** Constructs a String representation of <code>Path</code>s. The relative path String of each <code>Path</code> is
|
||||
* separated by the platform's path separator.*/
|
||||
def makeRelativeString(paths: Iterable[Path]): String = makeRelativeString(paths, sep.toString)
|
||||
def makeRelativeString(paths: Iterable[Path], separator: String): String = paths.map(_.relativePathString(separator)).mkString(pathSeparator)
|
||||
|
||||
def splitString(projectPath: Path, value: String): Iterable[Path] =
|
||||
{
|
||||
for(pathString <- FileUtilities.pathSplit(value) if pathString.length > 0) yield
|
||||
Path.fromString(projectPath, pathString)
|
||||
}
|
||||
|
||||
/** A <code>PathFinder</code> that always produces the empty set of <code>Path</code>s.*/
|
||||
def emptyPathFinder =
|
||||
new PathFinder
|
||||
{
|
||||
private[sbt] def addTo(pathSet: Set[Path]) {}
|
||||
}
|
||||
/** A <code>PathFinder</code> that selects the paths provided by the <code>paths</code> argument, which is
|
||||
* reevaluated on each call to the <code>PathFinder</code>'s <code>get</code> method. */
|
||||
def lazyPathFinder(paths: => Iterable[Path]): PathFinder =
|
||||
new PathFinder
|
||||
{
|
||||
private[sbt] def addTo(pathSet: Set[Path]) = pathSet ++= paths
|
||||
}
|
||||
|
||||
/** The separator character of the platform.*/
|
||||
val sep = java.io.File.separatorChar
|
||||
|
||||
/** Checks the string to verify that it is a legal path component. The string must be non-empty,
|
||||
* not a slash, and not '.' or '..'.*/
|
||||
def checkComponent(c: String): String =
|
||||
{
|
||||
require(c.length > 0, "Path component must not be empty")
|
||||
require(c.indexOf('/') == -1, "Path component '" + c + "' must not have forward slashes in it")
|
||||
require(c.indexOf('\\') == -1, "Path component '" + c + "' must not have backslashes in it")
|
||||
require(c != "..", "Path component cannot be '..'")
|
||||
require(c != ".", "Path component cannot be '.'")
|
||||
c
|
||||
}
|
||||
/** Converts a path string relative to the given base path to a <code>Path</code>. */
|
||||
def fromString(basePath: Path, value: String): Path =
|
||||
{
|
||||
if(value.isEmpty)
|
||||
basePath
|
||||
else
|
||||
{
|
||||
val components = value.split("""[/\\]""")
|
||||
(basePath /: components)( (path, component) => path / component )
|
||||
}
|
||||
}
|
||||
def baseAncestor(path: Path): Option[Path] =
|
||||
path match
|
||||
{
|
||||
case pd: ProjectDirectory => None
|
||||
case fp: FilePath => None
|
||||
case rp: RelativePath => baseAncestor(rp.parentPath)
|
||||
case b: BaseDirectory => Some(b.path)
|
||||
}
|
||||
|
||||
def relativize(basePath: Path, path: Path): Option[Path] = relativize(basePath, path.asFile)
|
||||
def relativize(basePath: Path, file: File): Option[Path] =
|
||||
basePathString(basePath) flatMap { baseString => relativize(basePath, baseString, file) }
|
||||
def relativize(basePath: Path, basePathString: String, file: File): Option[Path] =
|
||||
{
|
||||
val pathString = file.getAbsolutePath
|
||||
if(pathString.startsWith(basePathString))
|
||||
Some(fromString(basePath, pathString.substring(basePathString.length)))
|
||||
else
|
||||
None
|
||||
}
|
||||
private[sbt] def relativize(baseFile: File, file: File): Option[String] =
|
||||
{
|
||||
val pathString = file.getAbsolutePath
|
||||
baseFileString(baseFile) flatMap
|
||||
{
|
||||
baseString =>
|
||||
{
|
||||
if(pathString.startsWith(baseString))
|
||||
Some(pathString.substring(baseString.length))
|
||||
else
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
private[sbt] def basePathString(basePath: Path): Option[String] = baseFileString(basePath.asFile)
|
||||
private def baseFileString(baseFile: File): Option[String] =
|
||||
{
|
||||
if(baseFile.isDirectory)
|
||||
{
|
||||
val cp = baseFile.getAbsolutePath
|
||||
assert(cp.length > 0)
|
||||
if(cp.charAt(cp.length - 1) == File.separatorChar)
|
||||
Some(cp)
|
||||
else
|
||||
Some(cp + File.separatorChar)
|
||||
}
|
||||
else
|
||||
None
|
||||
}
|
||||
def fromFile(file: String): Path = fromFile(new File(file))
|
||||
def fromFile(file: File): Path = new FilePath(file)
|
||||
}
|
||||
|
||||
/** A path finder constructs a set of paths. The set is evaluated by a call to the <code>get</code>
|
||||
* method. The set will be different for different calls to <code>get</code> if the underlying filesystem
|
||||
* has changed.*/
|
||||
sealed abstract class PathFinder extends NotNull
|
||||
{
|
||||
/** The union of the paths found by this <code>PathFinder</code> with the paths found by 'paths'.*/
|
||||
def +++(paths: PathFinder): PathFinder = new Paths(this, paths)
|
||||
/** Excludes all paths from <code>excludePaths</code> from the paths selected by this <code>PathFinder</code>.*/
|
||||
def ---(excludePaths: PathFinder): PathFinder = new ExcludePaths(this, excludePaths)
|
||||
/** Constructs a new finder that selects all paths with a name that matches <code>filter</code> and are
|
||||
* descendents of paths selected by this finder.*/
|
||||
def **(filter: FileFilter): PathFinder = new DescendentOrSelfPathFinder(this, filter)
|
||||
/** Constructs a new finder that selects all paths with a name that matches <code>filter</code> and are
|
||||
* immediate children of paths selected by this finder.*/
|
||||
def *(filter: FileFilter): PathFinder = new ChildPathFinder(this, filter)
|
||||
/** Constructs a new finder that selects all paths with name <code>literal</code> that are immediate children
|
||||
* of paths selected by this finder.*/
|
||||
def / (literal: String): PathFinder = new ChildPathFinder(this, new ExactFilter(literal))
|
||||
/** Constructs a new finder that selects all paths with name <code>literal</code> that are immediate children
|
||||
* of paths selected by this finder.*/
|
||||
final def \ (literal: String): PathFinder = this / literal
|
||||
|
||||
/** Makes the paths selected by this finder into base directories.
|
||||
* @see Path.##
|
||||
*/
|
||||
def ## : PathFinder = new BasePathFinder(this)
|
||||
|
||||
/** Selects all descendent paths with a name that matches <code>include</code> and do not have an intermediate
|
||||
* path with a name that matches <code>intermediateExclude</code>. Typical usage is:
|
||||
*
|
||||
* <code>descendentsExcept("*.jar", ".svn")</code>*/
|
||||
def descendentsExcept(include: FileFilter, intermediateExclude: FileFilter): PathFinder =
|
||||
(this ** include) --- (this ** intermediateExclude ** include)
|
||||
|
||||
/** Evaluates this finder. The set returned by this method will reflect the underlying filesystem at the
|
||||
* time of calling. If the filesystem changes, two calls to this method might be different.*/
|
||||
final def get: scala.collection.Set[Path] =
|
||||
{
|
||||
val pathSet = new HashSet[Path]
|
||||
addTo(pathSet)
|
||||
wrap.Wrappers.readOnly(pathSet)
|
||||
}
|
||||
private[sbt] def addTo(pathSet: Set[Path])
|
||||
}
|
||||
private class BasePathFinder(base: PathFinder) extends PathFinder
|
||||
{
|
||||
private[sbt] def addTo(pathSet: Set[Path])
|
||||
{
|
||||
for(path <- base.get)
|
||||
pathSet += (path ##)
|
||||
}
|
||||
}
|
||||
private abstract class FilterPath extends PathFinder with FileFilter
|
||||
{
|
||||
def parent: PathFinder
|
||||
def filter: FileFilter
|
||||
final def accept(file: File) = filter.accept(file)
|
||||
|
||||
protected def handlePath(path: Path, pathSet: Set[Path])
|
||||
{
|
||||
for(matchedFile <- wrapNull(path.asFile.listFiles(this)))
|
||||
pathSet += path / matchedFile.getName
|
||||
}
|
||||
}
|
||||
private class DescendentOrSelfPathFinder(val parent: PathFinder, val filter: FileFilter) extends FilterPath
|
||||
{
|
||||
private[sbt] def addTo(pathSet: Set[Path])
|
||||
{
|
||||
for(path <- parent.get)
|
||||
{
|
||||
if(accept(path.asFile))
|
||||
pathSet += path
|
||||
handlePathDescendent(path, pathSet)
|
||||
}
|
||||
}
|
||||
private def handlePathDescendent(path: Path, pathSet: Set[Path])
|
||||
{
|
||||
handlePath(path, pathSet)
|
||||
for(childDirectory <- wrapNull(path.asFile.listFiles(DirectoryFilter)))
|
||||
handlePathDescendent(path / childDirectory.getName, pathSet)
|
||||
}
|
||||
}
|
||||
private class ChildPathFinder(val parent: PathFinder, val filter: FileFilter) extends FilterPath
|
||||
{
|
||||
private[sbt] def addTo(pathSet: Set[Path])
|
||||
{
|
||||
for(path <- parent.get)
|
||||
handlePath(path, pathSet)
|
||||
}
|
||||
}
|
||||
private class Paths(a: PathFinder, b: PathFinder) extends PathFinder
|
||||
{
|
||||
private[sbt] def addTo(pathSet: Set[Path])
|
||||
{
|
||||
a.addTo(pathSet)
|
||||
b.addTo(pathSet)
|
||||
}
|
||||
}
|
||||
private class ExcludePaths(include: PathFinder, exclude: PathFinder) extends PathFinder
|
||||
{
|
||||
private[sbt] def addTo(pathSet: Set[Path])
|
||||
{
|
||||
val includeSet = new HashSet[Path]
|
||||
include.addTo(includeSet)
|
||||
|
||||
val excludeSet = new HashSet[Path]
|
||||
exclude.addTo(excludeSet)
|
||||
|
||||
includeSet --= excludeSet
|
||||
pathSet ++= includeSet
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import java.lang.{Process => JProcess, ProcessBuilder => JProcessBuilder}
|
||||
import java.io.{Closeable, File, IOException}
|
||||
import java.io.{BufferedReader, InputStream, InputStreamReader, OutputStream, PipedInputStream, PipedOutputStream}
|
||||
import java.net.URL
|
||||
|
||||
/** Methods for constructing simple commands that can then be combined. */
|
||||
object Process
|
||||
{
|
||||
implicit def apply(command: String): ProcessBuilder = apply(command.split("""\s+""")) // TODO: use CommandParser
|
||||
implicit def apply(command: Seq[String]): ProcessBuilder = apply(new JProcessBuilder(command.toArray : _*))
|
||||
def apply(command: String, arguments: Seq[String]): ProcessBuilder = apply(new JProcessBuilder((command :: arguments.toList).toArray : _*))
|
||||
implicit def apply(builder: JProcessBuilder): ProcessBuilder = new SimpleProcessBuilder(builder)
|
||||
implicit def apply(file: File): FilePartialBuilder = new FileBuilder(file)
|
||||
implicit def apply(url: URL): URLPartialBuilder = new URLBuilder(url)
|
||||
}
|
||||
|
||||
trait URLPartialBuilder extends NotNull
|
||||
{
|
||||
def #>(b: ProcessBuilder): ProcessBuilder
|
||||
def #>>(b: File): ProcessBuilder
|
||||
def #>(b: File): ProcessBuilder
|
||||
}
|
||||
trait FilePartialBuilder extends NotNull
|
||||
{
|
||||
def #>(b: ProcessBuilder): ProcessBuilder
|
||||
def #<(b: ProcessBuilder): ProcessBuilder
|
||||
def #<(url: URL): ProcessBuilder
|
||||
def #>>(b: File): ProcessBuilder
|
||||
def #>(b: File): ProcessBuilder
|
||||
def #<(file: File): ProcessBuilder
|
||||
def #<<(file: File): ProcessBuilder
|
||||
}
|
||||
|
||||
/** Represents a process that is running or has finished running.
|
||||
* It may be a compound process with several underlying native processes (such as 'a #&& b`).*/
|
||||
trait Process extends NotNull
|
||||
{
|
||||
/** Blocks until this process exits and returns the exit code.*/
|
||||
def exitValue(): Int
|
||||
/** Destroys this process. */
|
||||
def destroy(): Unit
|
||||
}
|
||||
/** Represents a runnable process. */
|
||||
trait ProcessBuilder extends NotNull
|
||||
{
|
||||
/** Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are
|
||||
* sent to the console.*/
|
||||
def ! : Int
|
||||
/** Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are
|
||||
* sent to the given Logger.*/
|
||||
def !(log: Logger): Int
|
||||
/** Starts the process represented by this builder. Standard output and error are sent to the console.*/
|
||||
def run(): Process
|
||||
/** Starts the process represented by this builder. Standard output and error are sent to the given Logger.*/
|
||||
def run(log: Logger): Process
|
||||
/** Starts the process represented by this builder. I/O is handled by the given ProcessIO instance.*/
|
||||
def run(io: ProcessIO): Process
|
||||
|
||||
/** Constructs a command that runs this command first and then `other` if this command succeeds.*/
|
||||
def #&& (other: ProcessBuilder): ProcessBuilder
|
||||
/** Constructs a command that runs this command first and then `other` if this command does not succeed.*/
|
||||
def #|| (other: ProcessBuilder): ProcessBuilder
|
||||
/** Constructs a command that will run this command and pipes the output to `other`. `other` must be a simple command.*/
|
||||
def #| (other: ProcessBuilder): ProcessBuilder
|
||||
/** Constructs a command that will run this command and then `other`. The exit code will be the exit code of `other`.*/
|
||||
def ## (other: ProcessBuilder): ProcessBuilder
|
||||
/** Reads the given file into the input stream of this process. */
|
||||
def #< (f: File): ProcessBuilder
|
||||
/** Reads the given URL into the input stream of this process. */
|
||||
def #< (f: URL): ProcessBuilder
|
||||
/** Writes the output stream of this process to the given file. */
|
||||
def #> (f: File): ProcessBuilder
|
||||
/** Appends the output stream of this process to the given file. */
|
||||
def #>> (f: File): ProcessBuilder
|
||||
|
||||
def canPipeTo: Boolean
|
||||
}
|
||||
/** Each method will be called in a separate thread.*/
|
||||
final class ProcessIO(val writeInput: OutputStream => Unit, val processOutput: InputStream => Unit, val processError: InputStream => Unit) extends NotNull
|
||||
{
|
||||
def withOutput(process: InputStream => Unit): ProcessIO = new ProcessIO(writeInput, process, processError)
|
||||
def withError(process: InputStream => Unit): ProcessIO = new ProcessIO(writeInput, processOutput, process)
|
||||
def withInput(write: OutputStream => Unit): ProcessIO = new ProcessIO(write, processOutput, processError)
|
||||
}
|
||||
|
|
@ -0,0 +1,462 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah, David MacIver
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import java.io.File
|
||||
import scala.collection._
|
||||
import FileUtilities._
|
||||
import Project._
|
||||
|
||||
trait Project extends TaskManager with Dag[Project] with BasicEnvironment
|
||||
{
|
||||
/** The logger for this project definition. */
|
||||
final val log: Logger = logImpl
|
||||
protected def logImpl: Logger = new BufferedLogger(new ConsoleLogger)
|
||||
|
||||
trait ActionOption extends NotNull
|
||||
|
||||
/** Basic project information. */
|
||||
def info: ProjectInfo
|
||||
/** The project name. */
|
||||
final def name: String = projectName.value
|
||||
/** The project version. */
|
||||
final def version: Version = projectVersion.value
|
||||
/** The project organization. */
|
||||
final def organization: String = projectOrganization.value
|
||||
/** True if the project should cater to a quick throwaway project setup.*/
|
||||
def scratch = projectScratch.value
|
||||
|
||||
final type ManagerType = Project
|
||||
final type ManagedTask = Project#Task
|
||||
/** The tasks declared on this project. */
|
||||
def tasks: Map[String, Task]
|
||||
/** The task methods declared on this project */
|
||||
def methods: Map[String, MethodTask]
|
||||
/** The names of all available tasks that may be called through `act`. These include
|
||||
* the names of the Tasks in `tasks` and those of all dependencies.*/
|
||||
def taskNames: Iterable[String] = deepTasks.keys.toList
|
||||
/** The names of all available method tasks that may be called through `call`. These
|
||||
* only include the names of the MethodTasks in `methods` and not those of dependencies.*/
|
||||
def methodNames: Iterable[String] = methods.keys.toList
|
||||
/** A description of all available method tasks in this project, but not of dependencies. */
|
||||
def methodList: String = descriptionList(methods)
|
||||
/** A description of all available tasks in this project and all dependencies. If there
|
||||
* are different tasks with the same name, only one will be included. */
|
||||
def taskList: String = descriptionList(deepTasks)
|
||||
|
||||
final def taskName(task: Task) = tasks.find( _._2 eq task ).map(_._1).getOrElse(UnnamedName)
|
||||
/** A description of all available tasks in this project and all dependencies and all
|
||||
* available method tasks in this project, but not of dependencies. If there
|
||||
* are different tasks or methods with the same name, only one will be included. */
|
||||
def taskAndMethodList: String = descriptionList(tasksAndMethods)
|
||||
/** The actions and methods declared on this project. */
|
||||
final def tasksAndMethods: Map[String, Described] =
|
||||
immutable.TreeMap.empty[String, Described] ++ methods ++ tasks
|
||||
private def descriptionList(described: Map[String, Described]): String =
|
||||
{
|
||||
val buffer = new StringBuilder
|
||||
for((name, d) <- described)
|
||||
buffer.append("\t" + name + d.description.map(x => ": " + x).getOrElse("") + "\n")
|
||||
buffer.toString
|
||||
}
|
||||
/** Combines the method task maps of this project and all dependencies.*/
|
||||
private[sbt] def deepMethods: Map[String, Project#MethodTask] = deep(_.methods)
|
||||
/** Combines the task maps of this project and all dependencies.*/
|
||||
private[sbt] def deepTasks: Map[String, Project#Task] = deep(_.tasks)
|
||||
private def deep[T](p: Project => Map[String, T]): Map[String, T] =
|
||||
{
|
||||
var tasks: immutable.SortedMap[String,T] = new immutable.TreeMap[String, T]
|
||||
for(dependentProject <- topologicalSort)
|
||||
tasks ++= p(dependentProject).elements
|
||||
tasks
|
||||
}
|
||||
/** A map of names to projects for all subprojects of this project. These are typically explicitly
|
||||
* specified for the project and are different from those specified in the project constructor. The
|
||||
* main use within sbt is in ParentProject.*/
|
||||
def subProjects: Map[String, Project] = immutable.Map.empty
|
||||
/** The name of this project and the names of all subprojects/dependencies, transitively.*/
|
||||
def projectNames: Iterable[String] =
|
||||
{
|
||||
val names = new mutable.HashSet[String]
|
||||
names ++= subProjects.keys
|
||||
for(dependentProject <- topologicalSort)
|
||||
names ++= dependentProject.tasks.keys
|
||||
names.toList
|
||||
}
|
||||
|
||||
def call(name: String, parameters: Array[String]): Option[String] =
|
||||
{
|
||||
methods.get(name) match
|
||||
{
|
||||
case Some(method) =>run(method(parameters), name)
|
||||
case None => Some("Method '" + name + "' does not exist.")
|
||||
}
|
||||
}
|
||||
private def run(task: Project#Task, taskName: String): Option[String] =
|
||||
impl.RunTask(task, taskName, parallelExecution) match
|
||||
{
|
||||
case Nil => None
|
||||
case x => Some(Set(x: _*).mkString("\n"))
|
||||
}
|
||||
|
||||
/** Executes the task with the given name. This involves executing the task for all
|
||||
* project dependencies (transitive) and then for this project. Not every dependency
|
||||
* must define a task with the given name. If this project and all dependencies
|
||||
* do not define a task with the given name, an error is generated indicating this.*/
|
||||
def act(name: String): Option[String] =
|
||||
{
|
||||
val ordered = topologicalSort
|
||||
val definedTasks = ordered.flatMap(_.tasks.get(name).toList)
|
||||
def virtualTask(name: String): Task = new Task(None, definedTasks.filter(!_.interactive), false, None)
|
||||
|
||||
if(definedTasks.isEmpty)
|
||||
Some("Action '" + name + "' does not exist.")
|
||||
else
|
||||
{
|
||||
tasks.get(name) match
|
||||
{
|
||||
case None =>
|
||||
val virtual = virtualTask(name)
|
||||
if(virtual.dependencies.size == definedTasks.size)
|
||||
run(virtual, name)
|
||||
else
|
||||
{
|
||||
Some("Cannot run interactive action '" + name +
|
||||
"' defined on multiple subprojects (change to the desired project with 'project <name>').")
|
||||
}
|
||||
case Some(task) => run(task, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Logs the list of projects at the debug level.*/
|
||||
private def showBuildOrder(order: Iterable[Project])
|
||||
{
|
||||
log.debug("Project build order:")
|
||||
order.foreach(x => log.debug(" " + x.name) )
|
||||
log.debug("")
|
||||
}
|
||||
|
||||
/** Converts a String to a path relative to the project directory of this project. */
|
||||
implicit def path(component: String): Path = info.projectPath / component
|
||||
/** Converts a String to a simple name filter. * has the special meaning: zero or more of any character */
|
||||
implicit def filter(simplePattern: String): NameFilter = GlobFilter(simplePattern)
|
||||
|
||||
/** Loads the project at the given path and declares the project to have the given
|
||||
* dependencies. This method will configure the project according to the
|
||||
* project/ directory in the directory denoted by path.*/
|
||||
def project(path: Path, deps: Project*): Project = getProject(Project.loadProject(path, deps, Some(this), log), path)
|
||||
|
||||
/** Loads the project at the given path using the given name and inheriting this project's version.
|
||||
* The builder class is the default builder class, sbt.DefaultProject. The loaded project is declared
|
||||
* to have the given dependencies. Any project/build/ directory for the project is ignored.*/
|
||||
def project(path: Path, name: String, deps: Project*): Project = project(path, name, Project.DefaultBuilderClass, deps: _*)
|
||||
|
||||
/** Loads the project at the given path using the given name and inheriting it's version from this project.
|
||||
* The Project implementation used is given by builderClass. The dependencies are declared to be
|
||||
* deps. Any project/build/ directory for the project is ignored.*/
|
||||
def project[P <: Project](path: Path, name: String, builderClass: Class[P], deps: Project*): P =
|
||||
{
|
||||
require(builderClass != this.getClass, "Cannot recursively construct projects of same type: " + builderClass.getName)
|
||||
project(path, name, info => Project.constructProject(info, builderClass), deps: _*)
|
||||
}
|
||||
/** Loads the project at the given path using the given name and inheriting it's version from this project.
|
||||
* The construct function is used to obtain the Project instance. Any project/build/ directory for the project
|
||||
* is ignored. The project is declared to have the dependencies given by deps.*/
|
||||
def project[P <: Project](path: Path, name: String, construct: ProjectInfo => P, deps: Project*): P =
|
||||
initialize(construct(ProjectInfo(path.asFile, deps, Some(this))), Some(new SetupInfo(name, None, None, false)), log)
|
||||
|
||||
/** Initializes the project directories when a user has requested that sbt create a new project.*/
|
||||
def initializeDirectories() {}
|
||||
/** True if projects should be run in parallel, false if they should run sequentially.
|
||||
* This only has an effect for multi-projects. If this project has a parent, this value is
|
||||
* inherited from that parent project.*/
|
||||
def parallelExecution: Boolean =
|
||||
info.parent match
|
||||
{
|
||||
case Some(parent) => parent.parallelExecution
|
||||
case None => false
|
||||
}
|
||||
|
||||
/** True if a project and its dependencies should be checked to ensure that their
|
||||
* output directories are not the same, false if they should not be checked. */
|
||||
def shouldCheckOutputDirectories = true
|
||||
|
||||
/** The list of directories to which this project writes. This is used to verify that multiple
|
||||
* projects have not been defined with the same output directories. */
|
||||
def outputDirectories: Iterable[Path] = outputRootPath :: Nil
|
||||
def rootProject = Project.rootProject(this)
|
||||
/** The path to the file that provides persistence for properties.*/
|
||||
final def envBackingPath = info.builderPath / Project.DefaultEnvBackingName
|
||||
/** The path to the file that provides persistence for history. */
|
||||
def historyPath: Option[Path] = Some(outputRootPath / ".history")
|
||||
def outputPath = crossPath(outputRootPath)
|
||||
def outputRootPath = outputDirectoryName
|
||||
def outputDirectoryName = DefaultOutputDirectoryName
|
||||
|
||||
private def getProject(result: LoadResult, path: Path): Project =
|
||||
result match
|
||||
{
|
||||
case LoadSetupDeclined => Predef.error("No project exists at path " + path)
|
||||
case lse: LoadSetupError => Predef.error("Error setting up new project at path " + Path + " : " + lse.message)
|
||||
case err: LoadError => Predef.error("Error loading project at path " + path + " : " + err.message)
|
||||
case success: LoadSuccess => success.project
|
||||
}
|
||||
|
||||
/** The property for the project's version. */
|
||||
final val projectVersion = property[Version]
|
||||
/** The property for the project's name. */
|
||||
final val projectName = propertyLocalF[String](NonEmptyStringFormat)
|
||||
/** The property for the project's organization. Defaults to the parent project's organization or the project name if there is no parent. */
|
||||
final val projectOrganization = propertyOptional[String](normalizedName, true)
|
||||
/** The property that defines the version of Scala to build this project with by default. This property is only
|
||||
* ready by `sbt` on startup and reboot. When cross-building, this value may be different from the actual
|
||||
* version of Scala being used to build the project. ScalaVersion.current and ScalaVersion.cross should be used
|
||||
* to read the version of Scala building the project. This should only be used to change the version of Scala used
|
||||
* for normal development (not cross-building)*/
|
||||
final val scalaVersion = propertyOptional[String]("")
|
||||
final val sbtVersion = propertyOptional[String]("")
|
||||
final val projectInitialize = propertyOptional[Boolean](false)
|
||||
final val projectScratch = propertyOptional[Boolean](false)
|
||||
|
||||
/** If this project is cross-building, returns `base` with an additional path component containing the scala version.
|
||||
* Otherwise, this returns `base`.
|
||||
* By default, cross-building is enabled when a project is loaded by the loader and crossScalaVersions is not empty.*/
|
||||
def crossPath(base: Path) = ScalaVersion.withCross(disableCrossPaths)(base / ScalaVersion.crossString(_), base)
|
||||
/** If modifying paths for cross-building is enabled, this returns ScalaVersion.currentString.
|
||||
* Otherwise, this returns the empty string. */
|
||||
def crossScalaVersionString: String = if(disableCrossPaths) "" else ScalaVersion.currentString
|
||||
|
||||
/** True if crossPath should be the identity function.*/
|
||||
protected def disableCrossPaths = crossScalaVersions.isEmpty
|
||||
/** By default, this is empty and cross-building is disabled. Overriding this to a Set of Scala versions
|
||||
* will enable cross-building against those versions.*/
|
||||
def crossScalaVersions = scala.collection.immutable.Set.empty[String]
|
||||
/** A `PathFinder` that determines the files watched when an action is run with a preceeding ~ when this is the current
|
||||
* project. This project does not need to include the watched paths for projects that this project depends on.*/
|
||||
def watchPaths: PathFinder = Path.emptyPathFinder
|
||||
|
||||
protected final override def parentEnvironment = info.parent
|
||||
|
||||
// .* included because svn doesn't mark .svn hidden
|
||||
def defaultExcludes: FileFilter = (".*" - ".") || HiddenFileFilter
|
||||
/** Short for parent.descendentsExcept(include, defaultExcludes)*/
|
||||
def descendents(parent: PathFinder, include: FileFilter) = parent.descendentsExcept(include, defaultExcludes)
|
||||
override def toString = "Project " + projectName.get.getOrElse("at " + environmentLabel)
|
||||
|
||||
def normalizedName = StringUtilities.normalize(name)
|
||||
}
|
||||
private[sbt] sealed trait LoadResult extends NotNull
|
||||
private[sbt] final class LoadSuccess(val project: Project) extends LoadResult
|
||||
private[sbt] final class LoadError(val message: String) extends LoadResult
|
||||
private[sbt] final object LoadSetupDeclined extends LoadResult
|
||||
private[sbt] final class LoadSetupError(val message: String) extends LoadResult
|
||||
|
||||
object Project
|
||||
{
|
||||
val UnnamedName = "<anonymous>"
|
||||
val BootDirectoryName = "boot"
|
||||
val DefaultOutputDirectoryName = "target"
|
||||
val DefaultEnvBackingName = "build.properties"
|
||||
val DefaultBuilderClassName = "sbt.DefaultProject"
|
||||
val DefaultBuilderClass = Class.forName(DefaultBuilderClassName).asSubclass(classOf[Project])
|
||||
|
||||
/** The name of the directory for project definitions.*/
|
||||
val BuilderProjectDirectoryName = "build"
|
||||
/** The name of the directory for plugin definitions.*/
|
||||
val PluginProjectDirectoryName = "plugins"
|
||||
/** The name of the class that all projects must inherit from.*/
|
||||
val ProjectClassName = classOf[Project].getName
|
||||
|
||||
/** The logger that should be used before the root project definition is loaded.*/
|
||||
private[sbt] def bootLogger =
|
||||
{
|
||||
val log = new ConsoleLogger
|
||||
log.setLevel(Level.Debug)
|
||||
log.enableTrace(true)
|
||||
log
|
||||
}
|
||||
|
||||
private[sbt] def booted = java.lang.Boolean.getBoolean("sbt.boot")
|
||||
|
||||
/** Loads the project in the current working directory.*/
|
||||
private[sbt] def loadProject: LoadResult = loadProject(bootLogger)
|
||||
/** Loads the project in the current working directory.*/
|
||||
private[sbt] def loadProject(log: Logger): LoadResult = checkOutputDirectories(loadProject(new File("."), Nil, None, log))
|
||||
/** Loads the project in the directory given by 'path' and with the given dependencies.*/
|
||||
private[sbt] def loadProject(path: Path, deps: Iterable[Project], parent: Option[Project], log: Logger): LoadResult =
|
||||
loadProject(path.asFile, deps, parent, log)
|
||||
/** Loads the project in the directory given by 'projectDirectory' and with the given dependencies.*/
|
||||
private[sbt] def loadProject(projectDirectory: File, deps: Iterable[Project], parent: Option[Project], log: Logger): LoadResult =
|
||||
{
|
||||
val info = ProjectInfo(projectDirectory, deps, parent)
|
||||
ProjectInfo.setup(info, log) match
|
||||
{
|
||||
case err: SetupError => new LoadSetupError(err.message)
|
||||
case SetupDeclined => LoadSetupDeclined
|
||||
case AlreadySetup => loadProject(info, None, log)
|
||||
case setup: SetupInfo => loadProject(info, Some(setup), log)
|
||||
}
|
||||
}
|
||||
private def loadProject(info: ProjectInfo, setupInfo: Option[SetupInfo], log: Logger): LoadResult =
|
||||
{
|
||||
try
|
||||
{
|
||||
val oldLevel = log.getLevel
|
||||
log.setLevel(Level.Warn)
|
||||
val result =
|
||||
for(builderClass <- getProjectDefinition(info, log).right) yield
|
||||
initialize(constructProject(info, builderClass), setupInfo, log)
|
||||
log.setLevel(oldLevel)
|
||||
result.fold(new LoadError(_), new LoadSuccess(_))
|
||||
}
|
||||
catch
|
||||
{
|
||||
case ite: java.lang.reflect.InvocationTargetException =>
|
||||
{
|
||||
val cause =
|
||||
if(ite.getCause == null) ite
|
||||
else ite.getCause
|
||||
errorLoadingProject(cause, log)
|
||||
}
|
||||
case nme: NoSuchMethodException => new LoadError("Constructor with one argument of type sbt.ProjectInfo required for project definition.")
|
||||
case e: Exception => errorLoadingProject(e, log)
|
||||
}
|
||||
}
|
||||
/** Logs the stack trace and returns an error message in Left.*/
|
||||
private def errorLoadingProject(e: Throwable, log: Logger) =
|
||||
{
|
||||
log.trace(e)
|
||||
new LoadError("Error loading project: " + e.toString)
|
||||
}
|
||||
/** Loads the project for the given `info` and represented by an instance of 'builderClass'.*/
|
||||
private[sbt] def constructProject[P <: Project](info: ProjectInfo, builderClass: Class[P]): P =
|
||||
builderClass.getConstructor(classOf[ProjectInfo]).newInstance(info)
|
||||
/** Checks the project's dependencies, initializes its environment, and possibly its directories.*/
|
||||
private def initialize[P <: Project](p: P, setupInfo: Option[SetupInfo], log: Logger): P =
|
||||
{
|
||||
setupInfo match
|
||||
{
|
||||
case Some(setup) =>
|
||||
{
|
||||
p.projectName() = setup.name
|
||||
for(v <- setup.version)
|
||||
p.projectVersion() = v
|
||||
for(org <- setup.organization)
|
||||
p.projectOrganization() = org
|
||||
if(!setup.initializeDirectories)
|
||||
p.setEnvironmentModified(false)
|
||||
for(errorMessage <- p.saveEnvironment())
|
||||
log.error(errorMessage)
|
||||
if(setup.initializeDirectories)
|
||||
p.initializeDirectories()
|
||||
}
|
||||
case None =>
|
||||
if(p.projectInitialize.value)
|
||||
{
|
||||
p.initializeDirectories()
|
||||
p.projectInitialize() = false
|
||||
for(errorMessage <- p.saveEnvironment())
|
||||
log.error(errorMessage)
|
||||
}
|
||||
}
|
||||
val useName = p.projectName.get.getOrElse("at " + p.info.projectDirectory.getAbsolutePath)
|
||||
checkDependencies(useName, p.info.dependencies, log)
|
||||
p
|
||||
}
|
||||
/** Compiles the project definition classes and returns the project definition class name
|
||||
* and the class loader that should be used to load the definition. */
|
||||
private def getProjectDefinition(info: ProjectInfo, buildLog: Logger): Either[String, Class[P] forSome { type P <: Project }] =
|
||||
{
|
||||
val builderProjectPath = info.builderPath / BuilderProjectDirectoryName
|
||||
if(builderProjectPath.asFile.isDirectory)
|
||||
{
|
||||
val pluginProjectPath = info.builderPath / PluginProjectDirectoryName
|
||||
val builderProject = new BuilderProject(ProjectInfo(builderProjectPath.asFile, Nil, None), pluginProjectPath, buildLog)
|
||||
builderProject.compile.run.toLeft(()).right.flatMap { ignore =>
|
||||
builderProject.projectDefinition.right.map {
|
||||
case Some(definition) => getProjectClass[Project](definition, builderProject.projectClasspath)
|
||||
case None => DefaultBuilderClass
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
Right(DefaultBuilderClass)
|
||||
}
|
||||
/** Verifies that the given list of project dependencies contains no nulls. The
|
||||
* String argument should be the project name with the dependencies.*/
|
||||
private def checkDependencies(forProject: String, deps: Iterable[Project], log: Logger)
|
||||
{
|
||||
for(nullDep <- deps.find(_ == null))
|
||||
{
|
||||
log.error("Project " + forProject + " had a null dependency. This is probably an initialization problem and might be due to a circular dependency.")
|
||||
throw new RuntimeException("Null dependency in project " + forProject)
|
||||
}
|
||||
}
|
||||
/** Verifies that output directories of the given project and all of its dependencies are
|
||||
* all different. No verification is done if the project overrides
|
||||
* 'shouldCheckOutputDirectories' to be false. The 'Project.outputDirectories' method is
|
||||
* used to determine a project's output directories. */
|
||||
private def checkOutputDirectories(result: LoadResult): LoadResult =
|
||||
result match
|
||||
{
|
||||
case success: LoadSuccess =>
|
||||
if(success.project.shouldCheckOutputDirectories)
|
||||
checkOutputDirectoriesImpl(success.project)
|
||||
else
|
||||
success
|
||||
case x => x
|
||||
}
|
||||
/** Verifies that output directories of the given project and all of its dependencies are
|
||||
* all different. The 'Project.outputDirectories' method is used to determine a project's
|
||||
* output directories. */
|
||||
private def checkOutputDirectoriesImpl(project: Project): LoadResult =
|
||||
{
|
||||
val projects = project.topologicalSort
|
||||
import scala.collection.mutable.{HashMap, HashSet, Set}
|
||||
val outputDirectories = new HashMap[Path, Set[Project]]
|
||||
for(p <- projects; path <- p.outputDirectories)
|
||||
outputDirectories.getOrElseUpdate(path, new HashSet[Project]) += p
|
||||
val shared = outputDirectories.filter(_._2.size > 1)
|
||||
if(shared.isEmpty)
|
||||
new LoadSuccess(project)
|
||||
else
|
||||
{
|
||||
val sharedString =
|
||||
{
|
||||
val s =
|
||||
for((path, projectsSharingPath) <- shared) yield
|
||||
projectsSharingPath.map(_.name).mkString(", ") + " share " + path
|
||||
s.mkString("\n\t")
|
||||
}
|
||||
new LoadError("The same directory is used for output for multiple projects:\n\t" + sharedString +
|
||||
"\n (If this is intentional, use 'override def shouldCheckOutputDirectories = false' in your project definition.)")
|
||||
}
|
||||
}
|
||||
import scala.reflect.Manifest
|
||||
private[sbt] def getProjectClass[P <: Project](name: String, classpath: PathFinder)(implicit mf: Manifest[P]): Class[P] =
|
||||
{
|
||||
val loader =ClasspathUtilities.toLoader(classpath)
|
||||
val builderClass = Class.forName(name, false, loader)
|
||||
val projectClass = mf.erasure
|
||||
require(projectClass.isAssignableFrom(builderClass), "Builder class '" + builderClass + "' does not extend " + projectClass.getName + ".")
|
||||
builderClass.asSubclass(projectClass).asInstanceOf[Class[P]]
|
||||
}
|
||||
|
||||
/** Writes the project name and a separator to the project's log at the info level.*/
|
||||
def showProjectHeader(project: Project)
|
||||
{
|
||||
val projectHeader = "Project " + project.name
|
||||
project.log.info("")
|
||||
project.log.info(projectHeader)
|
||||
project.log.info("=" * projectHeader.length)
|
||||
}
|
||||
|
||||
def rootProject(p: Project): Project =
|
||||
p.info.parent match
|
||||
{
|
||||
case Some(parent) => rootProject(parent)
|
||||
case None => p
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import java.io.File
|
||||
import FileUtilities._
|
||||
|
||||
final case class ProjectInfo(projectDirectory: File, dependencies: Iterable[Project], parent: Option[Project]) extends NotNull
|
||||
{
|
||||
val projectPath = new ProjectDirectory(projectDirectory)
|
||||
val builderPath = projectPath / ProjectInfo.MetadataDirectoryName
|
||||
def bootPath = builderPath / Project.BootDirectoryName
|
||||
def builderProjectPath = builderPath / Project.BuilderProjectDirectoryName
|
||||
def builderProjectOutputPath = builderProjectPath / Project.DefaultOutputDirectoryName
|
||||
def pluginsPath = builderPath / Project.PluginProjectDirectoryName
|
||||
def pluginsOutputPath = pluginsPath / Project.DefaultOutputDirectoryName
|
||||
def pluginsManagedSourcePath = pluginsPath / BasicDependencyPaths.DefaultManagedSourceDirectoryName
|
||||
def pluginsManagedDependencyPath = pluginsPath / BasicDependencyPaths.DefaultManagedDirectoryName
|
||||
}
|
||||
|
||||
private[sbt] sealed trait SetupResult extends NotNull
|
||||
private[sbt] final object SetupDeclined extends SetupResult
|
||||
private[sbt] final class SetupError(val message: String) extends SetupResult
|
||||
private[sbt] final object AlreadySetup extends SetupResult
|
||||
private[sbt] final class SetupInfo(val name: String, val version: Option[Version], val organization: Option[String], val initializeDirectories: Boolean) extends SetupResult
|
||||
|
||||
object ProjectInfo
|
||||
{
|
||||
val MetadataDirectoryName = "project"
|
||||
private val DefaultOrganization = "empty"
|
||||
|
||||
def setup(info: ProjectInfo, log: Logger): SetupResult =
|
||||
{
|
||||
val builderDirectory = info.builderPath.asFile
|
||||
if(builderDirectory.exists)
|
||||
{
|
||||
if(builderDirectory.isDirectory)
|
||||
AlreadySetup
|
||||
else
|
||||
new SetupError("'" + builderDirectory.getAbsolutePath + "' is not a directory.")
|
||||
}
|
||||
else
|
||||
setupProject(info.projectDirectory, log)
|
||||
}
|
||||
private def setupProject(projectDirectory: File, log: Logger): SetupResult =
|
||||
{
|
||||
if(confirmPrompt("No project found. Create new project?", false))
|
||||
{
|
||||
val name = trim(SimpleReader.readLine("Project Name: "))
|
||||
if(name.isEmpty)
|
||||
new SetupError("Project not created: no name specified.")
|
||||
else
|
||||
{
|
||||
val organization =
|
||||
{
|
||||
val org = trim(SimpleReader.readLine("Organization [" + DefaultOrganization + "]: "))
|
||||
if(org.isEmpty)
|
||||
DefaultOrganization
|
||||
else
|
||||
org
|
||||
}
|
||||
readVersion(projectDirectory, log) match
|
||||
{
|
||||
case None => new SetupError("Project not created: no version specified.")
|
||||
case Some(version) =>
|
||||
if(verifyCreateProject(name, version, organization))
|
||||
new SetupInfo(name, Some(version), Some(organization), true)
|
||||
else
|
||||
SetupDeclined
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
SetupDeclined
|
||||
}
|
||||
private def verifyCreateProject(name: String, version: Version, organization: String): Boolean =
|
||||
confirmPrompt("Create new project " + name + " " + version + " with organization " + organization +" ?", true)
|
||||
|
||||
private def confirmPrompt(question: String, defaultYes: Boolean) =
|
||||
{
|
||||
val choices = if(defaultYes) " (Y/n) " else " (y/N) "
|
||||
val answer = trim(SimpleReader.readLine(question + choices))
|
||||
val yes = "y" :: "yes" :: (if(defaultYes) List("") else Nil)
|
||||
yes.contains(answer.toLowerCase)
|
||||
}
|
||||
|
||||
private def readVersion(projectDirectory: File, log: Logger): Option[Version] =
|
||||
{
|
||||
val version = trim(SimpleReader.readLine("Version: "))
|
||||
if(version.isEmpty)
|
||||
None
|
||||
else
|
||||
{
|
||||
Version.fromString(version) match
|
||||
{
|
||||
case Left(errorMessage) =>
|
||||
{
|
||||
log.error("Invalid version: " + errorMessage)
|
||||
readVersion(projectDirectory, log)
|
||||
}
|
||||
case Right(v) => Some(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
private def trim(s: Option[String]) = s.getOrElse("")
|
||||
}
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
trait PackagePaths extends NotNull
|
||||
{
|
||||
def jarPath: Path
|
||||
def packageTestJar: Path
|
||||
def packageDocsJar: Path
|
||||
def packageSrcJar: Path
|
||||
def packageTestSrcJar: Path
|
||||
def packageProjectZip: Path
|
||||
}
|
||||
/** These are the paths required by BasicScalaProject.*/
|
||||
trait ScalaPaths extends PackagePaths
|
||||
{
|
||||
/** A PathFinder that selects all main sources.*/
|
||||
def mainSources: PathFinder
|
||||
/** A PathFinder that selects all test sources.*/
|
||||
def testSources: PathFinder
|
||||
/** A PathFinder that selects all main resources.*/
|
||||
def mainResources: PathFinder
|
||||
/** A PathFinder that selects all test resources. */
|
||||
def testResources: PathFinder
|
||||
|
||||
def mainResourceClasspath: PathFinder
|
||||
def testResourceClasspath: PathFinder
|
||||
|
||||
def mainCompilePath: Path
|
||||
def testCompilePath: Path
|
||||
def mainAnalysisPath: Path
|
||||
def testAnalysisPath: Path
|
||||
def mainDocPath: Path
|
||||
def testDocPath: Path
|
||||
def graphPath: Path
|
||||
|
||||
/** A PathFinder that selects all the classes compiled from the main sources.*/
|
||||
def mainClasses: PathFinder
|
||||
/** A PathFinder that selects all the classes compiled from the test sources.*/
|
||||
def testClasses: PathFinder
|
||||
|
||||
/** Declares all paths to be packaged by the package action.*/
|
||||
def packagePaths: PathFinder
|
||||
/** Declares all paths to be packaged by the package-test action.*/
|
||||
def packageTestPaths: PathFinder
|
||||
/** Declares all sources to be packaged by the package-src action.*/
|
||||
def packageSourcePaths: PathFinder
|
||||
/** Declares all sources to be packaged by the package-test-src action.*/
|
||||
def packageTestSourcePaths: PathFinder
|
||||
/** Declares all paths to be packaged by the package-project action.*/
|
||||
def packageProjectPaths: PathFinder
|
||||
|
||||
/** These are the directories that are created when a user makes a new project from sbt.*/
|
||||
protected def directoriesToCreate: List[Path]
|
||||
/** The directories to which a project writes are listed here and is used
|
||||
* to check a project and its dependencies for collisions.*/
|
||||
def outputDirectories: Iterable[Path]
|
||||
|
||||
def artifactBaseName: String
|
||||
}
|
||||
|
||||
trait BasicScalaPaths extends Project with ScalaPaths
|
||||
{
|
||||
def mainSourceRoots: PathFinder
|
||||
def testSourceRoots: PathFinder
|
||||
def mainResourcesPath: PathFinder
|
||||
def testResourcesPath: PathFinder
|
||||
def managedDependencyRootPath: Path
|
||||
def dependencyPath: Path
|
||||
|
||||
protected def sources(base: PathFinder) = descendents(base, sourceExtensions)
|
||||
protected def sourceExtensions = "*.scala" | "*.java"
|
||||
|
||||
def mainSources =
|
||||
{
|
||||
val normal = sources(mainSourceRoots)
|
||||
if(scratch)
|
||||
normal +++ (info.projectPath * sourceExtensions)
|
||||
else
|
||||
normal
|
||||
}
|
||||
def testSources = sources(testSourceRoots)
|
||||
|
||||
def mainResourceClasspath = mainResourcesPath
|
||||
def testResourceClasspath = testResourcesPath
|
||||
def mainResources = descendents(mainResourcesPath ##, "*")
|
||||
def testResources = descendents(testResourcesPath ##, "*")
|
||||
|
||||
def mainClasses = (mainCompilePath ##) ** "*.class"
|
||||
def testClasses = (testCompilePath ##) ** "*.class"
|
||||
|
||||
def packagePaths = mainClasses +++ mainResources
|
||||
def packageTestPaths = testClasses +++ testResources
|
||||
def packageSourcePaths = mainSources +++ mainResources
|
||||
def packageTestSourcePaths = testSources +++ testResources
|
||||
def packageProjectPaths = descendents( (info.projectPath ##), "*") --- (packageProjectExcludes ** "*")
|
||||
protected def packageProjectExcludes: PathFinder =
|
||||
outputRootPath +++ managedDependencyRootPath +++
|
||||
info.bootPath +++ info.builderProjectOutputPath +++
|
||||
info.pluginsOutputPath +++ info.pluginsManagedSourcePath +++ info.pluginsManagedDependencyPath
|
||||
|
||||
override def outputDirectories = outputRootPath :: managedDependencyRootPath :: Nil
|
||||
}
|
||||
|
||||
@deprecated trait BasicProjectPaths extends MavenStyleScalaPaths
|
||||
trait MavenStyleScalaPaths extends BasicScalaPaths with BasicPackagePaths
|
||||
{
|
||||
import BasicProjectPaths._
|
||||
|
||||
def outputPath: Path
|
||||
|
||||
def sourceDirectoryName = DefaultSourceDirectoryName
|
||||
def mainDirectoryName = DefaultMainDirectoryName
|
||||
def scalaDirectoryName = DefaultScalaDirectoryName
|
||||
def javaDirectoryName = DefaultJavaDirectoryName
|
||||
def resourcesDirectoryName = DefaultResourcesDirectoryName
|
||||
def testDirectoryName = DefaultTestDirectoryName
|
||||
def mainCompileDirectoryName = DefaultMainCompileDirectoryName
|
||||
def testCompileDirectoryName = DefaultTestCompileDirectoryName
|
||||
def docDirectoryName = DefaultDocDirectoryName
|
||||
def apiDirectoryName = DefaultAPIDirectoryName
|
||||
def graphDirectoryName = DefaultGraphDirectoryName
|
||||
def mainAnalysisDirectoryName = DefaultMainAnalysisDirectoryName
|
||||
def testAnalysisDirectoryName = DefaultTestAnalysisDirectoryName
|
||||
|
||||
def sourcePath = path(sourceDirectoryName)
|
||||
|
||||
def mainSourcePath = sourcePath / mainDirectoryName
|
||||
def mainScalaSourcePath = mainSourcePath / scalaDirectoryName
|
||||
def mainJavaSourcePath = mainSourcePath / javaDirectoryName
|
||||
def mainResourcesPath = mainSourcePath / resourcesDirectoryName
|
||||
def mainDocPath = docPath / mainDirectoryName / apiDirectoryName
|
||||
def mainCompilePath = outputPath / mainCompileDirectoryName
|
||||
def mainAnalysisPath = outputPath / mainAnalysisDirectoryName
|
||||
|
||||
def testSourcePath = sourcePath / testDirectoryName
|
||||
def testJavaSourcePath = testSourcePath / javaDirectoryName
|
||||
def testScalaSourcePath = testSourcePath / scalaDirectoryName
|
||||
def testResourcesPath = testSourcePath / resourcesDirectoryName
|
||||
def testDocPath = docPath / testDirectoryName / apiDirectoryName
|
||||
def testCompilePath = outputPath / testCompileDirectoryName
|
||||
def testAnalysisPath = outputPath / testAnalysisDirectoryName
|
||||
|
||||
def docPath = outputPath / docDirectoryName
|
||||
def graphPath = outputPath / graphDirectoryName
|
||||
|
||||
/** These are the directories that are created when a user makes a new project from sbt.*/
|
||||
protected def directoriesToCreate: List[Path] =
|
||||
dependencyPath ::
|
||||
mainScalaSourcePath ::
|
||||
mainResourcesPath ::
|
||||
testScalaSourcePath ::
|
||||
testResourcesPath ::
|
||||
Nil
|
||||
|
||||
def mainSourceRoots = mainJavaSourcePath +++ mainScalaSourcePath
|
||||
def testSourceRoots = testJavaSourcePath +++ testScalaSourcePath
|
||||
}
|
||||
|
||||
trait BasicPackagePaths extends ScalaPaths with PackagePaths
|
||||
{
|
||||
def outputPath: Path
|
||||
|
||||
def defaultJarBaseName: String = artifactBaseName
|
||||
def defaultJarName = defaultJarBaseName + ".jar"
|
||||
def jarPath = outputPath / defaultJarName
|
||||
def packageTestJar = defaultJarPath("-test.jar")
|
||||
def packageDocsJar = defaultJarPath("-docs.jar")
|
||||
def packageSrcJar= defaultJarPath("-src.jar")
|
||||
def packageTestSrcJar = defaultJarPath("-test-src.jar")
|
||||
def packageProjectZip = defaultJarPath("-project.zip")
|
||||
def defaultJarPath(extension: String) = outputPath / (artifactBaseName + extension)
|
||||
}
|
||||
|
||||
object BasicProjectPaths
|
||||
{
|
||||
val DefaultSourceDirectoryName = "src"
|
||||
val DefaultMainCompileDirectoryName = "classes"
|
||||
val DefaultTestCompileDirectoryName = "test-classes"
|
||||
val DefaultDocDirectoryName = "doc"
|
||||
val DefaultAPIDirectoryName = "api"
|
||||
val DefaultGraphDirectoryName = "graph"
|
||||
val DefaultMainAnalysisDirectoryName = "analysis"
|
||||
val DefaultTestAnalysisDirectoryName = "test-analysis"
|
||||
|
||||
val DefaultMainDirectoryName = "main"
|
||||
val DefaultScalaDirectoryName = "scala"
|
||||
val DefaultJavaDirectoryName = "java"
|
||||
val DefaultResourcesDirectoryName = "resources"
|
||||
val DefaultTestDirectoryName = "test"
|
||||
|
||||
// forwarders to new locations
|
||||
def BootDirectoryName = Project.BootDirectoryName
|
||||
def DefaultManagedDirectoryName = BasicDependencyPaths.DefaultManagedDirectoryName
|
||||
def DefaultDependencyDirectoryName = BasicDependencyPaths.DefaultDependencyDirectoryName
|
||||
}
|
||||
|
||||
trait WebScalaPaths extends ScalaPaths
|
||||
{
|
||||
def temporaryWarPath: Path
|
||||
def webappResources: PathFinder
|
||||
def jettyContextPath: String
|
||||
def warPath: Path
|
||||
}
|
||||
@deprecated trait WebProjectPaths extends MavenStyleWebScalaPaths
|
||||
trait MavenStyleWebScalaPaths extends WebScalaPaths with MavenStyleScalaPaths
|
||||
{
|
||||
import WebProjectPaths._
|
||||
def temporaryWarPath = outputPath / webappDirectoryName
|
||||
def webappPath = mainSourcePath / webappDirectoryName
|
||||
def webappDirectoryName = DefaultWebappDirectoryName
|
||||
def jettyContextPath = DefaultJettyContextPath
|
||||
def defaultWarName = defaultJarBaseName + ".war"
|
||||
def warPath = outputPath / defaultWarName
|
||||
/** Additional files to include in the web application. */
|
||||
protected def extraWebappFiles: PathFinder = Path.emptyPathFinder
|
||||
def webappResources = descendents(webappPath ##, "*") +++ extraWebappFiles
|
||||
}
|
||||
object WebProjectPaths
|
||||
{
|
||||
val DefaultWebappDirectoryName = "webapp"
|
||||
val DefaultJettyContextPath = "/"
|
||||
}
|
||||
|
||||
/** Defines default paths for a webstart project. It directly extends WebstartOptions to make
|
||||
* it easy to implement and override webstart options in the common case of one webstartTask per
|
||||
* project.*/
|
||||
trait WebstartPaths extends ScalaPaths
|
||||
{
|
||||
import WebstartPaths._
|
||||
|
||||
def outputPath: Path
|
||||
def jnlpPath: Path
|
||||
|
||||
def webstartOutputDirectory = outputPath / webstartDirectoryName
|
||||
|
||||
def jnlpFile = webstartOutputDirectory / jnlpFileName
|
||||
def webstartLibDirectory = webstartOutputDirectory / webstartLibName
|
||||
def webstartZip: Option[Path] = Some(outputPath / webstartZipName)
|
||||
def jnlpResourcesPath = jnlpPath / BasicProjectPaths.DefaultResourcesDirectoryName
|
||||
|
||||
def webstartLibName = DefaultWebstartLibName
|
||||
def webstartDirectoryName = DefaultWebstartDirectoryName
|
||||
|
||||
def webstartZipName: String
|
||||
def jnlpFileName: String
|
||||
}
|
||||
object WebstartPaths
|
||||
{
|
||||
val DefaultWebstartDirectoryName = "webstart"
|
||||
val DefaultJnlpName = "jnlp"
|
||||
val DefaultWebstartLibName = "lib"
|
||||
}
|
||||
trait MavenStyleWebstartPaths extends WebstartPaths with MavenStyleScalaPaths
|
||||
{
|
||||
import WebstartPaths._
|
||||
def jnlpPath = mainSourcePath / DefaultJnlpName
|
||||
def webstartMainJar = jarPath
|
||||
def jnlpFileName = DefaultJnlpFileName
|
||||
def webstartZipName = artifactBaseName + ".zip"
|
||||
def DefaultJnlpFileName = artifactBaseName + ".jnlp"
|
||||
}
|
||||
|
||||
trait IntegrationTestPaths extends NotNull
|
||||
{
|
||||
def integrationTestSources: PathFinder
|
||||
def integrationTestResourcesPath: Path
|
||||
|
||||
def integrationTestCompilePath: Path
|
||||
def integrationTestAnalysisPath: Path
|
||||
}
|
||||
trait BasicIntegrationTestPaths extends IntegrationTestPaths
|
||||
{
|
||||
def integrationTestScalaSourcePath: Path
|
||||
def integrationTestSources = sources(integrationTestScalaSourcePath)
|
||||
protected def sources(base: Path): PathFinder
|
||||
}
|
||||
trait MavenStyleIntegrationTestPaths extends BasicIntegrationTestPaths with MavenStyleScalaPaths
|
||||
{
|
||||
import IntegrationTestPaths._
|
||||
|
||||
def integrationTestDirectoryName = DefaultIntegrationTestDirectoryName
|
||||
def integrationTestCompileDirectoryName = DefaultIntegrationTestCompileDirectoryName
|
||||
def integrationTestAnalysisDirectoryName = DefaultIntegrationTestAnalysisDirectoryName
|
||||
|
||||
def integrationTestSourcePath = sourcePath / integrationTestDirectoryName
|
||||
def integrationTestScalaSourcePath = integrationTestSourcePath / scalaDirectoryName
|
||||
def integrationTestResourcesPath = integrationTestSourcePath / resourcesDirectoryName
|
||||
|
||||
def integrationTestCompilePath = outputPath / integrationTestCompileDirectoryName
|
||||
def integrationTestAnalysisPath = outputPath / integrationTestAnalysisDirectoryName
|
||||
}
|
||||
|
||||
object IntegrationTestPaths
|
||||
{
|
||||
val DefaultIntegrationTestDirectoryName = "it"
|
||||
val DefaultIntegrationTestCompileDirectoryName = "it-classes"
|
||||
val DefaultIntegrationTestAnalysisDirectoryName = "it-analysis"
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008 David MacIver, Mark Harrah
|
||||
*/
|
||||
package sbt;
|
||||
|
||||
import scala.collection._
|
||||
|
||||
object ReflectUtilities
|
||||
{
|
||||
def transformCamelCase(name: String, separator: Char) =
|
||||
{
|
||||
val buffer = new StringBuilder
|
||||
for(char <- name)
|
||||
{
|
||||
import java.lang.Character._
|
||||
if(isUpperCase(char))
|
||||
{
|
||||
buffer += separator
|
||||
buffer += toLowerCase(char)
|
||||
}
|
||||
else
|
||||
buffer += char
|
||||
}
|
||||
buffer.toString
|
||||
}
|
||||
|
||||
def ancestry(clazz : Class[_]) : List[Class[_]] =
|
||||
if (clazz == classOf[AnyRef] || !classOf[AnyRef].isAssignableFrom(clazz)) List(clazz)
|
||||
else clazz :: ancestry(clazz.getSuperclass);
|
||||
|
||||
def fields(clazz : Class[_]) =
|
||||
mutable.OpenHashMap(ancestry(clazz).
|
||||
flatMap(_.getDeclaredFields).
|
||||
map(f => (f.getName, f)):_*)
|
||||
|
||||
def allValsC[T](self: AnyRef, clazz: Class[T]): Map[String, T] =
|
||||
{
|
||||
val mappings = new mutable.OpenHashMap[String, T]
|
||||
val correspondingFields = fields(self.getClass)
|
||||
for(method <- self.getClass.getMethods)
|
||||
{
|
||||
if(method.getParameterTypes.length == 0 && clazz.isAssignableFrom(method.getReturnType))
|
||||
{
|
||||
for(field <- correspondingFields.get(method.getName) if field.getType == method.getReturnType)
|
||||
mappings(method.getName) = method.invoke(self).asInstanceOf[T]
|
||||
}
|
||||
}
|
||||
mappings
|
||||
}
|
||||
def allVals[T](self: AnyRef)(implicit mt: scala.reflect.Manifest[T]): Map[String, T] =
|
||||
allValsC(self, mt.erasure).asInstanceOf[Map[String,T]]
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import java.io.File
|
||||
import FileUtilities._
|
||||
|
||||
object Resources
|
||||
{
|
||||
def apply(basePath: String) =
|
||||
{
|
||||
require(basePath.startsWith("/"))
|
||||
val resource = getClass.getResource(basePath)
|
||||
if(resource == null)
|
||||
throw new Exception("Resource base directory '" + basePath + "' not on classpath.")
|
||||
else
|
||||
{
|
||||
val file = new File(resource.toURI)
|
||||
if(file.exists)
|
||||
new Resources(file)
|
||||
else
|
||||
throw new Exception("Resource base directory '" + basePath + "' does not exist.")
|
||||
}
|
||||
}
|
||||
private val LoadErrorPrefix = "Error loading initial project: "
|
||||
}
|
||||
|
||||
class Resources(val baseDirectory: File)
|
||||
{
|
||||
import Resources._
|
||||
// The returned directory is not actually read-only, but it should be treated that way
|
||||
def readOnlyResourceDirectory(group: String, name: String): Either[String, File] =
|
||||
{
|
||||
val groupDirectory = new File(baseDirectory, group)
|
||||
if(groupDirectory.isDirectory)
|
||||
{
|
||||
val resourceDirectory = new File(groupDirectory, name)
|
||||
if(resourceDirectory.isDirectory)
|
||||
Right(resourceDirectory)
|
||||
else
|
||||
Left("Resource directory '" + name + "' in group '" + group + "' not found.")
|
||||
}
|
||||
else
|
||||
Left("Group '" + group + "' not found.")
|
||||
}
|
||||
def readWriteResourceDirectory[T](group: String, name: String, log: Logger)
|
||||
(withDirectory: File => Either[String, T]): Either[String, T] =
|
||||
readOnlyResourceDirectory(group, name).right flatMap(file => readWriteResourceDirectory(file, log)(withDirectory))
|
||||
def readWriteResourceDirectory[T](readOnly: File, log: Logger)
|
||||
(withDirectory: File => Either[String, T]): Either[String, T] =
|
||||
{
|
||||
require(readOnly.isDirectory)
|
||||
def readWrite(readOnly: File)(temporary: File): Either[String, T] =
|
||||
{
|
||||
val readWriteDirectory = new File(temporary, readOnly.getName)
|
||||
FileUtilities.copyDirectory(readOnly, readWriteDirectory, log).toLeft(()).right flatMap { x =>
|
||||
withDirectory(readWriteDirectory)
|
||||
}
|
||||
}
|
||||
doInTemporaryDirectory(log)(readWrite(readOnly))
|
||||
}
|
||||
|
||||
def withProject[T](projectDirectory: File, log: Logger)(f: Project => WithProjectResult[T]): Either[String, T] =
|
||||
readWriteResourceDirectory(projectDirectory, log)(withProject(log)(f))
|
||||
def withProject[T](group: String, name: String, log: Logger)(f: Project => WithProjectResult[T]): Either[String, T] =
|
||||
readWriteResourceDirectory(group, name, log)(withProject(log)(f))
|
||||
def withProject[T](log: Logger)(f: Project => WithProjectResult[T])(dir: File): Either[String, T] =
|
||||
withProject(log, None, new ReloadSuccessExpected(LoadErrorPrefix), dir )(f)
|
||||
private def withProject[T](log: Logger, previousProject: Option[Project], reload: ReloadProject, dir: File)
|
||||
(f: Project => WithProjectResult[T]): Either[String, T] =
|
||||
{
|
||||
require(previousProject.isDefined || reload != NoReload, "Previous project undefined and reload not requested.")
|
||||
val loadResult =
|
||||
if(reload == NoReload && previousProject.isDefined)
|
||||
Right(previousProject.get)
|
||||
else
|
||||
{
|
||||
val buffered = new BufferedLogger(log)
|
||||
def error(msg: String) =
|
||||
{
|
||||
buffered.playAll()
|
||||
buffered.stop()
|
||||
Left(msg)
|
||||
}
|
||||
|
||||
buffered.startRecording()
|
||||
resultToEither(Project.loadProject(dir, Nil, None, buffered)) match
|
||||
{
|
||||
case Left(msg) =>
|
||||
reload match
|
||||
{
|
||||
case ReloadErrorExpected =>
|
||||
buffered.stop()
|
||||
previousProject.toRight("Initial project load failed.")
|
||||
case s: ReloadSuccessExpected => error(s.prefixIfError + msg)
|
||||
case NoReload /* shouldn't happen */=> error(msg)
|
||||
}
|
||||
case Right(p) =>
|
||||
reload match
|
||||
{
|
||||
case ReloadErrorExpected => error("Expected project load failure, but it succeeded.")
|
||||
case _ =>
|
||||
buffered.stop()
|
||||
Right(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
loadResult match
|
||||
{
|
||||
case Right(project) =>
|
||||
project.log.enableTrace(log.traceEnabled)
|
||||
project.log.setLevel(log.getLevel)
|
||||
f(project) match
|
||||
{
|
||||
case ContinueResult(newF, newReload) => withProject(log, Some(project), newReload, dir)(newF)
|
||||
case ValueResult(value) => Right(value)
|
||||
case err: ErrorResult => Left(err.message)
|
||||
}
|
||||
case Left(message) => Left(message)
|
||||
}
|
||||
}
|
||||
|
||||
def resultToEither(result: LoadResult): Either[String, Project] =
|
||||
result match
|
||||
{
|
||||
case success: LoadSuccess => Right(success.project)
|
||||
case err: LoadError => Left(err.message)
|
||||
case err: LoadSetupError => Left(err.message)
|
||||
case LoadSetupDeclined => Left("Setup declined")
|
||||
}
|
||||
}
|
||||
sealed trait ReloadProject extends NotNull
|
||||
final object ReloadErrorExpected extends ReloadProject
|
||||
final class ReloadSuccessExpected(val prefixIfError: String) extends ReloadProject
|
||||
final object NoReload extends ReloadProject
|
||||
|
||||
sealed trait WithProjectResult[+T] extends NotNull
|
||||
final case class ContinueResult[T](f: Project => WithProjectResult[T], reload: ReloadProject) extends WithProjectResult[T]
|
||||
final case class ValueResult[T](value: T) extends WithProjectResult[T]
|
||||
final class ErrorResult(val message: String) extends WithProjectResult[Nothing]
|
||||
object ContinueResult
|
||||
{
|
||||
def apply[T](f: Project => WithProjectResult[T], prefixIfError: Option[String]) =
|
||||
{
|
||||
val reload = prefixIfError match { case None => NoReload; case Some(p) => new ReloadSuccessExpected(p) }
|
||||
new ContinueResult[T](f, reload)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import scala.tools.nsc.{GenericRunnerCommand, Interpreter, InterpreterLoop, ObjectRunner, Settings}
|
||||
import scala.tools.nsc.interpreter.InteractiveReader
|
||||
import scala.tools.nsc.reporters.Reporter
|
||||
import scala.tools.nsc.util.ClassPath
|
||||
|
||||
import java.io.File
|
||||
import java.net.{URL, URLClassLoader}
|
||||
|
||||
trait ScalaRun
|
||||
{
|
||||
def console(classpath: Iterable[Path], log: Logger): Option[String]
|
||||
def run(mainClass: String, classpath: Iterable[Path], options: Seq[String], log: Logger): Option[String]
|
||||
}
|
||||
class ForkRun(config: ForkScalaRun) extends ScalaRun
|
||||
{
|
||||
def console(classpath: Iterable[Path], log: Logger): Option[String] =
|
||||
{
|
||||
error("Forking the interpreter is not implemented.")
|
||||
//val exitCode = Fork.scala(config.javaHome, config.runJVMOptions, config.scalaJars, classpathOption(classpath), config.workingDirectory, log)
|
||||
//processExitCode(exitCode, "interpreter")
|
||||
}
|
||||
def run(mainClass: String, classpath: Iterable[Path], options: Seq[String], log: Logger): Option[String] =
|
||||
{
|
||||
val scalaOptions = classpathOption(classpath) ::: mainClass :: options.toList
|
||||
val exitCode = Fork.scala(config.javaHome, config.runJVMOptions, config.scalaJars, scalaOptions, config.workingDirectory, log)
|
||||
processExitCode(exitCode, "runner")
|
||||
}
|
||||
private def classpathOption(classpath: Iterable[Path]) = "-cp" :: Path.makeString(classpath) :: Nil
|
||||
private def processExitCode(exitCode: Int, label: String) =
|
||||
{
|
||||
if(exitCode == 0)
|
||||
None
|
||||
else
|
||||
Some("Nonzero exit code returned from " + label + ": " + exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
/** This module is an interface to starting the scala interpreter or runner.*/
|
||||
object Run extends ScalaRun
|
||||
{
|
||||
/** Starts an interactive scala interpreter session with the given classpath.*/
|
||||
def console(classpath: Iterable[Path], log: Logger) =
|
||||
createSettings(log)
|
||||
{
|
||||
(settings: Settings) =>
|
||||
{
|
||||
settings.classpath.value = Path.makeString(classpath)
|
||||
log.info("Starting scala interpreter...")
|
||||
log.debug(" Classpath: " + settings.classpath.value)
|
||||
log.info("")
|
||||
Control.trapUnit("Error during session: ", log)
|
||||
{
|
||||
val loop = new InterpreterLoop
|
||||
executeTrapExit(loop.main(settings), log)
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Executes the given function, trapping calls to System.exit. */
|
||||
private def executeTrapExit(f: => Unit, log: Logger): Option[String] =
|
||||
{
|
||||
val exitCode = TrapExit(f, log)
|
||||
if(exitCode == 0)
|
||||
{
|
||||
log.debug("Exited with code 0")
|
||||
None
|
||||
}
|
||||
else
|
||||
Some("Nonzero exit code: " + exitCode)
|
||||
}
|
||||
/** Runs the class 'mainClass' using the given classpath and options using the scala runner.*/
|
||||
def run(mainClass: String, classpath: Iterable[Path], options: Seq[String], log: Logger) =
|
||||
{
|
||||
createSettings(log)
|
||||
{
|
||||
(settings: Settings) =>
|
||||
{
|
||||
Control.trapUnit("Error during run: ", log)
|
||||
{
|
||||
val classpathURLs = classpath.map(_.asURL).toList
|
||||
val bootClasspath = FileUtilities.pathSplit(settings.bootclasspath.value)
|
||||
val extraURLs =
|
||||
for(pathString <- bootClasspath if pathString.length > 0) yield
|
||||
(new java.io.File(pathString)).toURI.toURL
|
||||
log.info("Running " + mainClass + " ...")
|
||||
log.debug(" Classpath:" + (classpathURLs ++ extraURLs).mkString("\n\t", "\n\t",""))
|
||||
executeTrapExit( ObjectRunner.run(classpathURLs ++ extraURLs, mainClass, options.toList), log )
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** If mainClassOption is None, then the interactive scala interpreter is started with the given classpath.
|
||||
* Otherwise, the class wrapped by Some is run using the scala runner with the given classpath and
|
||||
* options. */
|
||||
def apply(mainClassOption: Option[String], classpath: Iterable[Path], options: Seq[String], log: Logger) =
|
||||
{
|
||||
mainClassOption match
|
||||
{
|
||||
case Some(mainClass) => run(mainClass, classpath, options, log)
|
||||
case None => console(classpath, log)
|
||||
}
|
||||
}
|
||||
/** Create a settings object and execute the provided function if the settings are created ok.*/
|
||||
private def createSettings(log: Logger)(f: Settings => Option[String]) =
|
||||
{
|
||||
val command = new GenericRunnerCommand(Nil, message => log.error(message))
|
||||
if(command.ok)
|
||||
f(command.settings)
|
||||
else
|
||||
Some(command.usageMsg)
|
||||
}
|
||||
|
||||
/** Starts a Scala interpreter session with 'project' bound to the value 'current' in the console
|
||||
* and the following two lines executed:
|
||||
* import sbt._
|
||||
* import current._
|
||||
*/
|
||||
def projectConsole(project: Project): Option[String] =
|
||||
{
|
||||
import project.log
|
||||
createSettings(log) { interpreterSettings =>
|
||||
createSettings(log) { compilerSettings =>
|
||||
log.info("Starting scala interpreter with project definition " + project.name + " ...")
|
||||
log.info("")
|
||||
Control.trapUnit("Error during session: ", log)
|
||||
{
|
||||
val loop = new ProjectInterpreterLoop(compilerSettings, project)
|
||||
executeTrapExit(loop.main(interpreterSettings), log)
|
||||
}
|
||||
}}
|
||||
}
|
||||
/** A custom InterpreterLoop with the purpose of creating an interpreter with Project 'project' bound to the value 'current',
|
||||
* and the following two lines interpreted:
|
||||
* import sbt._
|
||||
* import current._.
|
||||
* To do this,
|
||||
* 1) The compiler uses a different settings instance: 'compilerSettings', which will have its classpath set to include the classpath
|
||||
* of the loader that loaded 'project'. The compiler can then find the classes it needs to compile code referencing the project.
|
||||
* 2) The parent class loader for the interpreter is the loader that loaded the project, so that the project can be bound to a variable
|
||||
* in the interpreter.
|
||||
*/
|
||||
private class ProjectInterpreterLoop(compilerSettings: Settings, project: Project) extends InterpreterLoop
|
||||
{
|
||||
override def createInterpreter()
|
||||
{
|
||||
val loader = project.getClass.getClassLoader.asInstanceOf[URLClassLoader]
|
||||
compilerSettings.classpath.value = loader.getURLs.flatMap(ClasspathUtilities.asFile).map(_.getAbsolutePath).mkString(File.pathSeparator)
|
||||
project.log.debug(" Compiler classpath: " + compilerSettings.classpath.value)
|
||||
|
||||
in = InteractiveReader.createDefault()
|
||||
interpreter = new Interpreter(settings)
|
||||
{
|
||||
override protected def parentClassLoader = loader
|
||||
override protected def newCompiler(settings: Settings, reporter: Reporter) = super.newCompiler(compilerSettings, reporter)
|
||||
}
|
||||
interpreter.setContextClassLoader()
|
||||
interpreter.bind("current", project.getClass.getName, project)
|
||||
interpreter.interpret("import sbt._")
|
||||
interpreter.interpret("import Process._")
|
||||
interpreter.interpret("import current._")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah, David MacIver
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import FileUtilities._
|
||||
import java.io.File
|
||||
import java.util.jar.{Attributes, Manifest}
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
trait SimpleScalaProject extends Project
|
||||
{
|
||||
def errorTask(message: String) = task{ Some(message) }
|
||||
|
||||
trait CleanOption extends ActionOption
|
||||
case class ClearAnalysis(analysis: TaskAnalysis[_, _, _]) extends CleanOption
|
||||
case class Preserve(paths: PathFinder) extends CleanOption
|
||||
|
||||
case class CompileOption(val asString: String) extends ActionOption
|
||||
case class JavaCompileOption(val asString: String) extends ActionOption
|
||||
|
||||
val Deprecation = CompileOption("-deprecation")
|
||||
val ExplainTypes = CompileOption("-explaintypes")
|
||||
val Optimize = CompileOption("-optimise")
|
||||
def Optimise = Optimize
|
||||
val Verbose = CompileOption("-verbose")
|
||||
val Unchecked = CompileOption("-unchecked")
|
||||
val DisableWarnings = CompileOption("-nowarn")
|
||||
def target(target: Target.Value) = CompileOption("-target:" + target)
|
||||
object Target extends Enumeration
|
||||
{
|
||||
val Java1_5 = Value("jvm-1.5")
|
||||
val Java1_4 = Value("jvm-1.4")
|
||||
val Msil = Value("msil")
|
||||
}
|
||||
|
||||
def cleanTask(paths: PathFinder, options: CleanOption*): Task =
|
||||
cleanTask(paths, options)
|
||||
def cleanTask(paths: PathFinder, options: => Seq[CleanOption]): Task =
|
||||
task
|
||||
{
|
||||
val cleanOptions = options
|
||||
val preservePaths = for(Preserve(preservePaths) <- cleanOptions; toPreserve <- preservePaths.get) yield toPreserve
|
||||
Control.thread(FileUtilities.preserve(preservePaths, log))
|
||||
{ preserved =>
|
||||
val pathClean = FileUtilities.clean(paths.get, log)
|
||||
for(ClearAnalysis(analysis) <- cleanOptions)
|
||||
{
|
||||
analysis.clear()
|
||||
analysis.save()
|
||||
}
|
||||
val restored = preserved.restore(log)
|
||||
pathClean orElse restored
|
||||
}
|
||||
}
|
||||
}
|
||||
trait ScalaProject extends SimpleScalaProject with FileTasks
|
||||
{
|
||||
import ScalaProject._
|
||||
|
||||
final case class MaxCompileErrors(val value: Int) extends CompileOption("") with ScaladocOption { def asList = Nil }
|
||||
trait PackageOption extends ActionOption
|
||||
trait TestOption extends ActionOption
|
||||
|
||||
case class TestSetup(setup: () => Option[String]) extends TestOption
|
||||
case class TestCleanup(cleanup: () => Option[String]) extends TestOption
|
||||
case class ExcludeTests(tests: Iterable[String]) extends TestOption
|
||||
case class TestListeners(listeners: Iterable[TestReportListener]) extends TestOption
|
||||
case class TestFilter(filterTest: String => Boolean) extends TestOption
|
||||
|
||||
case class JarManifest(m: Manifest) extends PackageOption
|
||||
{
|
||||
assert(m != null)
|
||||
}
|
||||
case class MainClass(mainClassName: String) extends PackageOption
|
||||
case class ManifestAttributes(attributes: (Attributes.Name, String)*) extends PackageOption
|
||||
case object Recursive extends PackageOption
|
||||
def ManifestAttributes(attributes: (String, String)*): ManifestAttributes =
|
||||
{
|
||||
val converted = for( (name,value) <- attributes ) yield (new Attributes.Name(name), value)
|
||||
new ManifestAttributes(converted : _*)
|
||||
}
|
||||
|
||||
|
||||
trait ScaladocOption extends ActionOption
|
||||
{
|
||||
def asList: List[String]
|
||||
}
|
||||
case class SimpleDocOption(optionValue: String) extends ScaladocOption
|
||||
{
|
||||
def asList = List(optionValue)
|
||||
}
|
||||
case class CompoundDocOption(label: String, value: String) extends ScaladocOption
|
||||
{
|
||||
def asList = List(label, value)
|
||||
}
|
||||
val LinkSource = SimpleDocOption("-linksource")
|
||||
val NoComment = SimpleDocOption("-nocomment")
|
||||
def access(access: Access.Value) = SimpleDocOption("-access:" + access)
|
||||
def documentBottom(bottomText: String) = CompoundDocOption("-bottom", bottomText)
|
||||
def documentCharset(charset: String) = CompoundDocOption("-charset", charset)
|
||||
def documentTitle(title: String) = CompoundDocOption("-doctitle", title)
|
||||
def documentFooter(footerText: String) = CompoundDocOption("-footer", footerText)
|
||||
def documentHeader(headerText: String) = CompoundDocOption("-header", headerText)
|
||||
def stylesheetFile(path: Path) = CompoundDocOption("-stylesheetfile", path.asFile.getAbsolutePath)
|
||||
def documentTop(topText: String) = CompoundDocOption("-top", topText)
|
||||
def windowTitle(title: String) = CompoundDocOption("-windowtitle", title)
|
||||
|
||||
object Access extends Enumeration
|
||||
{
|
||||
val Public = Value("public")
|
||||
val Default = Value("protected")
|
||||
val Private = Value("private")
|
||||
}
|
||||
|
||||
def consoleTask(classpath : PathFinder): Task =
|
||||
consoleTask(classpath, Run)
|
||||
def consoleTask(classpath : PathFinder, runner: ScalaRun): Task =
|
||||
interactiveTask { runner.console(classpath.get, log) }
|
||||
|
||||
def runTask(mainClass: => Option[String], classpath: PathFinder, options: String*): Task =
|
||||
runTask(mainClass, classpath, options)
|
||||
def runTask(mainClass: => Option[String], classpath: PathFinder, options: => Seq[String]): Task =
|
||||
runTask(mainClass, classpath, options, Run)
|
||||
def runTask(mainClass: => Option[String], classpath: PathFinder, options: => Seq[String], runner: ScalaRun): Task =
|
||||
task
|
||||
{
|
||||
mainClass match
|
||||
{
|
||||
case Some(main) => runner.run(main, classpath.get, options, log)
|
||||
case None => Some("No main class specified.")
|
||||
}
|
||||
}
|
||||
|
||||
def syncTask(sourceDirectory: Path, destinationDirectory: Path): Task =
|
||||
task { FileUtilities.sync(sourceDirectory, destinationDirectory, log) }
|
||||
def copyTask(sources: PathFinder, destinationDirectory: Path): Task =
|
||||
task { FileUtilities.copy(sources.get, destinationDirectory, log).left.toOption }
|
||||
|
||||
def testTask(frameworks: Iterable[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, options: TestOption*): Task =
|
||||
testTask(frameworks, classpath, analysis, options)
|
||||
def testTask(frameworks: Iterable[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, options: => Seq[TestOption]): Task =
|
||||
{
|
||||
def work =
|
||||
{
|
||||
val (begin, work, end) = testTasks(frameworks, classpath, analysis, options)
|
||||
val beginTasks = begin.map(toTask).toSeq // test setup tasks
|
||||
val workTasks = work.map(w => toTask(w) dependsOn(beginTasks : _*)) // the actual tests
|
||||
val endTasks = end.map(toTask).toSeq // tasks that perform test cleanup and are run regardless of success of tests
|
||||
val endTask = task { None } named("test-cleanup") dependsOn(endTasks : _*)
|
||||
val rootTask = task { None } named("test-complete") dependsOn(workTasks.toSeq : _*) // the task that depends on all test subtasks
|
||||
new SubWork[Project#Task](ParallelRunner.dagScheduler(rootTask), ParallelRunner.dagScheduler(endTask))
|
||||
}
|
||||
new CompoundTask(work)
|
||||
}
|
||||
private def toTask(testTask: NamedTestTask) = task(testTask.run()) named(testTask.name)
|
||||
|
||||
def graphTask(outputDirectory: Path, analysis: CompileAnalysis): Task = task { DotGraph(analysis, outputDirectory, log) }
|
||||
def scaladocTask(label: String, sources: PathFinder, outputDirectory: Path, classpath: PathFinder, options: ScaladocOption*): Task =
|
||||
scaladocTask(label, sources, outputDirectory, classpath, options)
|
||||
def scaladocTask(label: String, sources: PathFinder, outputDirectory: Path, classpath: PathFinder, options: => Seq[ScaladocOption]): Task =
|
||||
task
|
||||
{
|
||||
val classpathString = Path.makeString(classpath.get)
|
||||
val optionsLocal = options
|
||||
val maxErrors = maximumErrors(optionsLocal)
|
||||
(new Scaladoc(maxErrors))(label, sources.get, classpathString, outputDirectory, optionsLocal.flatMap(_.asList), log)
|
||||
}
|
||||
|
||||
def packageTask(sources: PathFinder, outputDirectory: Path, jarName: => String, options: PackageOption*): Task =
|
||||
packageTask(sources, outputDirectory / jarName, options)
|
||||
def packageTask(sources: PathFinder, outputDirectory: Path, jarName: => String, options: => Seq[PackageOption]): Task =
|
||||
packageTask(sources: PathFinder, outputDirectory / jarName, options)
|
||||
def packageTask(sources: PathFinder, jarPath: => Path, options: PackageOption*): Task =
|
||||
packageTask(sources, jarPath, options)
|
||||
def packageTask(sources: PathFinder, jarPath: => Path, options: => Seq[PackageOption]): Task =
|
||||
fileTask("package", jarPath from sources)
|
||||
{
|
||||
import wrap.{MutableMapWrapper,Wrappers}
|
||||
/** Copies the mappings in a2 to a1, mutating a1. */
|
||||
def mergeAttributes(a1: Attributes, a2: Attributes)
|
||||
{
|
||||
for( (key, value) <- Wrappers.toList(a2))
|
||||
a1.put(key, value)
|
||||
}
|
||||
|
||||
val manifest = new Manifest
|
||||
var recursive = false
|
||||
for(option <- options)
|
||||
{
|
||||
option match
|
||||
{
|
||||
case JarManifest(mergeManifest) =>
|
||||
{
|
||||
mergeAttributes(manifest.getMainAttributes, mergeManifest.getMainAttributes)
|
||||
val entryMap = new MutableMapWrapper(manifest.getEntries)
|
||||
for((key, value) <- Wrappers.toList(mergeManifest.getEntries))
|
||||
{
|
||||
entryMap.get(key) match
|
||||
{
|
||||
case Some(attributes) => mergeAttributes(attributes, value)
|
||||
case None => entryMap += (key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
case Recursive => recursive = true
|
||||
case MainClass(mainClassName) =>
|
||||
manifest.getMainAttributes.put(Attributes.Name.MAIN_CLASS, mainClassName)
|
||||
case ManifestAttributes(attributes @ _*) =>
|
||||
val main = manifest.getMainAttributes
|
||||
for( (name, value) <- attributes)
|
||||
main.put(name, value)
|
||||
case _ => log.warn("Ignored unknown package option " + option)
|
||||
}
|
||||
}
|
||||
val jarPathLocal = jarPath
|
||||
FileUtilities.clean(jarPathLocal :: Nil, log) orElse
|
||||
FileUtilities.jar(sources.get, jarPathLocal, manifest, recursive, log)
|
||||
}
|
||||
def zipTask(sources: PathFinder, outputDirectory: Path, zipName: => String): Task =
|
||||
zipTask(sources, outputDirectory / zipName)
|
||||
def zipTask(sources: PathFinder, zipPath: => Path): Task =
|
||||
fileTask("zip", zipPath from sources) { FileUtilities.zip(sources.get, zipPath, false, log) }
|
||||
def incrementVersionNumber()
|
||||
{
|
||||
projectVersion.get match
|
||||
{
|
||||
case Some(v: BasicVersion) =>
|
||||
{
|
||||
val newVersion = incrementImpl(v)
|
||||
log.info("Changing version to " + newVersion)
|
||||
projectVersion() = newVersion
|
||||
}
|
||||
case a => ()
|
||||
}
|
||||
}
|
||||
protected def incrementImpl(v: BasicVersion): Version = v.incrementMicro
|
||||
protected def testTasks(frameworks: Iterable[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, options: => Seq[TestOption]) = {
|
||||
import scala.collection.mutable.HashSet
|
||||
|
||||
val testFilters = new ListBuffer[String => Boolean]
|
||||
val excludeTestsSet = new HashSet[String]
|
||||
val setup, cleanup = new ListBuffer[() => Option[String]]
|
||||
val testListeners = new ListBuffer[TestReportListener]
|
||||
|
||||
options.foreach {
|
||||
case TestFilter(include) => testFilters += include
|
||||
case ExcludeTests(exclude) => excludeTestsSet ++= exclude
|
||||
case TestListeners(listeners) => testListeners ++= listeners
|
||||
case TestSetup(setupFunction) => setup += setupFunction
|
||||
case TestCleanup(cleanupFunction) => cleanup += cleanupFunction
|
||||
}
|
||||
|
||||
if(excludeTestsSet.size > 0 && log.atLevel(Level.Debug))
|
||||
{
|
||||
log.debug("Excluding tests: ")
|
||||
excludeTestsSet.foreach(test => log.debug("\t" + test))
|
||||
}
|
||||
def includeTest(test: TestDefinition) = !excludeTestsSet.contains(test.testClassName) && testFilters.forall(filter => filter(test.testClassName))
|
||||
val tests = HashSet.empty[TestDefinition] ++ analysis.allTests.filter(includeTest)
|
||||
TestFramework.testTasks(frameworks, classpath.get, tests, log, testListeners.readOnly, false, setup.readOnly, cleanup.readOnly)
|
||||
}
|
||||
private def flatten[T](i: Iterable[Iterable[T]]) = i.flatMap(x => x)
|
||||
|
||||
protected def testQuickMethod(testAnalysis: CompileAnalysis, options: => Seq[TestOption])(toRun: Seq[TestOption] => Task) =
|
||||
task { tests =>
|
||||
val (exactFilters, testFilters) = tests.toList.map(GlobFilter.apply).partition(_.isInstanceOf[ExactFilter])
|
||||
val includeTests = exactFilters.map(_.asInstanceOf[ExactFilter].matchName)
|
||||
val toCheck = scala.collection.mutable.HashSet(includeTests: _*)
|
||||
toCheck --= testAnalysis.allTests.map(_.testClassName)
|
||||
if(!toCheck.isEmpty && log.atLevel(Level.Warn))
|
||||
{
|
||||
log.warn("Test(s) not found:")
|
||||
toCheck.foreach(test => log.warn("\t" + test))
|
||||
}
|
||||
val includeTestsSet = scala.collection.mutable.HashSet(includeTests: _*)
|
||||
val newOptions =
|
||||
if(includeTests.isEmpty && testFilters.isEmpty) options
|
||||
else TestFilter(test => includeTestsSet.contains(test) || testFilters.exists(_.accept(test))) :: options.toList
|
||||
toRun(newOptions)
|
||||
} completeWith testAnalysis.allTests.map(_.testClassName).toList
|
||||
|
||||
protected final def maximumErrors[T <: ActionOption](options: Seq[T]) =
|
||||
(for( MaxCompileErrors(maxErrors) <- options) yield maxErrors).firstOption.getOrElse(DefaultMaximumCompileErrors)
|
||||
}
|
||||
trait WebScalaProject extends ScalaProject
|
||||
{
|
||||
@deprecated protected def prepareWebappTask(webappContents: PathFinder, warPath: => Path, classpath: PathFinder, extraJars: => Iterable[File]): Task =
|
||||
prepareWebappTask(webappContents, warPath, classpath, Path.lazyPathFinder(extraJars.map(Path.fromFile)))
|
||||
protected def prepareWebappTask(webappContents: PathFinder, warPath: => Path, classpath: PathFinder, extraJars: PathFinder): Task =
|
||||
task
|
||||
{
|
||||
val webInfPath = warPath / "WEB-INF"
|
||||
val webLibDirectory = webInfPath / "lib"
|
||||
val classesTargetDirectory = webInfPath / "classes"
|
||||
|
||||
val (libs, directories) = classpath.get.toList.partition(ClasspathUtilities.isArchive)
|
||||
val classesAndResources = descendents(Path.lazyPathFinder(directories) ##, "*")
|
||||
if(log.atLevel(Level.Debug))
|
||||
directories.foreach(d => log.debug(" Copying the contents of directory " + d + " to " + classesTargetDirectory))
|
||||
|
||||
import FileUtilities.{copy, copyFlat, copyFilesFlat, clean}
|
||||
(copy(webappContents.get, warPath, log).right flatMap { copiedWebapp =>
|
||||
copy(classesAndResources.get, classesTargetDirectory, log).right flatMap { copiedClasses =>
|
||||
copyFlat(libs, webLibDirectory, log).right flatMap { copiedLibs =>
|
||||
copyFilesFlat(extraJars.get.map(_.asFile), webLibDirectory, log).right flatMap { copiedExtraLibs =>
|
||||
{
|
||||
val toRemove = scala.collection.mutable.HashSet((warPath ** "*").get.toSeq : _*)
|
||||
toRemove --= copiedWebapp
|
||||
toRemove --= copiedClasses
|
||||
toRemove --= copiedLibs
|
||||
toRemove --= copiedExtraLibs
|
||||
val (directories, files) = toRemove.toList.partition(_.isDirectory)
|
||||
if(log.atLevel(Level.Debug))
|
||||
files.foreach(r => log.debug("Pruning file " + r))
|
||||
val result =
|
||||
clean(files, true, log) orElse
|
||||
{
|
||||
val emptyDirectories = directories.filter(directory => directory.asFile.listFiles.isEmpty)
|
||||
if(log.atLevel(Level.Debug))
|
||||
emptyDirectories.foreach(r => log.debug("Pruning directory " + r))
|
||||
clean(emptyDirectories, true, log)
|
||||
}
|
||||
result.toLeft(())
|
||||
}
|
||||
}}}}).left.toOption
|
||||
}
|
||||
def jettyRunTask(warPath: => Path, defaultContextPath: => String, port: Int, classpath: PathFinder, classpathName: String, scanDirectories: Seq[File], scanInterval: Int): Task =
|
||||
task { JettyRun(classpath.get, classpathName, warPath, defaultContextPath, port, scanDirectories, scanInterval, log) }
|
||||
def jettyRunTask(warPath: => Path, defaultContextPath: => String, classpath: PathFinder, classpathName: String, scanDirectories: Seq[File], scanInterval: Int): Task =
|
||||
jettyRunTask(warPath, defaultContextPath, JettyRun.DefaultPort, classpath, classpathName, scanDirectories, scanInterval)
|
||||
def jettyRunTask(warPath: => Path, defaultContextPath: => String, classpath: PathFinder, classpathName: String,
|
||||
jettyConfigurationXML: scala.xml.NodeSeq, jettyConfigurationFiles: Seq[File]): Task =
|
||||
task { JettyRun(classpath.get, classpathName, warPath, defaultContextPath, jettyConfigurationXML, jettyConfigurationFiles, log) }
|
||||
def jettyStopTask = task { JettyRun.stop(); None }
|
||||
}
|
||||
object ScalaProject
|
||||
{
|
||||
val DefaultMaximumCompileErrors = 100
|
||||
val AnalysisDirectoryName = "analysis"
|
||||
val MainClassKey = "Main-Class"
|
||||
val TestResourcesProperty = "sbt.test.resources"
|
||||
def optionsAsString(options: Seq[ScalaProject#CompileOption]) = options.map(_.asString).filter(!_.isEmpty)
|
||||
def javaOptionsAsString(options: Seq[ScalaProject#JavaCompileOption]) = options.map(_.asString)
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
/** Provides access to the current version of Scala being used to build a project. These methods typically
|
||||
* return None or the empty string when the loader is not used. */
|
||||
object ScalaVersion
|
||||
{
|
||||
/** The name of the system property containing the Scala version used for this project.*/
|
||||
private[sbt] val LiveKey = "sbt.scala.version"
|
||||
private[sbt] def crossString(v: String) = "scala_" + v
|
||||
/** Returns the current version of Scala being used to build the project, unless the sbt loader is not being used,
|
||||
* in which case this is the empty string.*/
|
||||
def currentString =
|
||||
{
|
||||
val v = System.getProperty(LiveKey)
|
||||
if(v == null)
|
||||
""
|
||||
else
|
||||
v.trim
|
||||
}
|
||||
/** Returns the current version of Scala being used to build the project. If the sbt loader is not being
|
||||
* used, this returns None. Otherwise, the value returned by this method is fixed for the duration of
|
||||
* a Project's existence. It only changes on reboot (during which a Project is recreated).*/
|
||||
val current: Option[String] =
|
||||
{
|
||||
val sv = currentString
|
||||
if(sv.isEmpty)
|
||||
None
|
||||
else
|
||||
Some(sv)
|
||||
}
|
||||
private[sbt] def withCross[T](crossDisabled: Boolean)(withVersion: String => T, disabled: => T): T =
|
||||
{
|
||||
if(crossDisabled)
|
||||
disabled
|
||||
else
|
||||
{
|
||||
current match
|
||||
{
|
||||
case Some(scalaV) => withVersion(scalaV)
|
||||
case _ => disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mikko Peltonen, Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
object SourceModificationWatch
|
||||
{
|
||||
def watchUntil(project: Project, pollDelaySec: Int)(terminationCondition: => Boolean)(onSourcesModified: => Unit)
|
||||
{
|
||||
def sourceFiles: Iterable[java.io.File] =
|
||||
sourcesFinder.get.map(Path.relativize(project.info.projectPath, _)).filter(_.isDefined).map(_.get.asFile)
|
||||
def sourcesFinder: PathFinder = (Path.emptyPathFinder /: project.topologicalSort)(_ +++ _.watchPaths)
|
||||
def loop(lastCallbackCallTime: Long, previousFileCount: Int)
|
||||
{
|
||||
val (lastModifiedTime, fileCount) = sourceFiles.foldLeft((0L, 0)){(acc, file) => (Math.max(acc._1, file.lastModified), acc._2 + 1)}
|
||||
val newCallbackCallTime =
|
||||
// check if sources are modified
|
||||
if (lastModifiedTime > lastCallbackCallTime || previousFileCount != fileCount)
|
||||
{
|
||||
val now = System.currentTimeMillis
|
||||
onSourcesModified
|
||||
now
|
||||
}
|
||||
else
|
||||
lastCallbackCallTime
|
||||
Thread.sleep(pollDelaySec * 1000)
|
||||
if(!terminationCondition)
|
||||
loop(newCallbackCallTime, fileCount)
|
||||
}
|
||||
loop(0L, 0)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008 David MacIver, Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
trait Described extends NotNull
|
||||
{
|
||||
def description: Option[String]
|
||||
}
|
||||
trait TaskManager{
|
||||
type ManagerType >: this.type <: TaskManager
|
||||
type ManagedTask >: Task <: TaskManager#Task with Dag[ManagedTask]
|
||||
/** Creates a task that executes the given action when invoked.*/
|
||||
def task(action : => Option[String]) = new Task(None, Nil, false, action)
|
||||
/** An interactive task is one that is not executed across all dependent projects when
|
||||
* it is called directly. The dependencies of the task are still invoked across all dependent
|
||||
* projects, however. */
|
||||
def interactiveTask(action: => Option[String]) = new Task(None, Nil, true, action)
|
||||
/** Creates a method task that executes the given action when invoked. */
|
||||
def task(action: Array[String] => ManagedTask) = new MethodTask(None, action, Nil)
|
||||
|
||||
def taskName(t: Task): String
|
||||
|
||||
/** A method task is an action that has parameters. Note that it is not a Task, though,
|
||||
* because it requires arguments to perform its work. It therefore cannot be a dependency of
|
||||
* a Task..*/
|
||||
final class MethodTask(val description: Option[String], action: Array[String] => ManagedTask, getCompletions: => Seq[String]) extends Described
|
||||
{
|
||||
/** Creates a new method task, identical to this method task, except with thE[String]e given description.*/
|
||||
def describedAs(description : String) = new MethodTask(Some(description), action, getCompletions)
|
||||
/** Invokes this method task with the given arguments.*/
|
||||
def apply(arguments: Array[String]) = action(arguments)
|
||||
def manager: ManagerType = TaskManager.this
|
||||
def completeWith(add: => Seq[String]) = new MethodTask(description, action, add)
|
||||
def completions = getCompletions
|
||||
}
|
||||
|
||||
sealed class Task(val explicitName: Option[String], val description : Option[String], val dependencies : List[ManagedTask],
|
||||
val interactive: Boolean, action : => Option[String]) extends Dag[ManagedTask] with Described
|
||||
{
|
||||
def this(description : Option[String], dependencies : List[ManagedTask], interactive: Boolean, action : => Option[String]) =
|
||||
this(None, description, dependencies, interactive, action)
|
||||
checkTaskDependencies(dependencies)
|
||||
def manager: ManagerType = TaskManager.this
|
||||
def name = explicitName.getOrElse(taskName(this))
|
||||
def named(name: String) = construct(Some(name), description,dependencies, interactive, action)
|
||||
override def toString = "Task " + name
|
||||
|
||||
/** Creates a new task, identical to this task, except with the additional dependencies specified.*/
|
||||
def dependsOn(tasks : ManagedTask*) = setDependencies(tasks.toList ::: dependencies)
|
||||
private[sbt] def setDependencies(dependencyList: List[ManagedTask]) =
|
||||
{
|
||||
checkTaskDependencies(dependencyList)
|
||||
construct(explicitName, description, dependencyList, interactive, action)
|
||||
}
|
||||
/** Creates a new task, identical to this task, except with the given description.*/
|
||||
def describedAs(description : String) = construct(explicitName, Some(description), dependencies, interactive, action);
|
||||
private[sbt] def invoke = action;
|
||||
|
||||
final def setInteractive = construct(explicitName, description, dependencies, true, action)
|
||||
final def run = runSequentially(topologicalSort)
|
||||
final def runDependenciesOnly = runSequentially(topologicalSort.dropRight(1))
|
||||
private def runSequentially(tasks: List[ManagedTask]) = Control.lazyFold(tasks)(_.invoke)
|
||||
|
||||
def &&(that : Task) =
|
||||
construct(explicitName, None, dependencies ::: that.dependencies, interactive || that.interactive, this.invoke.orElse(that.invoke))
|
||||
|
||||
protected def construct(explicitName: Option[String], description: Option[String], dependencies: List[ManagedTask], interactive: Boolean,
|
||||
action : => Option[String]): Task = new Task(explicitName, description, dependencies, interactive, action)
|
||||
}
|
||||
final class CompoundTask private (explicitName: Option[String], description : Option[String], dependencies : List[ManagedTask], interactive: Boolean,
|
||||
action : => Option[String], createWork: => SubWork[Project#Task]) extends Task(description, dependencies, interactive, action)
|
||||
with CompoundWork[Project#Task]
|
||||
{
|
||||
def this(createWork: => SubWork[Project#Task]) = this(None, None, Nil, false, None, createWork)
|
||||
override protected def construct(explicitName: Option[String], description: Option[String], dependencies: List[ManagedTask],
|
||||
interactive: Boolean, action : => Option[String]) = new CompoundTask(explicitName, description, dependencies, interactive, action, createWork)
|
||||
def work = createWork
|
||||
}
|
||||
|
||||
private def checkTaskDependencies(dependencyList: List[ManagedTask])
|
||||
{
|
||||
val nullDependencyIndex = dependencyList.findIndexOf(_ == null)
|
||||
require(nullDependencyIndex < 0, "Dependency (at index " + nullDependencyIndex + ") is null. This may be an initialization issue or a circular dependency.")
|
||||
val interactiveDependencyIndex = dependencyList.findIndexOf(_.interactive)
|
||||
require(interactiveDependencyIndex < 0, "Dependency (at index " + interactiveDependencyIndex + ") is interactive. Interactive tasks cannot be dependencies.")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Steven Blundy, Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
object Result extends Enumeration
|
||||
{
|
||||
val Error, Passed, Failed = Value
|
||||
}
|
||||
object ClassType extends Enumeration
|
||||
{
|
||||
val Module, Class = Value
|
||||
}
|
||||
|
||||
trait TestFramework extends NotNull
|
||||
{
|
||||
def name: String
|
||||
def testSuperClassName: String
|
||||
def testSubClassType: ClassType.Value
|
||||
|
||||
def testRunner(classLoader: ClassLoader, listeners: Iterable[TestReportListener], log: Logger): TestRunner
|
||||
}
|
||||
trait TestRunner extends NotNull
|
||||
{
|
||||
def run(testClassName: String): Result.Value
|
||||
}
|
||||
|
||||
abstract class BasicTestRunner extends TestRunner
|
||||
{
|
||||
protected def log: Logger
|
||||
protected def listeners: Seq[TestReportListener]
|
||||
|
||||
final def run(testClass: String): Result.Value =
|
||||
{
|
||||
safeListenersCall(_.startGroup(testClass))
|
||||
try
|
||||
{
|
||||
val result = runTest(testClass)
|
||||
safeListenersCall(_.endGroup(testClass, result))
|
||||
result
|
||||
}
|
||||
catch
|
||||
{
|
||||
case e =>
|
||||
{
|
||||
safeListenersCall(_.endGroup(testClass, e))
|
||||
Result.Error
|
||||
}
|
||||
}
|
||||
}
|
||||
def runTest(testClass: String): Result.Value
|
||||
|
||||
protected def fire(event: TestEvent) = safeListenersCall(_.testEvent(event))
|
||||
protected def safeListenersCall(call: (TestReportListener) => Unit) = TestFramework.safeForeach(listeners, log)(call)
|
||||
}
|
||||
|
||||
final class NamedTestTask(val name: String, action: => Option[String]) extends NotNull { def run() = action }
|
||||
object TestFramework
|
||||
{
|
||||
def runTests(frameworks: Iterable[TestFramework], classpath: Iterable[Path], tests: Iterable[TestDefinition], log: Logger,
|
||||
listeners: Iterable[TestReportListener]) =
|
||||
{
|
||||
val (start, runTests, end) = testTasks(frameworks, classpath, tests, log, listeners, true, Nil, Nil)
|
||||
def run(tasks: Iterable[NamedTestTask]) = tasks.foreach(_.run())
|
||||
run(start)
|
||||
run(runTests)
|
||||
run(end)
|
||||
}
|
||||
|
||||
private val ScalaCompilerJarPackages = "scala.tools.nsc." :: "jline." :: "ch.epfl.lamp." :: Nil
|
||||
|
||||
private val TestStartName = "test-start"
|
||||
private val TestFinishName = "test-finish"
|
||||
|
||||
private[sbt] def safeForeach[T](it: Iterable[T], log: Logger)(f: T => Unit): Unit = it.foreach(i => Control.trapAndLog(log){ f(i) } )
|
||||
import scala.collection.{Map, Set}
|
||||
def testTasks(frameworks: Iterable[TestFramework], classpath: Iterable[Path], tests: Iterable[TestDefinition], log: Logger,
|
||||
listeners: Iterable[TestReportListener], endErrorsEnabled: Boolean, setup: Iterable[() => Option[String]],
|
||||
cleanup: Iterable[() => Option[String]]): (Iterable[NamedTestTask], Iterable[NamedTestTask], Iterable[NamedTestTask]) =
|
||||
{
|
||||
val mappedTests = testMap(frameworks, tests)
|
||||
if(mappedTests.isEmpty)
|
||||
(new NamedTestTask(TestStartName, None) :: Nil, Nil, new NamedTestTask(TestFinishName, { log.info("No tests to run."); None }) :: Nil )
|
||||
else
|
||||
createTestTasks(classpath, mappedTests, log, listeners, endErrorsEnabled, setup, cleanup)
|
||||
}
|
||||
private def testMap(frameworks: Iterable[TestFramework], tests: Iterable[TestDefinition]): Map[TestFramework, Set[String]] =
|
||||
{
|
||||
import scala.collection.mutable.{HashMap, HashSet, Set}
|
||||
val map = new HashMap[TestFramework, Set[String]]
|
||||
if(!frameworks.isEmpty)
|
||||
{
|
||||
for(test <- tests)
|
||||
{
|
||||
def isTestForFramework(framework: TestFramework) =
|
||||
(framework.testSubClassType == ClassType.Module) == test.isModule &&
|
||||
framework.testSuperClassName == test.superClassName
|
||||
|
||||
for(framework <- frameworks.find(isTestForFramework))
|
||||
map.getOrElseUpdate(framework, new HashSet[String]) += test.testClassName
|
||||
}
|
||||
}
|
||||
wrap.Wrappers.readOnly(map)
|
||||
}
|
||||
private def createTasks(work: Iterable[() => Option[String]], baseName: String) =
|
||||
work.toList.zipWithIndex.map{ case (work, index) => new NamedTestTask(baseName + " " + (index+1), work()) }
|
||||
|
||||
private def createTestTasks(classpath: Iterable[Path], tests: Map[TestFramework, Set[String]], log: Logger,
|
||||
listeners: Iterable[TestReportListener], endErrorsEnabled: Boolean, setup: Iterable[() => Option[String]],
|
||||
cleanup: Iterable[() => Option[String]]) =
|
||||
{
|
||||
val filterCompilerLoader = new FilteredLoader(getClass.getClassLoader, ScalaCompilerJarPackages)
|
||||
val loader: ClassLoader = new IntermediateLoader(classpath.map(_.asURL).toSeq.toArray, filterCompilerLoader)
|
||||
val testsListeners = listeners.filter(_.isInstanceOf[TestsListener]).map(_.asInstanceOf[TestsListener])
|
||||
def foreachListenerSafe(f: TestsListener => Unit): Unit = safeForeach(testsListeners, log)(f)
|
||||
|
||||
import Result.{Error,Passed,Failed}
|
||||
object result
|
||||
{
|
||||
private[this] var value: Result.Value = Passed
|
||||
def apply() = synchronized { value }
|
||||
def update(v: Result.Value): Unit = synchronized { if(value != Error) value = v }
|
||||
}
|
||||
val startTask = new NamedTestTask(TestStartName, {foreachListenerSafe(_.doInit); None}) :: createTasks(setup, "Test setup")
|
||||
val testTasks =
|
||||
tests flatMap { case (framework, testClassNames) =>
|
||||
|
||||
val runner = framework.testRunner(loader, listeners, log)
|
||||
for(testClassName <- testClassNames) yield
|
||||
{
|
||||
def runTest() =
|
||||
{
|
||||
val oldLoader = Thread.currentThread.getContextClassLoader
|
||||
Thread.currentThread.setContextClassLoader(loader)
|
||||
try {
|
||||
runner.run(testClassName) match
|
||||
{
|
||||
case Error => result() = Error; Some("ERROR occurred during testing.")
|
||||
case Failed => result() = Failed; Some("Test FAILED")
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Thread.currentThread.setContextClassLoader(oldLoader)
|
||||
}
|
||||
}
|
||||
new NamedTestTask(testClassName, runTest())
|
||||
}
|
||||
}
|
||||
def end() =
|
||||
{
|
||||
foreachListenerSafe(_.doComplete(result()))
|
||||
result() match
|
||||
{
|
||||
case Error => if(endErrorsEnabled) Some("ERROR occurred during testing.") else None
|
||||
case Failed => if(endErrorsEnabled) Some("One or more tests FAILED.") else None
|
||||
case Passed =>
|
||||
{
|
||||
log.info(" ")
|
||||
log.info("All tests PASSED.")
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
val endTask = new NamedTestTask(TestFinishName, end() ) :: createTasks(cleanup, "Test cleanup")
|
||||
(startTask, testTasks, endTask)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class LazyTestFramework extends TestFramework
|
||||
{
|
||||
/** The class name of the the test runner that executes
|
||||
* tests for this framework.*/
|
||||
protected def testRunnerClassName: String
|
||||
|
||||
/** Creates an instance of the runner given by 'testRunnerClassName'.*/
|
||||
final def testRunner(projectLoader: ClassLoader, listeners: Iterable[TestReportListener], log: Logger): TestRunner =
|
||||
{
|
||||
val runnerClassName = testRunnerClassName
|
||||
val frameworkClasspath = FileUtilities.classLocation(getClass)
|
||||
val sbtURL = FileUtilities.sbtJar.toURI.toURL
|
||||
val lazyLoader = new LazyFrameworkLoader(runnerClassName, Array(frameworkClasspath, sbtURL), projectLoader, getClass.getClassLoader)
|
||||
val runnerClass = Class.forName(runnerClassName, true, lazyLoader).asSubclass(classOf[TestRunner])
|
||||
|
||||
runnerClass.getConstructor(classOf[Logger], classOf[Seq[TestReportListener]], classOf[ClassLoader]).newInstance(log, listeners, projectLoader)
|
||||
}
|
||||
}
|
||||
|
||||
/** The test framework definition for ScalaTest.*/
|
||||
object ScalaTestFramework extends LazyTestFramework
|
||||
{
|
||||
val name = "ScalaTest"
|
||||
val SuiteClassName = "org.scalatest.Suite"
|
||||
|
||||
def testSuperClassName = SuiteClassName
|
||||
def testSubClassType = ClassType.Class
|
||||
|
||||
def testRunnerClassName = "sbt.impl.ScalaTestRunner"
|
||||
}
|
||||
/** The test framework definition for ScalaCheck.*/
|
||||
object ScalaCheckFramework extends LazyTestFramework
|
||||
{
|
||||
val name = "ScalaCheck"
|
||||
val PropertiesClassName = "org.scalacheck.Properties"
|
||||
|
||||
def testSuperClassName = PropertiesClassName
|
||||
def testSubClassType = ClassType.Module
|
||||
|
||||
def testRunnerClassName = "sbt.impl.ScalaCheckRunner"
|
||||
}
|
||||
/** The test framework definition for specs.*/
|
||||
object SpecsFramework extends LazyTestFramework
|
||||
{
|
||||
val name = "specs"
|
||||
val SpecificationClassName = "org.specs.Specification"
|
||||
|
||||
def testSuperClassName = SpecificationClassName
|
||||
def testSubClassType = ClassType.Module
|
||||
|
||||
def testRunnerClassName = "sbt.impl.SpecsRunner"
|
||||
}
|
||||
|
|
@ -0,0 +1,316 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Steven Blundy, Mark Harrah
|
||||
*/
|
||||
|
||||
package sbt
|
||||
|
||||
trait TestReportListener
|
||||
{
|
||||
/** called for each class or equivalent grouping */
|
||||
def startGroup(name: String)
|
||||
/** called for each test method or equivalent */
|
||||
def testEvent(event: TestEvent)
|
||||
/** called if there was an error during test */
|
||||
def endGroup(name: String, t: Throwable)
|
||||
/** called if test completed */
|
||||
def endGroup(name: String, result: Result.Value)
|
||||
}
|
||||
|
||||
trait TestsListener extends TestReportListener
|
||||
{
|
||||
/** called once, at beginning. */
|
||||
def doInit
|
||||
/** called once, at end. */
|
||||
def doComplete(finalResult: Result.Value)
|
||||
/** called once, at end, if the test framework throws an exception. */
|
||||
@deprecated def doComplete(t: Throwable)
|
||||
}
|
||||
|
||||
abstract class WriterReportListener(val log: Logger) extends TestsListener
|
||||
{
|
||||
import java.io.{IOException, PrintWriter, Writer}
|
||||
import scala.collection.mutable.{Buffer, ListBuffer}
|
||||
|
||||
protected case class Summary(count: Int, failures: Int, errors: Int, skipped: Int, message: Option[String], cause: Option[Throwable]) extends NotNull
|
||||
private var out: Option[PrintWriter] = None
|
||||
private var groupCount: Int = 0
|
||||
private var groupFailures: Int = 0
|
||||
private var groupErrors: Int = 0
|
||||
private var groupSkipped: Int = 0
|
||||
private var groupMessages: Seq[String] = Nil
|
||||
|
||||
protected val passedEventHandler: TestEvent => Summary = (event: TestEvent) => event match
|
||||
{
|
||||
case SpecificationReportEvent(successes, failures, errors, skipped, desc, systems, subSpecs) => Summary(successes, failures, errors, skipped, None, None)
|
||||
case IgnoredEvent(name, Some(message)) => Summary(1, 0, 0, 1, Some(message), None)
|
||||
case IgnoredEvent(name, None) => Summary(1, 0, 0, 1, None, None)
|
||||
case _ => Summary(1, 0, 0, 0, None, None)
|
||||
}
|
||||
protected val failedEventHandler: TestEvent => Summary = (event: TestEvent) => event match
|
||||
{
|
||||
case FailedEvent(name, msg) => Summary(1, 1, 0, 0, Some("! " + name + ": " + msg), None)
|
||||
case TypedErrorEvent(name, event, Some(msg), cause) => Summary(1, 1, 0, 0, Some(event + " - " + name + ": " + msg), cause)
|
||||
case TypedErrorEvent(name, event, None, cause) => Summary(1, 1, 0, 0, Some(event + " - " + name), cause)
|
||||
case ErrorEvent(msg) => Summary(1, 1, 0, 0, Some(msg), None)
|
||||
case SpecificationReportEvent(successes, failures, errors, skipped, desc, systems, subSpecs) => Summary(successes + failures + errors + skipped, failures, errors, skipped, Some(desc), None)
|
||||
case _ => {log.warn("Unrecognized failure: " + event); Summary(1, 1, 0, 0, None, None)}
|
||||
}
|
||||
protected val errorEventHandler: TestEvent => Summary = (event: TestEvent) => event match
|
||||
{
|
||||
case FailedEvent(name, msg) => Summary(1, 0, 1, 0, Some("! " + name + ": " + msg), None)
|
||||
case TypedErrorEvent(name, event, Some(msg), cause) => Summary(1, 0, 1, 0, Some(event + " - " + name + ": " + msg), cause)
|
||||
case TypedErrorEvent(name, event, None, cause) => Summary(1, 0, 1, 0, Some(event + " - " + name), cause)
|
||||
case ErrorEvent(msg) => Summary(1, 0, 1, 0, Some(msg), None)
|
||||
case SpecificationReportEvent(successes, failures, errors, skipped, desc, systems, subSpecs) => Summary(successes + failures + errors + skipped, failures, errors, skipped, Some(desc), None)
|
||||
case _ => {log.warn("Unrecognized error: " + event); Summary(1, 0, 1, 0, None, None)}
|
||||
}
|
||||
protected def open: Writer
|
||||
protected def close =
|
||||
{
|
||||
onOut(_.close())
|
||||
out = None
|
||||
}
|
||||
def doInit = Control.trapAndLog(log){ out = Some(new PrintWriter(open)) }
|
||||
def doComplete(finalResult: Result.Value) =
|
||||
{
|
||||
finalResult match
|
||||
{
|
||||
case Result.Error => println("Error during Tests")
|
||||
case Result.Passed => println("All Tests Passed")
|
||||
case Result.Failed => println("Tests Failed")
|
||||
}
|
||||
close
|
||||
}
|
||||
def doComplete(t: Throwable) =
|
||||
{
|
||||
println("Exception in Test Framework")
|
||||
onOut(t.printStackTrace(_))
|
||||
close
|
||||
}
|
||||
def startGroup(name: String) =
|
||||
{
|
||||
groupCount = 0
|
||||
groupFailures = 0
|
||||
groupErrors = 0
|
||||
groupSkipped = 0
|
||||
groupMessages = Nil
|
||||
}
|
||||
def testEvent(event: TestEvent) = event.result match
|
||||
{
|
||||
case Some(result) =>
|
||||
{
|
||||
val Summary(count, failures, errors, skipped, msg, cause) = result match
|
||||
{
|
||||
case Result.Passed => passedEventHandler(event)
|
||||
case Result.Failed => failedEventHandler(event)
|
||||
case Result.Error => errorEventHandler(event)
|
||||
}
|
||||
groupCount += count
|
||||
groupFailures += failures
|
||||
groupErrors += errors
|
||||
groupSkipped += skipped
|
||||
groupMessages ++= msg.toList
|
||||
}
|
||||
case None => {}
|
||||
}
|
||||
def endGroup(name: String, t: Throwable) =
|
||||
{
|
||||
groupMessages = Nil
|
||||
println("Exception in " + name)
|
||||
onOut(t.printStackTrace(_))
|
||||
}
|
||||
def endGroup(name: String, result: Result.Value) =
|
||||
{
|
||||
result match
|
||||
{
|
||||
case Result.Error => println("Error: " + name + " - Count " + groupCount + ", Failed " + groupFailures + ", Errors " + groupErrors)
|
||||
case Result.Passed => println("Passed: " + name + " - Count " + groupCount + ", Failed " + groupFailures + ", Errors " + groupErrors)
|
||||
case Result.Failed => println("Failed: " + name + " - Count " + groupCount + ", Failed " + groupFailures + ", Errors " + groupErrors)
|
||||
}
|
||||
if(!groupMessages.isEmpty)
|
||||
{
|
||||
groupMessages.foreach(println(_))
|
||||
groupMessages = Nil
|
||||
println("")
|
||||
}
|
||||
}
|
||||
protected def onOut(f: PrintWriter => Unit) = Control.trapAndLog(log){
|
||||
out match
|
||||
{
|
||||
case Some(pw) => f(pw)
|
||||
case None => log.warn("Method called when output was not open")
|
||||
}
|
||||
}
|
||||
protected def println(s: String) = onOut(_.println(s))
|
||||
}
|
||||
|
||||
class FileReportListener(val file: Path, log: Logger) extends WriterReportListener(log)
|
||||
{
|
||||
def open = new java.io.FileWriter(file.asFile)
|
||||
}
|
||||
|
||||
abstract class TestEvent extends NotNull
|
||||
{
|
||||
def result: Option[Result.Value]
|
||||
}
|
||||
|
||||
sealed abstract class ScalaCheckEvent extends TestEvent
|
||||
final case class PassedEvent(name: String, msg: String) extends ScalaCheckEvent { def result = Some(Result.Passed) }
|
||||
final case class FailedEvent(name: String, msg: String) extends ScalaCheckEvent { def result = Some(Result.Failed) }
|
||||
|
||||
sealed abstract class ScalaTestEvent(val result: Option[Result.Value]) extends TestEvent
|
||||
final case class TypedEvent(name: String, `type`: String, msg: Option[String])(result: Option[Result.Value]) extends ScalaTestEvent(result)
|
||||
final case class TypedErrorEvent(name: String, `type`: String, msg: Option[String], cause: Option[Throwable])(result: Option[Result.Value]) extends ScalaTestEvent(result)
|
||||
final case class MessageEvent(msg: String) extends ScalaTestEvent(None)
|
||||
final case class ErrorEvent(msg: String) extends ScalaTestEvent(None)
|
||||
final case class IgnoredEvent(name: String, msg: Option[String]) extends ScalaTestEvent(Some(Result.Passed))
|
||||
|
||||
sealed abstract class SpecsEvent extends TestEvent
|
||||
final case class SpecificationReportEvent(successes: Int, failures: Int, errors: Int, skipped: Int, pretty: String, systems: Seq[SystemReportEvent], subSpecs: Seq[SpecificationReportEvent]) extends SpecsEvent
|
||||
{
|
||||
def result = if(errors > 0) Some(Result.Error) else if(failures > 0) Some(Result.Failed) else Some(Result.Passed)
|
||||
}
|
||||
final case class SystemReportEvent(description: String, verb: String, skippedSus:Option[Throwable], literateDescription: Option[Seq[String]], examples: Seq[ExampleReportEvent]) extends SpecsEvent { def result = None }
|
||||
final case class ExampleReportEvent(description: String, errors: Seq[Throwable], failures: Seq[RuntimeException], skipped: Seq[RuntimeException], subExamples: Seq[ExampleReportEvent]) extends SpecsEvent { def result = None }
|
||||
|
||||
trait EventOutput[E <: TestEvent]
|
||||
{
|
||||
def output(e: E): Unit
|
||||
}
|
||||
|
||||
sealed abstract class LazyEventOutput[E <: TestEvent](val log: Logger) extends EventOutput[E]
|
||||
|
||||
class ScalaCheckOutput(log: Logger) extends LazyEventOutput[ScalaCheckEvent](log)
|
||||
{
|
||||
def output(event: ScalaCheckEvent) = event match
|
||||
{
|
||||
case PassedEvent(name, msg) => log.info("+ " + name + ": " + msg)
|
||||
case FailedEvent(name, msg) => log.error("! " + name + ": " + msg)
|
||||
}
|
||||
}
|
||||
|
||||
class ScalaTestOutput(log: Logger) extends LazyEventOutput[ScalaTestEvent](log)
|
||||
{
|
||||
def output(event: ScalaTestEvent) = event match
|
||||
{
|
||||
case TypedEvent(name, event, Some(msg)) => log.info(event + " - " + name + ": " + msg)
|
||||
case TypedEvent(name, event, None) => log.info(event + " - " + name)
|
||||
case TypedErrorEvent(name, event, Some(msg), cause) => logError(event + " - " + name + ": " + msg, cause)
|
||||
case TypedErrorEvent(name, event, None, cause) => logError(event + " - " + name, cause)
|
||||
case MessageEvent(msg) => log.info(msg)
|
||||
case ErrorEvent(msg) => logError(msg, None)
|
||||
case IgnoredEvent(name, Some(msg)) => log.info("Test ignored - " + name + ": " + msg)
|
||||
case IgnoredEvent(name, None) => log.info("Test ignored - " + name)
|
||||
}
|
||||
private def logError(message: String, cause: Option[Throwable])
|
||||
{
|
||||
cause.foreach(x => log.trace(x))
|
||||
log.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
class SpecsOutput(val log: Logger) extends EventOutput[SpecsEvent]
|
||||
{
|
||||
private val Indent = " "
|
||||
|
||||
def output(event: SpecsEvent) = event match
|
||||
{
|
||||
case sre: SpecificationReportEvent => reportSpecification(sre, "")
|
||||
case sre: SystemReportEvent => reportSystem(sre, "")
|
||||
case ere: ExampleReportEvent => reportExample(ere, "")
|
||||
}
|
||||
|
||||
/* The following is closely based on org.specs.runner.OutputReporter,
|
||||
* part of specs, which is Copyright 2007-2008 Eric Torreborre.
|
||||
* */
|
||||
|
||||
private def reportSpecification(specification: SpecificationReportEvent, padding: String)
|
||||
{
|
||||
val newIndent = padding + Indent
|
||||
reportSpecifications(specification.subSpecs, newIndent)
|
||||
reportSystems(specification.systems, newIndent)
|
||||
}
|
||||
private def reportSpecifications(specifications: Iterable[SpecificationReportEvent], padding: String)
|
||||
{
|
||||
for(specification <- specifications)
|
||||
reportSpecification(specification, padding)
|
||||
}
|
||||
private def reportSystems(systems: Iterable[SystemReportEvent], padding: String)
|
||||
{
|
||||
for(system <- systems)
|
||||
reportSystem(system, padding)
|
||||
}
|
||||
private def reportSystem(sus: SystemReportEvent, padding: String)
|
||||
{
|
||||
log.info(padding + sus.description + " " + sus.verb + sus.skippedSus.map(" (skipped: " + _.getMessage + ")").getOrElse(""))
|
||||
for(description <- sus.literateDescription)
|
||||
log.info(padding + description.mkString)
|
||||
reportExamples(sus.examples, padding)
|
||||
log.info(" ")
|
||||
}
|
||||
private def reportExamples(examples: Iterable[ExampleReportEvent], padding: String)
|
||||
{
|
||||
for(example <- examples)
|
||||
{
|
||||
reportExample(example, padding)
|
||||
reportExamples(example.subExamples, padding + Indent)
|
||||
}
|
||||
}
|
||||
private def status(example: ExampleReportEvent) =
|
||||
{
|
||||
if (example.errors.size + example.failures.size > 0)
|
||||
"x "
|
||||
else if (example.skipped.size > 0)
|
||||
"o "
|
||||
else
|
||||
"+ "
|
||||
}
|
||||
private def reportExample(example: ExampleReportEvent, padding: String)
|
||||
{
|
||||
log.info(padding + status(example) + example.description)
|
||||
for(skip <- example.skipped)
|
||||
{
|
||||
log.trace(skip)
|
||||
log.warn(padding + skip.toString)
|
||||
}
|
||||
for(e <- example.failures ++ example.errors)
|
||||
{
|
||||
log.trace(e)
|
||||
log.error(padding + e.toString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LogTestReportListener(val log: Logger) extends TestReportListener
|
||||
{
|
||||
lazy val scalaCheckOutput: EventOutput[ScalaCheckEvent] = createScalaCheckOutput
|
||||
lazy val scalaTestOutput: EventOutput[ScalaTestEvent] = createScalaTestOutput
|
||||
lazy val specsOutput: EventOutput[SpecsEvent] = createSpecsOutput
|
||||
|
||||
protected def createScalaCheckOutput = new ScalaCheckOutput(log)
|
||||
protected def createScalaTestOutput = new ScalaTestOutput(log)
|
||||
protected def createSpecsOutput = new SpecsOutput(log)
|
||||
|
||||
def startGroup(name: String) {}
|
||||
def testEvent(event: TestEvent)
|
||||
{
|
||||
log.debug("in testEvent:" + event)
|
||||
event match
|
||||
{
|
||||
case sce: ScalaCheckEvent => scalaCheckOutput.output(sce)
|
||||
case ste: ScalaTestEvent => scalaTestOutput.output(ste)
|
||||
case se: SpecsEvent => specsOutput.output(se)
|
||||
case e => handleOtherTestEvent(e)
|
||||
}
|
||||
}
|
||||
protected def handleOtherTestEvent(event: TestEvent) {}
|
||||
def endGroup(name: String, t: Throwable)
|
||||
{
|
||||
log.error("Could not run test " + name + ": " + t.toString)
|
||||
log.trace(t)
|
||||
}
|
||||
def endGroup(name: String, result: Result.Value)
|
||||
{
|
||||
log.debug("in endGroup:" + result)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008 Mark Harrah
|
||||
*
|
||||
* Partially based on exit trapping in Nailgun by Pete Kirkham,
|
||||
* copyright 2004, Martian Software, Inc
|
||||
* licensed under Apache 2.0 License.
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import scala.collection.Set
|
||||
import scala.reflect.Manifest
|
||||
|
||||
/** This provides functionality to catch System.exit calls to prevent the JVM from terminating.
|
||||
* This is useful for executing user code that may call System.exit, but actually exiting is
|
||||
* undesirable. This file handles the call to exit by disposing all top-level windows and interrupting
|
||||
* all user started threads. It does not stop the threads and does not call shutdown hooks. It is
|
||||
* therefore inappropriate to use this with code that requires shutdown hooks or creates threads that
|
||||
* do not terminate. This category of code should only be called by forking the JVM. */
|
||||
object TrapExit
|
||||
{
|
||||
/** Executes the given thunk in a context where System.exit(code) throws
|
||||
* a custom SecurityException, which is then caught and the exit code returned.
|
||||
* Otherwise, 0 is returned. No other exceptions are handled by this method.*/
|
||||
def apply(execute: => Unit, log: Logger): Int =
|
||||
{
|
||||
log.debug("Starting sandboxed run...")
|
||||
|
||||
/** Take a snapshot of the threads that existed before execution in order to determine
|
||||
* the threads that were created by 'execute'.*/
|
||||
val originalThreads = allThreads
|
||||
val code = new ExitCode
|
||||
val customThreadGroup = new ExitThreadGroup(new ExitHandler(Thread.getDefaultUncaughtExceptionHandler, originalThreads, code))
|
||||
val executionThread = new Thread(customThreadGroup, "run-main") { override def run() { execute } }
|
||||
|
||||
val originalSecurityManager = System.getSecurityManager
|
||||
try
|
||||
{
|
||||
val newSecurityManager = new TrapExitSecurityManager(originalSecurityManager, customThreadGroup)
|
||||
System.setSecurityManager(newSecurityManager)
|
||||
|
||||
executionThread.start()
|
||||
|
||||
log.debug("Waiting for threads to exit or System.exit to be called.")
|
||||
waitForExit(originalThreads, log)
|
||||
log.debug("Interrupting remaining threads (should be all daemons).")
|
||||
interruptAll(originalThreads) // should only be daemon threads left now
|
||||
log.debug("Sandboxed run complete..")
|
||||
code.value.getOrElse(0)
|
||||
}
|
||||
finally { System.setSecurityManager(originalSecurityManager) }
|
||||
}
|
||||
// wait for all non-daemon threads to terminate
|
||||
private def waitForExit(originalThreads: Set[Thread], log: Logger)
|
||||
{
|
||||
var daemonsOnly = true
|
||||
processThreads(originalThreads, thread =>
|
||||
if(!thread.isDaemon)
|
||||
{
|
||||
daemonsOnly = false
|
||||
waitOnThread(thread, log)
|
||||
}
|
||||
)
|
||||
if(!daemonsOnly)
|
||||
waitForExit(originalThreads, log)
|
||||
}
|
||||
/** Waits for the given thread to exit. */
|
||||
private def waitOnThread(thread: Thread, log: Logger)
|
||||
{
|
||||
log.debug("Waiting for thread " + thread.getName + " to exit")
|
||||
thread.join
|
||||
log.debug("\tThread " + thread.getName + " exited.")
|
||||
}
|
||||
/** Returns the exit code of the System.exit that caused the given Exception, or rethrows the exception
|
||||
* if its cause was not calling System.exit.*/
|
||||
private def exitCode(e: Throwable) =
|
||||
withCause[TrapExitSecurityException, Int](e)
|
||||
{exited => exited.exitCode}
|
||||
{other => throw other}
|
||||
/** Recurses into the causes of the given exception looking for a cause of type CauseType. If one is found, `withType` is called with that cause.
|
||||
* If not, `notType` is called with the root cause.*/
|
||||
private def withCause[CauseType <: Throwable, T](e: Throwable)(withType: CauseType => T)(notType: Throwable => T)(implicit mf: Manifest[CauseType]): T =
|
||||
{
|
||||
val clazz = mf.erasure
|
||||
if(clazz.isInstance(e))
|
||||
withType(e.asInstanceOf[CauseType])
|
||||
else
|
||||
{
|
||||
val cause = e.getCause
|
||||
if(cause == null)
|
||||
notType(e)
|
||||
else
|
||||
withCause(cause)(withType)(notType)(mf)
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns all threads that are not in the 'system' thread group and are not the AWT implementation
|
||||
* thread (AWT-XAWT, AWT-Windows, ...)*/
|
||||
private def allThreads: Set[Thread] =
|
||||
{
|
||||
val allThreads = wrap.Wrappers.toList(Thread.getAllStackTraces.keySet)
|
||||
val threads = new scala.collection.mutable.HashSet[Thread]
|
||||
for(thread <- allThreads if !isSystemThread(thread))
|
||||
threads += thread
|
||||
threads
|
||||
}
|
||||
/** Returns true if the given thread is in the 'system' thread group and is an AWT thread other than
|
||||
* AWT-EventQueue or AWT-Shutdown.*/
|
||||
private def isSystemThread(t: Thread) =
|
||||
{
|
||||
val name = t.getName
|
||||
if(name.startsWith("AWT-"))
|
||||
!(name.startsWith("AWT-EventQueue") || name.startsWith("AWT-Shutdown"))
|
||||
else
|
||||
{
|
||||
val group = t.getThreadGroup
|
||||
(group != null) && (group.getName == "system")
|
||||
}
|
||||
}
|
||||
/** Calls the provided function for each thread in the system as provided by the
|
||||
* allThreads function except those in ignoreThreads.*/
|
||||
private def processThreads(ignoreThreads: Set[Thread], process: Thread => Unit)
|
||||
{
|
||||
allThreads.filter(thread => !ignoreThreads.contains(thread)).foreach(process)
|
||||
}
|
||||
/** Handles System.exit by disposing all frames and calling interrupt on all user threads */
|
||||
private def stopAll(originalThreads: Set[Thread])
|
||||
{
|
||||
disposeAllFrames()
|
||||
interruptAll(originalThreads)
|
||||
}
|
||||
private def disposeAllFrames()
|
||||
{
|
||||
val allFrames = java.awt.Frame.getFrames
|
||||
if(allFrames.length > 0)
|
||||
{
|
||||
allFrames.foreach(_.dispose) // dispose all top-level windows, which will cause the AWT-EventQueue-* threads to exit
|
||||
Thread.sleep(2000) // AWT Thread doesn't exit immediately, so wait to interrupt it
|
||||
}
|
||||
}
|
||||
// interrupt all threads that appear to have been started by the user
|
||||
private def interruptAll(originalThreads: Set[Thread]): Unit =
|
||||
processThreads(originalThreads, safeInterrupt)
|
||||
// interrupts the given thread, but first replaces the exception handler so that the InterruptedException is not printed
|
||||
private def safeInterrupt(thread: Thread)
|
||||
{
|
||||
if(!thread.getName.startsWith("AWT-"))
|
||||
{
|
||||
thread.setUncaughtExceptionHandler(new TrapInterrupt(thread.getUncaughtExceptionHandler))
|
||||
thread.interrupt
|
||||
}
|
||||
}
|
||||
// an uncaught exception handler that swallows InterruptedExceptions and otherwise defers to originalHandler
|
||||
private final class TrapInterrupt(originalHandler: Thread.UncaughtExceptionHandler) extends Thread.UncaughtExceptionHandler
|
||||
{
|
||||
def uncaughtException(thread: Thread, e: Throwable)
|
||||
{
|
||||
withCause[InterruptedException, Unit](e)
|
||||
{interrupted => ()}
|
||||
{other => originalHandler.uncaughtException(thread, e) }
|
||||
thread.setUncaughtExceptionHandler(originalHandler)
|
||||
}
|
||||
}
|
||||
/** An uncaught exception handler that delegates to the original uncaught exception handler except when
|
||||
* the cause was a call to System.exit (which generated a SecurityException)*/
|
||||
private final class ExitHandler(originalHandler: Thread.UncaughtExceptionHandler, originalThreads: Set[Thread], codeHolder: ExitCode) extends Thread.UncaughtExceptionHandler
|
||||
{
|
||||
def uncaughtException(t: Thread, e: Throwable)
|
||||
{
|
||||
try
|
||||
{
|
||||
codeHolder.set(exitCode(e)) // will rethrow e if it was not because of a call to System.exit
|
||||
stopAll(originalThreads)
|
||||
}
|
||||
catch
|
||||
{
|
||||
case _ => originalHandler.uncaughtException(t, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
private final class ExitThreadGroup(handler: Thread.UncaughtExceptionHandler) extends ThreadGroup("trap.exit")
|
||||
{
|
||||
override def uncaughtException(t: Thread, e: Throwable) = handler.uncaughtException(t, e)
|
||||
}
|
||||
}
|
||||
private final class ExitCode extends NotNull
|
||||
{
|
||||
private var code: Option[Int] = None
|
||||
def set(c: Int): Unit = synchronized { code = code orElse Some(c) }
|
||||
def value: Option[Int] = synchronized { code }
|
||||
}
|
||||
/////// These two classes are based on similar classes in Nailgun
|
||||
/** A custom SecurityManager to disallow System.exit. */
|
||||
private final class TrapExitSecurityManager(delegateManager: SecurityManager, group: ThreadGroup) extends SecurityManager
|
||||
{
|
||||
import java.security.Permission
|
||||
override def checkExit(status: Int)
|
||||
{
|
||||
val stack = Thread.currentThread.getStackTrace
|
||||
if(stack == null || stack.exists(isRealExit))
|
||||
throw new TrapExitSecurityException(status)
|
||||
}
|
||||
/** This ensures that only actual calls to exit are trapped and not just calls to check if exit is allowed.*/
|
||||
private def isRealExit(element: StackTraceElement): Boolean =
|
||||
element.getClassName == "java.lang.Runtime" && element.getMethodName == "exit"
|
||||
override def checkPermission(perm: Permission)
|
||||
{
|
||||
if(delegateManager != null)
|
||||
delegateManager.checkPermission(perm)
|
||||
}
|
||||
override def checkPermission(perm: Permission, context: AnyRef)
|
||||
{
|
||||
if(delegateManager != null)
|
||||
delegateManager.checkPermission(perm, context)
|
||||
}
|
||||
override def getThreadGroup = group
|
||||
}
|
||||
/** A custom SecurityException that tries not to be caught.*/
|
||||
private final class TrapExitSecurityException(val exitCode: Int) extends SecurityException
|
||||
{
|
||||
private var accessAllowed = false
|
||||
def allowAccess
|
||||
{
|
||||
accessAllowed = true
|
||||
}
|
||||
override def printStackTrace = ifAccessAllowed(super.printStackTrace)
|
||||
override def toString = ifAccessAllowed(super.toString)
|
||||
override def getCause = ifAccessAllowed(super.getCause)
|
||||
override def getMessage = ifAccessAllowed(super.getMessage)
|
||||
override def fillInStackTrace = ifAccessAllowed(super.fillInStackTrace)
|
||||
override def getLocalizedMessage = ifAccessAllowed(super.getLocalizedMessage)
|
||||
private def ifAccessAllowed[T](f: => T): T =
|
||||
{
|
||||
if(accessAllowed)
|
||||
f
|
||||
else
|
||||
throw this
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
sealed trait Version extends NotNull
|
||||
case class BasicVersion(major: Int, minor: Option[Int], micro: Option[Int], extra: Option[String]) extends Version
|
||||
{
|
||||
import Version._
|
||||
require(major >= 0, "Major revision must be nonnegative.")
|
||||
require(minor.isDefined || micro.isEmpty, "Cannot define micro revision without defining minor revision.")
|
||||
requirePositive(minor)
|
||||
requirePositive(micro)
|
||||
require(isValidExtra(extra))
|
||||
|
||||
def incrementMicro = BasicVersion(major, minor orElse Some(0), increment(micro), extra)
|
||||
def incrementMinor = BasicVersion(major, increment(minor), micro, extra)
|
||||
def incrementMajor = BasicVersion(major+1, minor, micro, extra)
|
||||
|
||||
override def toString = major +
|
||||
minor.map(minorI => "." + minorI + micro.map(microI => "." + microI).getOrElse("")).getOrElse("") +
|
||||
extra.map(x => "-" + x).getOrElse("")
|
||||
}
|
||||
case class OpaqueVersion(value: String) extends Version
|
||||
{
|
||||
require(!value.trim.isEmpty)
|
||||
override def toString = value
|
||||
}
|
||||
object Version
|
||||
{
|
||||
private[sbt] def increment(i: Option[Int]) = Some(i.getOrElse(0) + 1)
|
||||
private[sbt] def requirePositive(i: Option[Int]) { i.foreach(x => require(x >= 0)) }
|
||||
|
||||
import java.util.regex.Pattern
|
||||
val versionPattern = Pattern.compile("""(\d+)(?:\.(\d+)(?:\.(\d+))?)?(?:-(.+))?""")
|
||||
def fromString(v: String): Either[String, Version] =
|
||||
{
|
||||
val trimmed = v.trim
|
||||
if(trimmed.isEmpty)
|
||||
Left("Version cannot be empty.")
|
||||
else
|
||||
{
|
||||
val matcher = versionPattern.matcher(trimmed)
|
||||
import matcher._
|
||||
if(matches)
|
||||
{
|
||||
def toOption(index: Int) =
|
||||
{
|
||||
val v = group(index)
|
||||
if(v == null) None else Some(v)
|
||||
}
|
||||
def toInt(index: Int) = toOption(index).map(_.toInt)
|
||||
val extra = toOption(4)
|
||||
if(isValidExtra(extra))
|
||||
Right(BasicVersion(group(1).toInt, toInt(2), toInt(3), extra))
|
||||
else
|
||||
Right(OpaqueVersion(trimmed))
|
||||
}
|
||||
else
|
||||
Right(OpaqueVersion(trimmed))
|
||||
}
|
||||
}
|
||||
def isValidExtra(e: Option[String]): Boolean = e.map(isValidExtra).getOrElse(true)
|
||||
def isValidExtra(s: String): Boolean = !(s.trim.isEmpty || s.exists(java.lang.Character.isISOControl))
|
||||
}
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import java.io.File
|
||||
import java.net.{URL, URLClassLoader}
|
||||
import scala.xml.NodeSeq
|
||||
|
||||
object JettyRun extends ExitHook
|
||||
{
|
||||
val DefaultPort = 8080
|
||||
|
||||
ExitHooks.register(this)
|
||||
|
||||
def name = "jetty-shutdown"
|
||||
def runBeforeExiting() { stop() }
|
||||
private var running: Option[Stoppable] = None
|
||||
private def started(s: Stoppable) { running = Some(s) }
|
||||
def stop()
|
||||
{
|
||||
synchronized
|
||||
{
|
||||
running.foreach(_.stop())
|
||||
running = None
|
||||
}
|
||||
}
|
||||
def apply(classpath: Iterable[Path], classpathName: String, war: Path, defaultContextPath: String, jettyConfigurationXML: NodeSeq,
|
||||
jettyConfigurationFiles: Seq[File], log: Logger): Option[String] =
|
||||
run(classpathName, new JettyRunConfiguration(war, defaultContextPath, DefaultPort, jettyConfigurationXML,
|
||||
jettyConfigurationFiles, Nil, 0, toURLs(classpath)), log)
|
||||
def apply(classpath: Iterable[Path], classpathName: String, war: Path, defaultContextPath: String, port: Int, scanDirectories: Seq[File],
|
||||
scanPeriod: Int, log: Logger): Option[String] =
|
||||
run(classpathName, new JettyRunConfiguration(war, defaultContextPath, port, NodeSeq.Empty, Nil, scanDirectories, scanPeriod, toURLs(classpath)), log)
|
||||
private def toURLs(paths: Iterable[Path]) = paths.map(_.asURL).toSeq
|
||||
private def run(classpathName: String, configuration: JettyRunConfiguration, log: Logger): Option[String] =
|
||||
synchronized
|
||||
{
|
||||
import configuration._
|
||||
def runJetty() =
|
||||
{
|
||||
val baseLoader = this.getClass.getClassLoader
|
||||
val loader: ClassLoader = new SelectiveLoader(classpathURLs.toArray, baseLoader, "org.mortbay." :: "javax.servlet." :: Nil)
|
||||
val lazyLoader = new LazyFrameworkLoader(implClassName, Array(FileUtilities.sbtJar.toURI.toURL), loader, baseLoader)
|
||||
val runner = ModuleUtilities.getObject(implClassName, lazyLoader).asInstanceOf[JettyRun]
|
||||
runner(configuration, log)
|
||||
}
|
||||
|
||||
if(running.isDefined)
|
||||
Some("Jetty is already running.")
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
started(runJetty())
|
||||
None
|
||||
}
|
||||
catch
|
||||
{
|
||||
case e: NoClassDefFoundError => runError(e, "Jetty and its dependencies must be on the " + classpathName + " classpath: ", log)
|
||||
case e => runError(e, "Error running Jetty: ", log)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val implClassName = "sbt.LazyJettyRun"
|
||||
|
||||
private def runError(e: Throwable, messageBase: String, log: Logger) =
|
||||
{
|
||||
log.trace(e)
|
||||
Some(messageBase + e.toString)
|
||||
}
|
||||
}
|
||||
|
||||
private trait Stoppable
|
||||
{
|
||||
def stop(): Unit
|
||||
}
|
||||
private trait JettyRun
|
||||
{
|
||||
def apply(configuration: JettyRunConfiguration, log: Logger): Stoppable
|
||||
}
|
||||
private class JettyRunConfiguration(val war: Path, val defaultContextPath: String, val port: Int,
|
||||
val jettyConfigurationXML: NodeSeq, val jettyConfigurationFiles: Seq[File],
|
||||
val scanDirectories: Seq[File], val scanInterval: Int, val classpathURLs: Seq[URL]) extends NotNull
|
||||
|
||||
/* This class starts Jetty.
|
||||
* NOTE: DO NOT actively use this class. You will see NoClassDefFoundErrors if you fail
|
||||
* to do so.Only use its name in JettyRun for reflective loading. This allows using
|
||||
* the Jetty libraries provided on the project classpath instead of requiring them to be
|
||||
* available on sbt's classpath at startup.
|
||||
*/
|
||||
private object LazyJettyRun extends JettyRun
|
||||
{
|
||||
import org.mortbay.jetty.{Handler, Server}
|
||||
import org.mortbay.jetty.nio.SelectChannelConnector
|
||||
import org.mortbay.jetty.webapp.WebAppContext
|
||||
import org.mortbay.log.Log
|
||||
import org.mortbay.util.Scanner
|
||||
import org.mortbay.xml.XmlConfiguration
|
||||
|
||||
import java.lang.ref.{Reference, WeakReference}
|
||||
|
||||
val DefaultMaxIdleTime = 30000
|
||||
|
||||
def apply(configuration: JettyRunConfiguration, log: Logger): Stoppable =
|
||||
{
|
||||
import configuration._
|
||||
val oldLog = Log.getLog
|
||||
Log.setLog(new JettyLogger(log))
|
||||
val server = new Server
|
||||
val useDefaults = jettyConfigurationXML.isEmpty && jettyConfigurationFiles.isEmpty
|
||||
|
||||
val listener =
|
||||
if(useDefaults)
|
||||
{
|
||||
configureDefaultConnector(server, port)
|
||||
def createLoader = new URLClassLoader(classpathURLs.toArray, this.getClass.getClassLoader)
|
||||
val webapp = new WebAppContext(war.absolutePath, defaultContextPath)
|
||||
webapp.setClassLoader(createLoader)
|
||||
server.setHandler(webapp)
|
||||
|
||||
Some(new Scanner.BulkListener {
|
||||
def filesChanged(files: java.util.List[_]) {
|
||||
reload(server, webapp.setClassLoader(createLoader), log)
|
||||
}
|
||||
})
|
||||
}
|
||||
else
|
||||
{
|
||||
for(x <- jettyConfigurationXML)
|
||||
(new XmlConfiguration(x.toString)).configure(server)
|
||||
for(file <- jettyConfigurationFiles)
|
||||
(new XmlConfiguration(file.toURI.toURL)).configure(server)
|
||||
None
|
||||
}
|
||||
|
||||
def configureScanner() =
|
||||
{
|
||||
if(listener.isEmpty || scanDirectories.isEmpty)
|
||||
None
|
||||
else
|
||||
{
|
||||
log.debug("Scanning for changes to: " + scanDirectories.mkString(", "))
|
||||
val scanner = new Scanner
|
||||
val list = new java.util.ArrayList[File]
|
||||
scanDirectories.foreach(x => list.add(x))
|
||||
scanner.setScanDirs(list)
|
||||
scanner.setRecursive(true)
|
||||
scanner.setScanInterval(scanInterval)
|
||||
scanner.setReportExistingFilesOnStartup(false)
|
||||
scanner.addListener(listener.get)
|
||||
scanner.start()
|
||||
Some(new WeakReference(scanner))
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
server.start()
|
||||
new StopServer(new WeakReference(server), configureScanner(), oldLog)
|
||||
}
|
||||
catch { case e => server.stop(); throw e }
|
||||
}
|
||||
private def configureDefaultConnector(server: Server, port: Int)
|
||||
{
|
||||
val defaultConnector = new SelectChannelConnector
|
||||
defaultConnector.setPort(port)
|
||||
defaultConnector.setMaxIdleTime(DefaultMaxIdleTime)
|
||||
server.addConnector(defaultConnector)
|
||||
}
|
||||
private class StopServer(serverReference: Reference[Server], scannerReferenceOpt: Option[Reference[Scanner]], oldLog: org.mortbay.log.Logger) extends Stoppable
|
||||
{
|
||||
def stop()
|
||||
{
|
||||
val server = serverReference.get
|
||||
if(server != null)
|
||||
server.stop()
|
||||
for(scannerReference <- scannerReferenceOpt)
|
||||
{
|
||||
val scanner = scannerReference.get
|
||||
if(scanner != null)
|
||||
scanner.stop()
|
||||
}
|
||||
Log.setLog(oldLog)
|
||||
}
|
||||
}
|
||||
private def reload(server: Server, reconfigure: => Unit, log: Logger)
|
||||
{
|
||||
JettyRun.synchronized
|
||||
{
|
||||
log.info("Reloading web application...")
|
||||
val handlers = wrapNull(server.getHandlers, server.getHandler)
|
||||
log.debug("Stopping handlers: " + handlers.mkString(", "))
|
||||
handlers.foreach(_.stop)
|
||||
log.debug("Reconfiguring...")
|
||||
reconfigure
|
||||
log.debug("Restarting handlers: " + handlers.mkString(", "))
|
||||
handlers.foreach(_.start)
|
||||
log.info("Reload complete.")
|
||||
}
|
||||
}
|
||||
private def wrapNull(a: Array[Handler], b: Handler) =
|
||||
(a, b) match
|
||||
{
|
||||
case (null, null) => Nil
|
||||
case (null, notB) => notB :: Nil
|
||||
case (notA, null) => notA.toList
|
||||
case (notA, notB) => notB :: notA.toList
|
||||
}
|
||||
private class JettyLogger(delegate: Logger) extends org.mortbay.log.Logger
|
||||
{
|
||||
def isDebugEnabled = delegate.atLevel(Level.Debug)
|
||||
def setDebugEnabled(enabled: Boolean) = delegate.setLevel(if(enabled) Level.Debug else Level.Info)
|
||||
|
||||
def getLogger(name: String) = this
|
||||
def info(msg: String, arg0: AnyRef, arg1: AnyRef) { delegate.info(format(msg, arg0, arg1)) }
|
||||
def debug(msg: String, arg0: AnyRef, arg1: AnyRef) { delegate.debug(format(msg, arg0, arg1)) }
|
||||
def warn(msg: String, arg0: AnyRef, arg1: AnyRef) { delegate.warn(format(msg, arg0, arg1)) }
|
||||
def warn(msg: String, th: Throwable)
|
||||
{
|
||||
delegate.warn(msg)
|
||||
delegate.trace(th)
|
||||
}
|
||||
def debug(msg: String, th: Throwable)
|
||||
{
|
||||
delegate.debug(msg)
|
||||
delegate.trace(th)
|
||||
}
|
||||
private def format(msg: String, arg0: AnyRef, arg1: AnyRef) =
|
||||
{
|
||||
def toString(arg: AnyRef) = if(arg == null) "" else arg.toString
|
||||
val pieces = msg.split("""\{\}""", 3)
|
||||
if(pieces.length == 1)
|
||||
pieces(0)
|
||||
else
|
||||
{
|
||||
val base = pieces(0) + toString(arg0) + pieces(1)
|
||||
if(pieces.length == 2)
|
||||
base
|
||||
else
|
||||
base + toString(arg1) + pieces(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import java.io.File
|
||||
import scala.xml.{Elem, NodeSeq}
|
||||
import Control._
|
||||
|
||||
/** Defines the configurable parameters for the webstart task. */
|
||||
trait WebstartOptions extends NotNull
|
||||
{
|
||||
/** The main jar to use for webstart.*/
|
||||
def webstartMainJar: Path
|
||||
/** The location to put all generated files for webstart.*/
|
||||
def webstartOutputDirectory: Path
|
||||
/** Generates the .jnlp file using the provided resource descriptions. Each resource description
|
||||
* provides the path of the jar relative to 'webstartOutputDirectory' and whether or not
|
||||
* it is the main jar.*/
|
||||
def jnlpXML(jars: Seq[WebstartJarResource]): Elem
|
||||
/** The location to write the .jnlp file to. It must be in 'webstartOutputDirectory'.*/
|
||||
def jnlpFile: Path
|
||||
/** The location to put all jars that are not the main jar. It must be in 'webstartOutputDirectory'.*/
|
||||
def webstartLibDirectory: Path
|
||||
/** The libraries needed for webstart. Note that only jars are used; directories are discarded.*/
|
||||
def webstartLibraries: PathFinder
|
||||
/** Libraries external to the project needed for webstart. This is mainly for scala libraries.*/
|
||||
def webstartExtraLibraries: PathFinder
|
||||
/** Resources to copy to the webstart output directory.*/
|
||||
def webstartResources: PathFinder
|
||||
/** If defined, this specifies where to create a zip of the webstart output directory. It cannot be
|
||||
* in the output directory.*/
|
||||
def webstartZip: Option[Path]
|
||||
|
||||
/** If defined, configures signing of jars. All jars (main and libraries) are signed using
|
||||
* this configuration.*/
|
||||
def webstartSignConfiguration: Option[SignConfiguration]
|
||||
/** If true, pack200 compression is applied to all jars (main and libraries). A version of each jar
|
||||
* without pack200 compression is still created in the webstart output directory.*/
|
||||
def webstartPack200: Boolean
|
||||
/** If true, gzip compression will be applied to all jars. If pack200 compression is enabled,
|
||||
* gzip compression is also applied to the archives with pack200 compression. A version of
|
||||
* each file without gzip compression is still created in the webstart output directory. */
|
||||
def webstartGzip: Boolean
|
||||
}
|
||||
/** Represents a library included in the webstart distribution. Name is the filename of the jar.
|
||||
* href is the path of the jar relative to the webstart output directory. isMain is true only for
|
||||
* the main jar. */
|
||||
final class WebstartJarResource(val name: String, val href: String, val isMain: Boolean) extends NotNull
|
||||
/** Configuration for signing jars. */
|
||||
final class SignConfiguration(val alias: String, val options: Seq[SignJar.SignOption]) extends NotNull
|
||||
/** A scala project that produces a webstart distribution. */
|
||||
trait WebstartScalaProject extends ScalaProject
|
||||
{
|
||||
import WebstartScalaProject._
|
||||
/** Creates a task that produces a webstart distribution using the given options.*/
|
||||
def webstartTask(options: WebstartOptions) =
|
||||
task
|
||||
{
|
||||
import options._
|
||||
FileUtilities.createDirectories(webstartOutputDirectory :: webstartLibDirectory :: Nil, log) // ignore errors
|
||||
verifyOptions(options)
|
||||
|
||||
def relativize(jar: Path) = Path.relativize(webstartOutputDirectory ##, jar) getOrElse
|
||||
error("Jar (" + jar + ") was not in webstart output directory (" + webstartOutputDirectory + ").")
|
||||
def signAndPack(jars: List[Path], targetDirectory: Path): Either[String, List[Path]] =
|
||||
{
|
||||
lazyFold(jars, Nil: List[Path])
|
||||
{ (allJars, jar) =>
|
||||
val signPackResult =
|
||||
webstartSignConfiguration match
|
||||
{
|
||||
case Some(config) =>
|
||||
if(webstartPack200)
|
||||
signAndPack200(jar, config, targetDirectory, log)
|
||||
else
|
||||
signOnly(jar, config, targetDirectory, log)
|
||||
case None =>
|
||||
if(webstartPack200)
|
||||
pack200Only(jar, targetDirectory, log)
|
||||
else
|
||||
copyJar(jar, targetDirectory, log).right.map(jars => new Jars(jars, Nil))
|
||||
}
|
||||
val deleteOriginal = webstartPack200
|
||||
signPackResult.right flatMap { addJars =>
|
||||
if(webstartGzip)
|
||||
Control.lazyFold(addJars.gzippable, addJars.allJars ::: allJars)
|
||||
{ (accumulate, jar) => gzipJar(jar, deleteOriginal, log).right.map(_ ::: accumulate) }
|
||||
else
|
||||
Right(addJars.allJars ::: allJars)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import FileUtilities._
|
||||
|
||||
val jars = (webstartLibraries +++ webstartExtraLibraries).get.filter(ClasspathUtilities.isArchive)
|
||||
def process(jars: Iterable[Path]) = for(jar <- jars if jar.asFile.getName.endsWith(".jar")) yield relativize(jar)
|
||||
|
||||
thread(signAndPack(webstartMainJar :: Nil, webstartOutputDirectory)) { mainJars =>
|
||||
thread(signAndPack(jars.toList, webstartLibDirectory)) { libJars =>
|
||||
writeXML(jnlpXML(jarResources(process(mainJars), process(libJars))), jnlpFile, log) orElse
|
||||
thread(copy(webstartResources.get, webstartOutputDirectory, log)) { copiedResources =>
|
||||
val keep = jnlpFile +++ Path.lazyPathFinder(mainJars ++ libJars ++ copiedResources) +++
|
||||
webstartOutputDirectory +++ webstartLibDirectory
|
||||
prune(webstartOutputDirectory, keep.get, log) orElse
|
||||
webstartZip.flatMap( zipPath => zip(List(webstartOutputDirectory ##), zipPath, true, log) )
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Creates default XML elements for a JNLP file for the given resources.*/
|
||||
protected def defaultElements(resources: Seq[WebstartJarResource]): NodeSeq = NodeSeq.fromSeq(resources.map(defaultElement))
|
||||
/** Creates a default XML element for a JNLP file for the given resource.*/
|
||||
protected def defaultElement(resource: WebstartJarResource): Elem =
|
||||
<jar href={resource.href} main={resource.isMain.toString}/>
|
||||
|
||||
}
|
||||
private class Jars(val gzippable: List[Path], val nonGzippable: List[Path]) extends NotNull
|
||||
{
|
||||
def allJars = gzippable ::: nonGzippable
|
||||
}
|
||||
private object WebstartScalaProject
|
||||
{
|
||||
import FileTasks.{runOption, wrapProduct, wrapProducts}
|
||||
/** Changes the extension of the Path of the given jar from ".jar" to newExtension. If append is true,
|
||||
* the new extension is simply appended to the jar's filename. */
|
||||
private def appendExtension(jar: Path, newExtension: String) =
|
||||
jar match
|
||||
{
|
||||
case rp: RelativePath => rp.parentPath / (rp.component + newExtension)
|
||||
case x => x
|
||||
}
|
||||
private def gzipJarPath(jar: Path) = appendExtension(jar, ".gz")
|
||||
private def packPath(jar: Path) = appendExtension(jar, ".pack")
|
||||
private def signOnly(jar: Path, signConfiguration: SignConfiguration, targetDirectory: Path, log: Logger) =
|
||||
{
|
||||
val targetJar = targetDirectory / jar.asFile.getName
|
||||
runOption("sign", targetJar from jar, log) {
|
||||
log.debug("Signing " + jar)
|
||||
signAndVerify(jar, signConfiguration, targetJar, log)
|
||||
}.toLeft(new Jars(targetJar :: Nil, Nil))
|
||||
}
|
||||
private def signAndVerify(jar: Path, signConfiguration: SignConfiguration, targetJar: Path, log: Logger) =
|
||||
{
|
||||
import SignJar._
|
||||
sign(jar, signConfiguration.alias, signedJar(targetJar) :: signConfiguration.options.toList, log) orElse
|
||||
verify(jar, signConfiguration.options, log).map(err => "Signed jar failed verification: " + err)
|
||||
}
|
||||
private def gzipJar(jar: Path, deleteOriginal: Boolean, log: Logger) =
|
||||
{
|
||||
val gzipJar = gzipJarPath(jar)
|
||||
runOption("gzip", gzipJar from jar, log)
|
||||
{
|
||||
log.debug("Gzipping " + jar)
|
||||
FileUtilities.gzip(jar, gzipJar, log) orElse
|
||||
(if(deleteOriginal) FileUtilities.clean(jar :: Nil, true, log) else None)
|
||||
}.toLeft(gzipJar :: Nil)
|
||||
}
|
||||
/** Properly performs both signing and pack200 compression and verifies the result. This method only does anything if
|
||||
* its outputs are out of date with respect to 'jar'. Note that it does not determine if the signing configuration has changed.
|
||||
* See java.util.jar.Pack200 for more information.*/
|
||||
private def signAndPack200(jar: Path, signConfiguration: SignConfiguration, targetDirectory: Path, log: Logger) =
|
||||
{
|
||||
val signedJar = targetDirectory / jar.asFile.getName
|
||||
val packedJar = packPath(signedJar)
|
||||
import signConfiguration._
|
||||
|
||||
runOption("sign and pack200", List(packedJar, signedJar) from jar, log) {
|
||||
log.debug("Applying pack200 compression and signing " + jar)
|
||||
signAndPack(jar, signedJar, packedJar, alias, options, log) orElse
|
||||
signAndVerify(jar, signConfiguration, signedJar, log)
|
||||
}.toLeft(new Jars(packedJar :: Nil, signedJar :: Nil))
|
||||
}
|
||||
/** Properly performs both signing and pack200 compression and verifies the result. See java.util.jar.Pack200 for more information.*/
|
||||
private def signAndPack(jarPath: Path, signedPath: Path, out: Path, alias: String, options: Seq[SignJar.SignOption], log: Logger): Option[String] =
|
||||
{
|
||||
import Pack._
|
||||
import SignJar._
|
||||
pack(jarPath, out, log) orElse
|
||||
unpack(out, signedPath, log) orElse
|
||||
sign(signedPath, alias, options, log) orElse
|
||||
pack(signedPath, out, log) orElse
|
||||
unpack(out, signedPath, log) orElse
|
||||
verify(signedPath, options, log)
|
||||
}
|
||||
private def pack200Only(jar: Path, targetDirectory: Path, log: Logger) =
|
||||
{
|
||||
val targetJar = targetDirectory / jar.asFile.getName
|
||||
val packedJar = packPath(targetJar)
|
||||
val packResult =
|
||||
runOption("pack200", packedJar from jar, log)
|
||||
{
|
||||
log.debug("Applying pack200 compression to " + jar)
|
||||
Pack.pack(jar, packedJar, log)
|
||||
}
|
||||
packResult match
|
||||
{
|
||||
case Some(err) => Left(err)
|
||||
case None => copyJar(jar, targetDirectory, log).right.map(jars => new Jars(packedJar :: Nil, jars))
|
||||
}
|
||||
}
|
||||
private def copyJar(jar: Path, targetDirectory: Path, log: Logger) =
|
||||
{
|
||||
val targetJar = targetDirectory / jar.asFile.getName
|
||||
runOption("copy jar", targetJar from jar, log)( FileUtilities.copyFile(jar, targetJar, log) ).toLeft(targetJar :: Nil)
|
||||
}
|
||||
/** Writes the XML string 'xmlString' to the file 'outputPath'.*/
|
||||
private def writeXML(xmlString: String, outputPath: Path, log: Logger): Option[String] =
|
||||
FileUtilities.write(outputPath.asFile, xmlString, log)
|
||||
/** Writes the XML string 'xmlString' to the file 'outputPath' if the hashes are different.*/
|
||||
private def writeXML(xml: Elem, outputPath: Path, log: Logger): Option[String] =
|
||||
{
|
||||
val xmlString = scala.xml.Utility.toXML(xml, false)
|
||||
if(!outputPath.exists)
|
||||
{
|
||||
log.debug("JNLP file did not exist, writing inline XML to " + outputPath)
|
||||
writeXML(xmlString, outputPath, log)
|
||||
}
|
||||
else
|
||||
{
|
||||
val result =
|
||||
for( xmlHash <- Hash(xmlString, log).right; fileHash <- Hash(outputPath, log).right ) yield
|
||||
{
|
||||
if(xmlHash deepEquals fileHash)
|
||||
{
|
||||
log.debug("JNLP file " + outputPath + " uptodate.")
|
||||
None
|
||||
}
|
||||
else
|
||||
{
|
||||
log.debug("Inline JNLP XML modified, updating file " + outputPath + ".")
|
||||
writeXML(xmlString, outputPath, log)
|
||||
}
|
||||
}
|
||||
result.fold(err => Some(err), x => x)
|
||||
}
|
||||
}
|
||||
private def jarResource(isMain: Boolean)(jar: Path): WebstartJarResource =
|
||||
new WebstartJarResource(jar.asFile.getName, jar.relativePathString("/"), isMain)
|
||||
private def jarResources(mainJars: Iterable[Path], libraries: Iterable[Path]): Seq[WebstartJarResource] =
|
||||
mainJars.map(jarResource(true)).toList ::: libraries.map(jarResource(false)).toList
|
||||
|
||||
/** True iff 'directory' is an ancestor (strictly) of 'check'.*/
|
||||
private def isInDirectory(directory: Path, check: Path) = Path.relativize(directory, check).isDefined && directory != check
|
||||
/** Checks the paths in the given options for validity. See the documentation for WebstartOptions.*/
|
||||
private def verifyOptions(options: WebstartOptions)
|
||||
{
|
||||
import options._
|
||||
require(isInDirectory(webstartOutputDirectory, webstartLibDirectory),
|
||||
"Webstart dependency directory (" + webstartLibDirectory + ") must be a subdirectory of webstart output directory (" +
|
||||
webstartOutputDirectory + ").")
|
||||
require(isInDirectory(webstartOutputDirectory, jnlpFile), "Webstart JNLP file output location (" + jnlpFile +
|
||||
") must be in the webstart output directory (" + webstartOutputDirectory + ").")
|
||||
for(wz <- webstartZip)
|
||||
require(!isInDirectory(webstartOutputDirectory, wz),
|
||||
"Webstart output zip location (" + wz + " cannot be in webstart output directory (" + webstartOutputDirectory + ").")
|
||||
}
|
||||
}
|
||||
/** The default extension point for a webstart project. There is one method that is required to be defined: jnlpXML.
|
||||
* 'webstartSignConfiguration', 'webstartPack200', and 'webstartGzip' are methods of interest. */
|
||||
abstract class DefaultWebstartProject(val info: ProjectInfo) extends BasicWebstartProject with MavenStyleWebstartPaths
|
||||
/** Defines default implementations of all methods in WebstartOptions except for jnlpXML. packageAction is overridden
|
||||
* to create a webstart distribution after the normal package operation. */
|
||||
abstract class BasicWebstartProject extends BasicScalaProject with WebstartScalaProject with WebstartOptions with WebstartPaths
|
||||
{
|
||||
def webstartSignConfiguration: Option[SignConfiguration] = None
|
||||
|
||||
def webstartExtraLibraries = mainDependencies.scalaJars
|
||||
def webstartLibraries = publicClasspath +++ jarsOfProjectDependencies
|
||||
def webstartResources = descendents(jnlpResourcesPath ##, AllPassFilter)
|
||||
|
||||
def webstartPack200 = true
|
||||
def webstartGzip = true
|
||||
|
||||
override def packageAction = super.packageAction && webstartTask(this)
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt.impl
|
||||
|
||||
import scala.util.parsing.combinator.Parsers
|
||||
import scala.util.parsing.input.CharSequenceReader
|
||||
import scala.util.parsing.input.CharArrayReader.EofCh
|
||||
|
||||
/** Parses a command of the form:
|
||||
* identifier argument*
|
||||
* where argument may be quoted to include spaces and
|
||||
* quotes and backslashes should be escaped.
|
||||
* (Most of the complexity is for decent error handling.)*/
|
||||
private[sbt] object CommandParser extends Parsers
|
||||
{
|
||||
type Elem = Char
|
||||
def parse(commandString: String): Either[String, (String, List[String])] =
|
||||
{
|
||||
command(new CharSequenceReader(commandString.trim, 0)) match
|
||||
{
|
||||
case Success(id ~ args, next) => Right((id, args))
|
||||
case err: NoSuccess =>
|
||||
{
|
||||
val pos = err.next.pos
|
||||
Left("Could not parse command: (" + pos.line + "," + pos.column + "): " + err.msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
def command = phrase(identifier ~! (argument*))
|
||||
def identifier = unquoted | err("Expected identifier")
|
||||
def argument = ( (whitespaceChar+) ~> (unquoted | quoted) )
|
||||
|
||||
def unquoted: Parser[String] = ((unquotedChar ~! (unquotedMainChar*)) ^^ { case a ~ tail => (a :: tail).mkString("") })
|
||||
def quoted: Parser[String] = quote ~> quotedChars <~ (quote | err("Missing closing quote character"))
|
||||
|
||||
def quotedChars: Parser[String] = (escape | nonescapeChar)*
|
||||
def escape: Parser[Char] = backslash ~> (escapeChar | err("Illegal escape"))
|
||||
def escapeChar: Parser[Char] = quote | backslash
|
||||
def nonescapeChar: Parser[Char] = elem("", ch => !isEscapeChar(ch) && ch != EofCh)
|
||||
def unquotedChar: Parser[Char] = elem("", ch => !isEscapeChar(ch) && !Character.isWhitespace(ch) && ch != EofCh)
|
||||
def unquotedMainChar: Parser[Char] = unquotedChar | (errorIfEscape ~> failure(""))
|
||||
|
||||
private def errorIfEscape = (not(quote) | err("Unexpected quote character")) ~>
|
||||
(not(backslash) | err("Escape sequences can only occur in a quoted argument"))
|
||||
|
||||
private def isEscapeChar(ch: Char) = ch == '\\' || ch == '"'
|
||||
|
||||
def quote: Parser[Char] = '"'
|
||||
def backslash: Parser[Char] = '\\'
|
||||
def whitespaceChar: Parser[Char] = elem("whitespace", ch => Character.isWhitespace(ch))
|
||||
|
||||
private implicit def toString(p: Parser[List[Char]]): Parser[String] = p ^^ {_ mkString "" }
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
|
||||
package sbt.impl
|
||||
|
||||
import java.util.Properties
|
||||
import java.io.{File, FileInputStream, FileOutputStream, InputStream, OutputStream}
|
||||
import scala.collection.mutable.{HashMap, HashSet, ListBuffer, Map, Set}
|
||||
|
||||
private[sbt] object PropertiesUtilities
|
||||
{
|
||||
def write(properties: Properties, label: String, to: Path, log: Logger) =
|
||||
FileUtilities.writeStream(to.asFile, log)(output => { properties.store(output, label); None })
|
||||
def load(properties: Properties, from: Path, log: Logger): Option[String] =
|
||||
{
|
||||
val file = from.asFile
|
||||
if(file.exists)
|
||||
FileUtilities.readStream(file, log)( input => { properties.load(input); None })
|
||||
else
|
||||
None
|
||||
}
|
||||
def propertyNames(properties: Properties): Iterable[String] =
|
||||
wrap.Wrappers.toList(properties.propertyNames).map(_.toString)
|
||||
}
|
||||
|
||||
private[sbt] object MapUtilities
|
||||
{
|
||||
def write[Key, Value](map: Map[Key, Value], label: String, to: Path, log: Logger)(implicit keyFormat: Format[Key], valueFormat: Format[Value]): Option[String] =
|
||||
{
|
||||
val properties = new Properties
|
||||
map foreach { pair => properties.setProperty(keyFormat.toString(pair._1), valueFormat.toString(pair._2)) }
|
||||
PropertiesUtilities.write(properties, label, to, log)
|
||||
}
|
||||
def read[Key, Value](map: Map[Key, Value], from: Path, log: Logger)(implicit keyFormat: Format[Key], valueFormat: Format[Value]): Option[String] =
|
||||
{
|
||||
map.clear
|
||||
val properties = new Properties
|
||||
PropertiesUtilities.load(properties, from, log) orElse
|
||||
{
|
||||
for(name <- PropertiesUtilities.propertyNames(properties))
|
||||
map.put( keyFormat.fromString(name), valueFormat.fromString(properties.getProperty(name)))
|
||||
None
|
||||
}
|
||||
}
|
||||
def all[Key, Value](map: Map[Key, Set[Value]]): Iterable[Value] =
|
||||
map.values.toList.flatMap(set => set.toList)
|
||||
|
||||
def readOnlyIterable[Key, Value](i: Map[Key, Set[Value]]): Iterable[(Key, scala.collection.Set[Value])] =
|
||||
for( (key, set) <- i.elements.toList) yield (key, wrap.Wrappers.readOnly(set))//.readOnly)
|
||||
|
||||
def mark[Key, Value](source: Key, map: Map[Key, Set[Value]])
|
||||
{
|
||||
if(!map.contains(source))
|
||||
map.put(source, new HashSet[Value])
|
||||
}
|
||||
def add[Key, Value](key: Key, value: Value, map: Map[Key, Set[Value]]): Unit =
|
||||
map.getOrElseUpdate(key, new HashSet[Value]) + value
|
||||
}
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
import java.lang.{Process => JProcess, ProcessBuilder => JProcessBuilder}
|
||||
import java.io.{BufferedReader, Closeable, InputStream, InputStreamReader, IOException, OutputStream}
|
||||
import java.io.{PipedInputStream, PipedOutputStream}
|
||||
import java.io.{File, FileInputStream, FileOutputStream}
|
||||
import java.net.URL
|
||||
|
||||
import scala.concurrent.ops.future
|
||||
import scala.concurrent.SyncVar
|
||||
|
||||
/** Runs provided code in a new Thread and returns the Thread instance. */
|
||||
private object Spawn
|
||||
{
|
||||
def apply(f: => Unit): Thread =
|
||||
{
|
||||
val thread = new Thread() { override def run() = { f } }
|
||||
thread.start()
|
||||
thread
|
||||
}
|
||||
}
|
||||
|
||||
private object BasicIO
|
||||
{
|
||||
def apply(log: Logger) = new ProcessIO(ignoreOut, processFully(log, Level.Info), processFully(log, Level.Error))
|
||||
|
||||
def ignoreOut = (i: OutputStream) => ()
|
||||
val BufferSize = 8192
|
||||
def close(c: java.io.Closeable) = try { c.close() } catch { case _: java.io.IOException => () }
|
||||
def processFully(log: Logger, level: Level.Value)(i: InputStream) { processFully(line => log.log(level, line))(i) }
|
||||
def processFully(processLine: String => Unit)(i: InputStream)
|
||||
{
|
||||
val reader = new BufferedReader(new InputStreamReader(i))
|
||||
def readFully()
|
||||
{
|
||||
val line = reader.readLine()
|
||||
if(line != null)
|
||||
{
|
||||
processLine(line)
|
||||
readFully()
|
||||
}
|
||||
}
|
||||
readFully()
|
||||
}
|
||||
def standard: ProcessIO = new ProcessIO(ignoreOut, processFully(System.out.println), processFully(System.err.println))
|
||||
|
||||
def transferFully(in: InputStream, out: OutputStream)
|
||||
{
|
||||
val continueCount = 1//if(in.isInstanceOf[PipedInputStream]) 1 else 0
|
||||
val buffer = new Array[Byte](BufferSize)
|
||||
def read
|
||||
{
|
||||
val byteCount = in.read(buffer)
|
||||
if(byteCount >= continueCount)
|
||||
{
|
||||
out.write(buffer, 0, byteCount)
|
||||
read
|
||||
}
|
||||
}
|
||||
read
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private abstract class AbstractProcessBuilder extends ProcessBuilder
|
||||
{
|
||||
def #&&(other: ProcessBuilder): ProcessBuilder = new AndProcessBuilder(this, other)
|
||||
def #||(other: ProcessBuilder): ProcessBuilder = new OrProcessBuilder(this, other)
|
||||
def #|(other: ProcessBuilder): ProcessBuilder =
|
||||
{
|
||||
require(other.canPipeTo, "Piping to multiple processes is not supported.")
|
||||
new PipedProcessBuilder(this, other, false)
|
||||
}
|
||||
def ##(other: ProcessBuilder): ProcessBuilder = new SequenceProcessBuilder(this, other)
|
||||
|
||||
def #< (f: File): ProcessBuilder = new PipedProcessBuilder(new FileInput(f), this, false)
|
||||
def #< (url: URL): ProcessBuilder = new PipedProcessBuilder(new URLInput(url), this, false)
|
||||
def #> (f: File): ProcessBuilder = new PipedProcessBuilder(this, new FileOutput(f, false), false)
|
||||
def #>> (f: File): ProcessBuilder = new PipedProcessBuilder(this, new FileOutput(f, true), true)
|
||||
|
||||
def run(): Process = run(BasicIO.standard)
|
||||
def run(log: Logger): Process = run(BasicIO(log))
|
||||
|
||||
def ! = run().exitValue()
|
||||
def !(log: Logger) =
|
||||
{
|
||||
val log2 = new BufferedLogger(log)
|
||||
log2.startRecording()
|
||||
try { run(log2).exitValue() }
|
||||
finally { log2.playAll(); log2.clearAll() }
|
||||
}
|
||||
def !(io: ProcessIO) = run(io).exitValue()
|
||||
|
||||
def canPipeTo = false
|
||||
}
|
||||
private[sbt] class URLBuilder(url: URL) extends URLPartialBuilder
|
||||
{
|
||||
def #>(b: ProcessBuilder): ProcessBuilder = b #< url
|
||||
def #>>(file: File): ProcessBuilder = toFile(file, true)
|
||||
def #>(file: File): ProcessBuilder = toFile(file, false)
|
||||
private def toFile(file: File, append: Boolean) = new PipedProcessBuilder(new URLInput(url), new FileOutput(file, append), false)
|
||||
}
|
||||
private[sbt] class FileBuilder(base: File) extends FilePartialBuilder
|
||||
{
|
||||
def #>(b: ProcessBuilder): ProcessBuilder = b #< base
|
||||
def #<(b: ProcessBuilder): ProcessBuilder = b #> base
|
||||
def #<(url: URL): ProcessBuilder = new URLBuilder(url) #> base
|
||||
def #>>(file: File): ProcessBuilder = pipe(base, file, true)
|
||||
def #>(file: File): ProcessBuilder = pipe(base, file, false)
|
||||
def #<(file: File): ProcessBuilder = pipe(file, base, false)
|
||||
def #<<(file: File): ProcessBuilder = pipe(file, base, true)
|
||||
private def pipe(from: File, to: File, append: Boolean) = new PipedProcessBuilder(new FileInput(from), new FileOutput(to, append), false)
|
||||
}
|
||||
|
||||
private abstract class BasicBuilder extends AbstractProcessBuilder
|
||||
{
|
||||
protected[this] def checkNotThis(a: ProcessBuilder) = require(a != this, "Compound process '" + a + "' cannot contain itself.")
|
||||
final def run(io: ProcessIO): Process =
|
||||
{
|
||||
val p = createProcess(io)
|
||||
p.start()
|
||||
p
|
||||
}
|
||||
protected[this] def createProcess(io: ProcessIO): BasicProcess
|
||||
}
|
||||
private abstract class BasicProcess extends Process
|
||||
{
|
||||
def start(): Unit
|
||||
}
|
||||
|
||||
private abstract class CompoundProcess extends BasicProcess
|
||||
{
|
||||
def destroy() { destroyer() }
|
||||
def exitValue() = getExitValue().getOrElse(error("No exit code: process destroyed."))
|
||||
|
||||
def start() = getExitValue
|
||||
|
||||
protected lazy val (getExitValue, destroyer) =
|
||||
{
|
||||
val code = new SyncVar[Option[Int]]()
|
||||
code.set(None)
|
||||
val thread = Spawn(code.set(runAndExitValue()))
|
||||
|
||||
(
|
||||
future { thread.join(); code.get },
|
||||
() => thread.interrupt()
|
||||
)
|
||||
}
|
||||
|
||||
/** Start and block until the exit value is available and then return it in Some. Return None if destroyed (use 'run')*/
|
||||
protected[this] def runAndExitValue(): Option[Int]
|
||||
|
||||
protected[this] def runInterruptible[T](action: => T)(destroyImpl: => Unit): Option[T] =
|
||||
{
|
||||
try { Some(action) }
|
||||
catch { case _: InterruptedException => destroyImpl; None }
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class SequentialProcessBuilder(a: ProcessBuilder, b: ProcessBuilder, operatorString: String) extends BasicBuilder
|
||||
{
|
||||
checkNotThis(a)
|
||||
checkNotThis(b)
|
||||
override def toString = " ( " + a + " " + operatorString + " " + b + " ) "
|
||||
}
|
||||
private class PipedProcessBuilder(first: ProcessBuilder, second: ProcessBuilder, toError: Boolean) extends SequentialProcessBuilder(first, second, if(toError) "#|!" else "#|")
|
||||
{
|
||||
override def createProcess(io: ProcessIO) = new PipedProcesses(first, second, io, toError)
|
||||
}
|
||||
private class AndProcessBuilder(first: ProcessBuilder, second: ProcessBuilder) extends SequentialProcessBuilder(first, second, "#&&")
|
||||
{
|
||||
override def createProcess(io: ProcessIO) = new AndProcess(first, second, io)
|
||||
}
|
||||
private class OrProcessBuilder(first: ProcessBuilder, second: ProcessBuilder) extends SequentialProcessBuilder(first, second, "#||")
|
||||
{
|
||||
override def createProcess(io: ProcessIO) = new OrProcess(first, second, io)
|
||||
}
|
||||
private class SequenceProcessBuilder(first: ProcessBuilder, second: ProcessBuilder) extends SequentialProcessBuilder(first, second, "##")
|
||||
{
|
||||
override def createProcess(io: ProcessIO) = new ProcessSequence(first, second, io)
|
||||
}
|
||||
|
||||
private class SequentialProcess(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO, evaluateSecondProcess: Int => Boolean) extends CompoundProcess
|
||||
{
|
||||
protected[this] override def runAndExitValue() =
|
||||
{
|
||||
val first = a.run(io)
|
||||
runInterruptible(first.exitValue)(first.destroy()) flatMap
|
||||
{ codeA =>
|
||||
if(evaluateSecondProcess(codeA))
|
||||
{
|
||||
val second = b.run(io)
|
||||
runInterruptible(second.exitValue)(second.destroy())
|
||||
}
|
||||
else
|
||||
Some(codeA)
|
||||
}
|
||||
}
|
||||
}
|
||||
private class AndProcess(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO) extends SequentialProcess(a, b, io, _ == 0)
|
||||
private class OrProcess(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO) extends SequentialProcess(a, b, io, _ != 0)
|
||||
private class ProcessSequence(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO) extends SequentialProcess(a, b, io, ignore => true)
|
||||
|
||||
|
||||
private class PipedProcesses(a: ProcessBuilder, b: ProcessBuilder, defaultIO: ProcessIO, toError: Boolean) extends CompoundProcess
|
||||
{
|
||||
protected[this] override def runAndExitValue() =
|
||||
{
|
||||
val currentSource = new SyncVar[Option[InputStream]]
|
||||
val pipeOut = new PipedOutputStream
|
||||
val source = new PipeSource(currentSource, pipeOut, a.toString)
|
||||
source.start()
|
||||
|
||||
val pipeIn = new PipedInputStream(pipeOut)
|
||||
val currentSink = new SyncVar[Option[OutputStream]]
|
||||
val sink = new PipeSink(pipeIn, currentSink, b.toString)
|
||||
sink.start()
|
||||
|
||||
def handleOutOrError(fromOutput: InputStream) = currentSource.put(Some(fromOutput))
|
||||
|
||||
val firstIO =
|
||||
if(toError)
|
||||
defaultIO.withError(handleOutOrError)
|
||||
else
|
||||
defaultIO.withOutput(handleOutOrError)
|
||||
val secondIO = defaultIO.withInput(toInput => currentSink.put(Some(toInput)) )
|
||||
|
||||
val second = b.run(secondIO)
|
||||
val first = a.run(firstIO)
|
||||
try
|
||||
{
|
||||
runInterruptible {
|
||||
first.exitValue
|
||||
currentSource.put(None)
|
||||
currentSink.put(None)
|
||||
val result = second.exitValue
|
||||
result
|
||||
} {
|
||||
first.destroy()
|
||||
second.destroy()
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
BasicIO.close(pipeIn)
|
||||
BasicIO.close(pipeOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
private class PipeSource(currentSource: SyncVar[Option[InputStream]], pipe: PipedOutputStream, label: => String) extends Thread
|
||||
{
|
||||
final override def run()
|
||||
{
|
||||
currentSource.get match
|
||||
{
|
||||
case Some(source) =>
|
||||
try { BasicIO.transferFully(source, pipe) }
|
||||
catch { case e: IOException => println("I/O error " + e.getMessage + " for process: " + label); e.printStackTrace() }
|
||||
finally
|
||||
{
|
||||
BasicIO.close(source)
|
||||
currentSource.unset()
|
||||
}
|
||||
run()
|
||||
case None =>
|
||||
currentSource.unset()
|
||||
BasicIO.close(pipe)
|
||||
}
|
||||
}
|
||||
}
|
||||
private class PipeSink(pipe: PipedInputStream, currentSink: SyncVar[Option[OutputStream]], label: => String) extends Thread
|
||||
{
|
||||
final override def run()
|
||||
{
|
||||
currentSink.get match
|
||||
{
|
||||
case Some(sink) =>
|
||||
try { BasicIO.transferFully(pipe, sink) }
|
||||
catch { case e: IOException => println("I/O error " + e.getMessage + " for process: " + label); e.printStackTrace() }
|
||||
finally
|
||||
{
|
||||
BasicIO.close(sink)
|
||||
currentSink.unset()
|
||||
}
|
||||
run()
|
||||
case None =>
|
||||
currentSink.unset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Represents a simple command without any redirection or combination. */
|
||||
private[sbt] class SimpleProcessBuilder(p: JProcessBuilder) extends AbstractProcessBuilder
|
||||
{
|
||||
override def run(io: ProcessIO): Process =
|
||||
{
|
||||
val process = p.start() // start the external process
|
||||
import io.{writeInput, processOutput, processError}
|
||||
// spawn threads that process the input, output, and error streams using the functions defined in `io`
|
||||
val inThread = Spawn(writeInput(process.getOutputStream))
|
||||
val outThread = Spawn(processOutput(process.getInputStream))
|
||||
val errorThread =
|
||||
if(!p.redirectErrorStream)
|
||||
Spawn(processError(process.getErrorStream)) :: Nil
|
||||
else
|
||||
Nil
|
||||
new SimpleProcess(process, inThread :: outThread :: errorThread)
|
||||
}
|
||||
override def toString = p.command.toString
|
||||
override def canPipeTo = true
|
||||
}
|
||||
/** A thin wrapper around a java.lang.Process. `ioThreads` are the Threads created to do I/O.
|
||||
* The implementation of `exitValue` waits until these threads die before returning. */
|
||||
private class SimpleProcess(p: JProcess, ioThreads: Iterable[Thread]) extends Process
|
||||
{
|
||||
override def exitValue() =
|
||||
{
|
||||
p.waitFor() // wait for the process to terminate
|
||||
ioThreads.foreach(_.join()) // this ensures that all output is complete before returning (waitFor does not ensure this)
|
||||
p.exitValue()
|
||||
}
|
||||
override def destroy() = p.destroy()
|
||||
}
|
||||
|
||||
private class FileOutput(file: File, append: Boolean) extends OutputStreamBuilder(new FileOutputStream(file, append), file.getAbsolutePath)
|
||||
private class URLInput(url: URL) extends InputStreamBuilder(url.openStream, url.toString)
|
||||
private class FileInput(file: File) extends InputStreamBuilder(new FileInputStream(file), file.getAbsolutePath)
|
||||
|
||||
private class OutputStreamBuilder(stream: => OutputStream, label: String) extends ThreadProcessBuilder(label, _.writeInput(stream))
|
||||
private class InputStreamBuilder(stream: => InputStream, label: String) extends ThreadProcessBuilder(label, _.processOutput(stream))
|
||||
|
||||
private abstract class ThreadProcessBuilder(override val toString: String, runImpl: ProcessIO => Unit) extends AbstractProcessBuilder
|
||||
{
|
||||
override def run(io: ProcessIO): Process =
|
||||
{
|
||||
val success = new SyncVar[Boolean]
|
||||
success.put(false)
|
||||
new ThreadProcess(Spawn {runImpl(io); success.set(true) }, success)
|
||||
}
|
||||
}
|
||||
private class ThreadProcess(thread: Thread, success: SyncVar[Boolean]) extends Process
|
||||
{
|
||||
override def exitValue() =
|
||||
{
|
||||
thread.join()
|
||||
if(success.get) 0 else 1
|
||||
}
|
||||
override def destroy() { thread.interrupt() }
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt.impl
|
||||
|
||||
import scala.collection.{immutable, mutable}
|
||||
import scala.collection.Map
|
||||
import sbt.wrap.Wrappers.identityMap
|
||||
|
||||
private[sbt] object RunTask
|
||||
{
|
||||
final type Task = Project#Task
|
||||
def apply(root: Task, rootName: String): List[WorkFailure[Task]] = apply(root, rootName, true)
|
||||
def apply(root: Task, rootName: String, parallelExecution: Boolean): List[WorkFailure[Task]] =
|
||||
apply(root, rootName, if(parallelExecution) Runtime.getRuntime.availableProcessors else 1)
|
||||
def apply(root: Task, rootName: String, maximumTasks: Int): List[WorkFailure[Task]] = (new RunTask(root, rootName, maximumTasks)).run()
|
||||
}
|
||||
import RunTask._
|
||||
private final class RunTask(root: Task, rootName: String, maximumTasks: Int) extends NotNull
|
||||
{
|
||||
require(maximumTasks >= 1)
|
||||
def parallel = maximumTasks > 1
|
||||
def multiProject = allProjects.size >= 2
|
||||
def run(): List[WorkFailure[Task]] =
|
||||
{
|
||||
try
|
||||
{
|
||||
runTasksExceptRoot() match
|
||||
{
|
||||
case Nil =>
|
||||
val result = runTask(root, rootName)
|
||||
result.map( errorMessage => WorkFailure(root, "Error running " + rootName + ": " + errorMessage) ).toList
|
||||
case failures => failures
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
for(project <- allProjects; saveError <- project.saveEnvironment)
|
||||
project.log.warn("Could not save properties for project " + project.name + ": " + saveError)
|
||||
}
|
||||
}
|
||||
// This runs all tasks except the root.task.
|
||||
// It uses a buffered logger in record mode to ensure that all output for a given task is consecutive
|
||||
// it ignores the root task so that the root task may be run with buffering disabled so that the output
|
||||
// occurs without delay.
|
||||
private def runTasksExceptRoot() =
|
||||
{
|
||||
withBuffered(_.startRecording())
|
||||
try { ParallelRunner.run(expandedRoot, expandedTaskName, runIfNotRoot, maximumTasks, (t: Task) => t.manager.log) }
|
||||
finally { withBuffered(_.stop()) }
|
||||
}
|
||||
private def withBuffered(f: BufferedLogger => Unit)
|
||||
{
|
||||
for(buffered <- bufferedLoggers)
|
||||
Control.trap(f(buffered))
|
||||
}
|
||||
/** Will be called in its own thread. Runs the given task if it is not the root task.*/
|
||||
private def runIfNotRoot(action: Task): Option[String] =
|
||||
{
|
||||
if(isRoot(action))
|
||||
None
|
||||
else
|
||||
runTask(action, expandedTaskName(action))
|
||||
}
|
||||
private def isRoot(t: Task) = t == expandedRoot
|
||||
/** Will be called in its own thread except for the root task. */
|
||||
private def runTask(action: Task, actionName: String): Option[String] =
|
||||
{
|
||||
val label = if(multiProject) (action.manager.name + " / " + actionName) else actionName
|
||||
def banner(event: ControlEvent.Value, firstSeparator: String, secondSeparator: String) =
|
||||
Control.trap(action.manager.log.control(event, firstSeparator + " " + label + " " + secondSeparator))
|
||||
if(parallel)
|
||||
{
|
||||
try { banner(ControlEvent.Start, "\n ", "...") }
|
||||
finally { flush(action) }
|
||||
}
|
||||
banner(ControlEvent.Header, "\n==", "==")
|
||||
try { action.invoke }
|
||||
catch { case e: Exception => action.manager.log.trace(e); Some(e.toString) }
|
||||
finally
|
||||
{
|
||||
banner(ControlEvent.Finish, "==", "==")
|
||||
if(parallel)
|
||||
flush(action)
|
||||
}
|
||||
}
|
||||
private def trapFinally(toTrap: => Unit)(runFinally: => Unit)
|
||||
{
|
||||
try { toTrap }
|
||||
catch { case e: Exception => () }
|
||||
finally { try { runFinally } catch { case e: Exception => () } }
|
||||
}
|
||||
private def flush(action: Task)
|
||||
{
|
||||
for(buffered <- bufferedLogger(action.manager))
|
||||
Control.trap(flush(buffered))
|
||||
}
|
||||
private def flush(buffered: BufferedLogger)
|
||||
{
|
||||
buffered.play()
|
||||
buffered.clear()
|
||||
}
|
||||
|
||||
/* Most of the following is for implicitly adding dependencies (see the expand method)*/
|
||||
private val projectDependencyCache = identityMap[Project, Iterable[Project]]
|
||||
private def dependencies(project: Project) = projectDependencyCache.getOrElseUpdate(project, project.topologicalSort.dropRight(1))
|
||||
|
||||
private val expandedCache = identityMap[Task, Task]
|
||||
private def expanded(task: Task): Task = expandedCache.getOrElseUpdate(task, expandImpl(task))
|
||||
|
||||
private val expandedTaskNameCache = identityMap[Task, String]
|
||||
private def expandedTaskName(task: Task) =
|
||||
if(task == expandedRoot)
|
||||
rootName
|
||||
else
|
||||
expandedTaskNameCache.getOrElse(task, task.name)
|
||||
|
||||
private val nameToTaskCache = identityMap[Project, Map[String, Task]]
|
||||
private def nameToTaskMap(project: Project): Map[String, Task] = nameToTaskCache.getOrElseUpdate(project, project.tasks)
|
||||
private def taskForName(project: Project, name: String): Option[Task] = nameToTaskMap(project).get(name)
|
||||
|
||||
private val taskNameCache = identityMap[Project, Map[Task, String]]
|
||||
private def taskName(task: Task) =
|
||||
{
|
||||
val project = task.manager
|
||||
taskNameCache.getOrElseUpdate(project, taskNameMap(project)).get(task)
|
||||
}
|
||||
|
||||
private val expandedRoot = expand(root)
|
||||
private val allTasks = expandedRoot.topologicalSort
|
||||
private val allProjects = Set(allTasks.map(_.manager).toSeq : _*)
|
||||
private val bufferedLoggers = if(parallel) allProjects.toList.flatMap(bufferedLogger) else Nil
|
||||
|
||||
/** Adds implicit dependencies, which are tasks with the same name in the project dependencies
|
||||
* of the enclosing project of the task.*/
|
||||
private def expand(root: Task): Task = expanded(root)
|
||||
private def expandImpl(task: Task): Task =
|
||||
{
|
||||
val nameOption = taskName(task)
|
||||
val explicitDependencies = task.dependencies
|
||||
val implicitDependencies = nameOption.map(name => dependencies(task.manager).flatMap(noninteractiveTask(name)) ).getOrElse(Nil)
|
||||
val allDependencies = mutable.HashSet( (explicitDependencies ++ implicitDependencies).toSeq : _* )
|
||||
val expandedTask = task.setDependencies(allDependencies.toList.map(expanded))
|
||||
nameOption.foreach(name => expandedTaskNameCache(expandedTask) = name)
|
||||
expandedTask
|
||||
}
|
||||
private def noninteractiveTask(name: String)(project: Project): Option[Task] =
|
||||
taskForName(project, name) flatMap { task =>
|
||||
if(task.interactive)
|
||||
{
|
||||
project.log.debug("Not including task " + name + " in project " + project.name + ": interactive tasks can only be run directly.")
|
||||
None
|
||||
}
|
||||
else
|
||||
Some(task)
|
||||
}
|
||||
private def taskNameMap(project: Project) = mutable.Map(nameToTaskMap(project).map(_.swap).toSeq : _*)
|
||||
private def bufferedLogger(project: Project): Option[BufferedLogger] =
|
||||
project.log match
|
||||
{
|
||||
case buffered: BufferedLogger => Some(buffered)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package sbt.impl
|
||||
|
||||
private[sbt] object SelectMainClass
|
||||
{
|
||||
def apply(promptIfMultipleChoices: Boolean, mainClasses: List[String]) =
|
||||
{
|
||||
mainClasses match
|
||||
{
|
||||
case Nil => None
|
||||
case head :: Nil => Some(head)
|
||||
case multiple =>
|
||||
if(promptIfMultipleChoices)
|
||||
{
|
||||
println("\nMultiple main classes detected, select one to run:\n")
|
||||
for( (className, index) <- multiple.zipWithIndex )
|
||||
println(" [" + (index+1) + "] " + className)
|
||||
val line = trim(SimpleReader.readLine("\nEnter number: "))
|
||||
println("")
|
||||
toInt(line, multiple.length) map multiple.apply
|
||||
}
|
||||
else
|
||||
None
|
||||
}
|
||||
}
|
||||
private def trim(s: Option[String]) = s.getOrElse("")
|
||||
private def toInt(s: String, size: Int) =
|
||||
try
|
||||
{
|
||||
val i = s.toInt
|
||||
if(i > 0 && i <= size)
|
||||
Some(i-1)
|
||||
else
|
||||
{
|
||||
println("Number out of range: was " + i + ", expected number between 1 and " + size)
|
||||
None
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
case nfe: NumberFormatException =>
|
||||
println("Invalid number: " + nfe.toString)
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Steven Blundy, Mark Harrah
|
||||
*/
|
||||
|
||||
package sbt.impl
|
||||
|
||||
import scala.xml.{Elem, Group}
|
||||
|
||||
/* The following classes run tests for their associated test framework.
|
||||
* NOTE #1: DO NOT actively use these classes. Only specify their names to LazyTestFramework
|
||||
* for reflective loading. This allows using the test libraries provided on the
|
||||
* project classpath instead of requiring global versions.
|
||||
* NOTE #2: Keep all active uses of these test frameworks inside these classes so that sbt
|
||||
* runs without error when a framework is not available at runtime and no tests for that
|
||||
* framework are defined.*/
|
||||
|
||||
/** The test runner for ScalaCheck tests. */
|
||||
private[sbt] class ScalaCheckRunner(val log: Logger, val listeners: Seq[TestReportListener], val testLoader: ClassLoader) extends BasicTestRunner
|
||||
{
|
||||
import org.scalacheck.{Pretty, Properties, Test}
|
||||
def runTest(testClassName: String): Result.Value =
|
||||
{
|
||||
val test = ModuleUtilities.getObject(testClassName, testLoader).asInstanceOf[Properties]
|
||||
if(Test.checkProperties(test, Test.defaultParams, propReport, testReport).find(!_._2.passed).isEmpty)
|
||||
Result.Passed
|
||||
else
|
||||
Result.Failed
|
||||
}
|
||||
private def propReport(pName: String, s: Int, d: Int) {}
|
||||
private def testReport(pName: String, res: Test.Result) =
|
||||
{
|
||||
if(res.passed)
|
||||
fire(PassedEvent(pName, Pretty.pretty(res)))
|
||||
else
|
||||
fire(FailedEvent(pName, Pretty.pretty(res)))
|
||||
}
|
||||
}
|
||||
/** The test runner for ScalaTest suites. */
|
||||
private[sbt] class ScalaTestRunner(val log: Logger, val listeners: Seq[TestReportListener], val testLoader: ClassLoader) extends BasicTestRunner
|
||||
{
|
||||
def runTest(testClassName: String): Result.Value =
|
||||
{
|
||||
import org.scalatest.{Stopper, Suite}
|
||||
val testClass = Class.forName(testClassName, true, testLoader).asSubclass(classOf[Suite])
|
||||
val test = testClass.newInstance
|
||||
val reporter = new ScalaTestReporter
|
||||
val stopper = new Stopper { override def stopRequested = false }
|
||||
test.execute(None, reporter, stopper, Set.empty, Set("org.scalatest.Ignore"), Map.empty, None)
|
||||
if(reporter.succeeded)
|
||||
Result.Passed
|
||||
else
|
||||
Result.Failed
|
||||
}
|
||||
|
||||
/** An implementation of Reporter for ScalaTest. */
|
||||
private class ScalaTestReporter extends org.scalatest.Reporter with NotNull
|
||||
{
|
||||
import org.scalatest.Report
|
||||
override def testIgnored(report: Report) =
|
||||
{
|
||||
if(report.message.trim.isEmpty) fire(IgnoredEvent(report.name, None))
|
||||
else fire(IgnoredEvent(report.name, Some(report.message.trim)))
|
||||
}
|
||||
override def testStarting(report: Report) { info(report, "Test starting", None) }
|
||||
override def testSucceeded(report: Report) { info(report, "Test succeeded", Some(Result.Passed)) }
|
||||
override def testFailed(report: Report)
|
||||
{
|
||||
succeeded = false
|
||||
error(report, "Test failed", Some(Result.Failed))
|
||||
}
|
||||
|
||||
override def infoProvided(report : Report) { info(report, "", None) }
|
||||
|
||||
override def suiteStarting(report: Report) { info(report, "Suite starting", None) }
|
||||
override def suiteCompleted(report: Report) { info(report, "Suite completed", None) }
|
||||
override def suiteAborted(report: Report) { error(report, "Suite aborted", None) }
|
||||
|
||||
override def runStarting(testCount: Int) { fire(MessageEvent("Run starting")) }
|
||||
override def runStopped()
|
||||
{
|
||||
succeeded = false
|
||||
fire(ErrorEvent("Run stopped"))
|
||||
}
|
||||
override def runAborted(report: Report)
|
||||
{
|
||||
succeeded = false
|
||||
error(report, "Run aborted", None)
|
||||
}
|
||||
override def runCompleted() { log.info("Run completed.") }
|
||||
|
||||
private def error(report: Report, event: String, result: Option[Result.Value]) { logReport(report, event, result, Level.Error) }
|
||||
private def info(report: Report, event: String, result: Option[Result.Value]) { logReport(report, event, result, Level.Info) }
|
||||
private def logReport(report: Report, event: String, result: Option[Result.Value], level: Level.Value)
|
||||
{
|
||||
level match
|
||||
{
|
||||
case Level.Error =>
|
||||
if(report.message.trim.isEmpty)
|
||||
fire(TypedErrorEvent(report.name, event, None, report.throwable)(result))
|
||||
else
|
||||
fire(TypedErrorEvent(report.name, event, Some(report.message.trim), report.throwable)(result))
|
||||
case Level.Info =>
|
||||
if(report.message.trim.isEmpty)
|
||||
fire(TypedEvent(report.name, event, None)(result))
|
||||
else
|
||||
fire(TypedEvent(report.name, event, Some(report.message.trim))(result))
|
||||
case l => log.warn("Level not expected:" + l)
|
||||
}
|
||||
}
|
||||
|
||||
var succeeded = true
|
||||
}
|
||||
}
|
||||
/** The test runner for specs tests. */
|
||||
private[sbt] class SpecsRunner(val log: Logger, val listeners: Seq[TestReportListener], val testLoader: ClassLoader) extends BasicTestRunner
|
||||
{
|
||||
import org.specs.Specification
|
||||
import org.specs.specification.{Example, Sus}
|
||||
|
||||
def runTest(testClassName: String): Result.Value =
|
||||
{
|
||||
val test = ModuleUtilities.getObject(testClassName, testLoader).asInstanceOf[Specification]
|
||||
val event = reportSpecification(test)
|
||||
fire(event)
|
||||
if(test.isFailing)
|
||||
Result.Failed
|
||||
else
|
||||
Result.Passed
|
||||
}
|
||||
|
||||
/* The following is closely based on org.specs.runner.OutputReporter,
|
||||
* part of specs, which is Copyright 2007-2008 Eric Torreborre.
|
||||
* */
|
||||
|
||||
private def reportSpecification(spec: Specification): SpecificationReportEvent =
|
||||
{
|
||||
// this is for binary compatibility between specs 1.4.x and 1.5.0: the ancestor of Specification containing these two methods changed
|
||||
val reflectedSpec: { def systems: Seq[Sus]; def subSpecifications: Seq[Specification] } = spec
|
||||
|
||||
return SpecificationReportEvent(spec.successes.size, spec.failures.size, spec.errors.size, spec.skipped.size, spec.pretty,
|
||||
reportSystems(reflectedSpec.systems), reportSpecifications(reflectedSpec.subSpecifications))
|
||||
}
|
||||
private def reportSpecifications(specifications: Seq[Specification]): Seq[SpecificationReportEvent] =
|
||||
{
|
||||
for(specification <- specifications) yield
|
||||
reportSpecification(specification)
|
||||
}
|
||||
private def reportSystems(systems: Seq[Sus]): Seq[SystemReportEvent] =
|
||||
{
|
||||
for(system <- systems) yield
|
||||
reportSystem(system)
|
||||
}
|
||||
private def reportSystem(sus: Sus): SystemReportEvent =
|
||||
{
|
||||
def format =
|
||||
{
|
||||
class ElemDesc(e: Elem) { def desc = e.child }
|
||||
implicit def elemToDesc(e: Elem): ElemDesc = new ElemDesc(e)
|
||||
|
||||
for(description <- sus.literateDescription) yield
|
||||
{
|
||||
// for source compatibility between specs 1.4.x and 1.5.0:
|
||||
// in specs 1.5.0, description is LiterateDescription
|
||||
// in specs < 1.5.0, description is Elem
|
||||
// LiterateDescription.desc is a Node
|
||||
// Elem.child is a Seq[Node]
|
||||
// each has a map[T](f: Node => T): Seq[T] defined so we implicitly convert
|
||||
// an Elem e to an intermediate object that has desc defined to be e.child
|
||||
|
||||
//description.child.map(_.text) // Elem equivalent
|
||||
description.desc.map(_.text) // LiterateDescription
|
||||
}
|
||||
}
|
||||
|
||||
SystemReportEvent(sus.description, sus.verb, sus.skippedSus, format, reportExamples(sus.examples))
|
||||
}
|
||||
private def reportExamples(examples: Seq[Example]): Seq[ExampleReportEvent] =
|
||||
{
|
||||
for(example <- examples) yield
|
||||
reportExample(example)
|
||||
}
|
||||
private def reportExample(example: Example): ExampleReportEvent =
|
||||
{
|
||||
ExampleReportEvent(example.description, example.errors, example.failures, example.skipped, reportExamples(example.subExamples))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2008, 2009 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
/* The following implements the simple syntax for storing test definitions.
|
||||
* The syntax is:
|
||||
*
|
||||
* definition := isModule? className separator className
|
||||
* isModule := '<module>'
|
||||
* separator := '<<'
|
||||
*/
|
||||
|
||||
import scala.util.parsing.combinator._
|
||||
|
||||
import TestParser._
|
||||
/** Represents a test implemented by 'testClassName' of type 'superClassName'.*/
|
||||
final case class TestDefinition(isModule: Boolean, testClassName: String, superClassName: String) extends NotNull
|
||||
{
|
||||
override def toString =
|
||||
(if(isModule) IsModuleLiteral else "") + testClassName + SubSuperSeparator + superClassName
|
||||
}
|
||||
final class TestParser extends RegexParsers with NotNull
|
||||
{
|
||||
def test: Parser[TestDefinition] =
|
||||
( isModule ~! className ~! SubSuperSeparator ~! className ) ^^
|
||||
{ case module ~ testName ~ SubSuperSeparator ~ superName => TestDefinition(module, testName.trim, superName.trim) }
|
||||
def isModule: Parser[Boolean] = (IsModuleLiteral?) ^^ (_.isDefined)
|
||||
def className: Parser[String] = ClassNameRegexString.r
|
||||
|
||||
def parse(testDefinitionString: String): Either[String, TestDefinition] =
|
||||
{
|
||||
def parseError(msg: String) = Left("Could not parse test definition '" + testDefinitionString + "': " + msg)
|
||||
parseAll(test, testDefinitionString) match
|
||||
{
|
||||
case Success(result, next) => Right(result)
|
||||
case err: NoSuccess => parseError(err.msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
object TestParser
|
||||
{
|
||||
val IsModuleLiteral = "<module>"
|
||||
val SubSuperSeparator = "<<"
|
||||
val ClassNameRegexString = """[^<]+"""
|
||||
def parse(testDefinitionString: String): Either[String, TestDefinition] = (new TestParser).parse(testDefinitionString)
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt.impl
|
||||
|
||||
import java.io.File
|
||||
import scala.collection.mutable.{HashMap, Map}
|
||||
|
||||
/** Only intended to be used once per instance. */
|
||||
private[sbt] class TestStatusReporter(path: Path, log: Logger) extends TestsListener
|
||||
{
|
||||
private lazy val succeeded: Map[String, Long] = TestStatus.read(path, log)
|
||||
|
||||
def doInit {}
|
||||
def startGroup(name: String) { succeeded removeKey name }
|
||||
def testEvent(event: TestEvent) {}
|
||||
def endGroup(name: String, t: Throwable) {}
|
||||
def endGroup(name: String, result: Result.Value)
|
||||
{
|
||||
if(result == Result.Passed)
|
||||
succeeded(name) = System.currentTimeMillis
|
||||
}
|
||||
def doComplete(finalResult: Result.Value) { complete() }
|
||||
def doComplete(t: Throwable) { complete() }
|
||||
|
||||
private def complete()
|
||||
{
|
||||
TestStatus.write(succeeded, "Successful Tests", path, log)
|
||||
}
|
||||
}
|
||||
|
||||
private[sbt] class TestQuickFilter(testAnalysis: CompileAnalysis, failedOnly: Boolean, path: Path, log: Logger) extends (String => Boolean) with NotNull
|
||||
{
|
||||
private lazy val exclude = TestStatus.read(path, log)
|
||||
private lazy val map = testAnalysis.testSourceMap
|
||||
def apply(test: String) =
|
||||
exclude.get(test) match
|
||||
{
|
||||
case None => true // include because this test has not been run or did not succeed
|
||||
case Some(lastSuccessTime) => // succeeded the last time it was run
|
||||
if(failedOnly)
|
||||
false // don't include because the last time succeeded
|
||||
else
|
||||
testAnalysis.products(map(test)) match
|
||||
{
|
||||
case None => true
|
||||
case Some(products) => products.exists(lastSuccessTime <= _.lastModified) // include if the test is newer than the last run
|
||||
}
|
||||
}
|
||||
}
|
||||
private object TestStatus
|
||||
{
|
||||
import java.util.Properties
|
||||
def read(path: Path, log: Logger): Map[String, Long] =
|
||||
{
|
||||
val map = new HashMap[String, Long]
|
||||
val properties = new Properties
|
||||
logError(PropertiesUtilities.load(properties, path, log), "loading", log)
|
||||
for(test <- PropertiesUtilities.propertyNames(properties))
|
||||
map.put(test, properties.getProperty(test).toLong)
|
||||
map
|
||||
}
|
||||
def write(map: Map[String, Long], label: String, path: Path, log: Logger)
|
||||
{
|
||||
val properties = new Properties
|
||||
for( (test, lastSuccessTime) <- map)
|
||||
properties.setProperty(test, lastSuccessTime.toString)
|
||||
logError(PropertiesUtilities.write(properties, label, path, log), "writing", log)
|
||||
}
|
||||
private def logError(result: Option[String], action: String, log: Logger)
|
||||
{
|
||||
result.foreach(msg => log.error("Error " + action + " test status: " + msg))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package sbt.wrap
|
||||
|
||||
// This file exists for compatibility between Scala 2.7.x and 2.8.0
|
||||
|
||||
import java.util.{Map => JMap, Set => JSet}
|
||||
|
||||
private[sbt] object Wrappers
|
||||
{
|
||||
def identityMap[K,V] = new MutableMapWrapper(new java.util.IdentityHashMap[K,V])
|
||||
def weakMap[K,V] = new MutableMapWrapper(new java.util.WeakHashMap[K,V])
|
||||
def toList[K,V](s: java.util.Map[K,V]): List[(K,V)] = toList(s.entrySet).map(e => (e.getKey, e.getValue))
|
||||
def toList[T](s: java.util.Collection[T]): List[T] = toList(s.iterator)
|
||||
def toList[T](s: java.util.Iterator[T]): List[T] =
|
||||
{
|
||||
def add(l: List[T]): List[T] =
|
||||
if(s.hasNext)
|
||||
add(s.next() :: l)
|
||||
else
|
||||
l
|
||||
add(Nil).reverse
|
||||
}
|
||||
def toList[T](s: java.util.Enumeration[T]): List[T] =
|
||||
{
|
||||
def add(l: List[T]): List[T] =
|
||||
if(s.hasMoreElements)
|
||||
add(s.nextElement() :: l)
|
||||
else
|
||||
l
|
||||
add(Nil).reverse
|
||||
}
|
||||
def readOnly[K,V](map: scala.collection.mutable.Map[K,V]): scala.collection.Map[K,V] = map//.readOnly
|
||||
def readOnly[T](set: scala.collection.mutable.Set[T]): scala.collection.Set[T] = set//.readOnly
|
||||
def readOnly[T](buffer: scala.collection.mutable.Buffer[T]): Seq[T] = buffer//.readOnly
|
||||
}
|
||||
|
||||
private[sbt] sealed abstract class Iterable[T] extends NotNull
|
||||
{
|
||||
def foreach(f: T => Unit) = toList.foreach(f)
|
||||
def toList: List[T]
|
||||
}
|
||||
private[sbt] sealed trait Removable[T] extends NotNull
|
||||
{
|
||||
def -=(t: T) : Unit
|
||||
def --=(all: Iterable[T]) { all.foreach(-=) }
|
||||
def --=(all: scala.Iterable[T]) { all.foreach(-=) }
|
||||
}
|
||||
private[sbt] sealed trait Addable[T] extends NotNull
|
||||
{
|
||||
def +=(t: T) : Unit
|
||||
def ++=(all: Iterable[T]) { all.foreach(+=) }
|
||||
def ++=(all: scala.Iterable[T]) { all.foreach(+=) }
|
||||
}
|
||||
private[sbt] sealed abstract class Set[T] extends Iterable[T]
|
||||
{
|
||||
def contains(t: T): Boolean
|
||||
}
|
||||
private[sbt] sealed class SetWrapper[T](val underlying: JSet[T]) extends Set[T]
|
||||
{
|
||||
def contains(t: T) = underlying.contains(t)
|
||||
def toList =Wrappers.toList(underlying.iterator)
|
||||
}
|
||||
private[sbt] final class MutableSetWrapper[T](wrapped: JSet[T]) extends SetWrapper[T](wrapped) with Addable[T] with Removable[T]
|
||||
{
|
||||
def +=(t: T) { underlying.add(t) }
|
||||
def -=(t: T) { underlying.remove(t) }
|
||||
def readOnly: Set[T] = this
|
||||
}
|
||||
private[sbt] sealed abstract class Map[K,V] extends Iterable[(K,V)]
|
||||
{
|
||||
def apply(key: K): V
|
||||
def get(key: K): Option[V]
|
||||
final def getOrElse[V2 >: V](key: K, default: => V2): V2 =
|
||||
get(key) match
|
||||
{
|
||||
case Some(value) => value
|
||||
case None => default
|
||||
}
|
||||
}
|
||||
private[sbt] sealed abstract class MapWrapper[K,V](val underlying: JMap[K,V]) extends Map[K,V]
|
||||
{
|
||||
final def apply(key: K) = underlying.get(key)
|
||||
final def get(key: K) =
|
||||
{
|
||||
val value = underlying.get(key)
|
||||
if(value == null)
|
||||
None
|
||||
else
|
||||
Some(value)
|
||||
}
|
||||
final def toList = Wrappers.toList(underlying)
|
||||
final def values = toList.map(_._2)
|
||||
}
|
||||
private[sbt] sealed class MutableMapWrapper[K,V](wrapped: JMap[K,V]) extends MapWrapper[K,V](wrapped) with Removable[K] with Addable[(K,V)]
|
||||
{
|
||||
final def getOrElseUpdate(key: K, default: => V): V =
|
||||
get(key) match
|
||||
{
|
||||
case Some(value) => value
|
||||
case None =>
|
||||
val newValue = default
|
||||
underlying.put(key, newValue)
|
||||
newValue
|
||||
}
|
||||
final def clear() = underlying.clear()
|
||||
final def update(key: K, value: V) { underlying.put(key, value) }
|
||||
final def +=(pair: (K, V) ) { update(pair._1, pair._2) }
|
||||
final def -=(key: K) { underlying.remove(key) }
|
||||
final def readOnly: Map[K,V] = this
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import sbt._
|
||||
|
||||
class TestProject(info: ProjectInfo) extends DefaultProject(info) with Marker
|
||||
{
|
||||
lazy val interactiveTest = interactiveTask { mark() }
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import sbt._
|
||||
|
||||
class TestProject(info: ProjectInfo) extends ParentProject(info) with Marker
|
||||
{
|
||||
val subA = project("a", "A")
|
||||
lazy val interactiveTest = interactiveTask { mark() }
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import sbt._
|
||||
|
||||
class TestProject(info: ProjectInfo) extends ParentProject(info)
|
||||
{
|
||||
val subA = project("a", "A", new SubA(_))
|
||||
class SubA(info: ProjectInfo) extends DefaultProject(info) with Marker
|
||||
{
|
||||
lazy val interactiveTest = interactiveTask { mark() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import sbt._
|
||||
|
||||
class TestProject(info: ProjectInfo) extends ParentProject(info) with Marker
|
||||
{
|
||||
val subA = project("a", "A", new SubA(_))
|
||||
lazy val interactiveTest = interactiveTask { mark() }
|
||||
|
||||
class SubA(info: ProjectInfo) extends DefaultProject(info)
|
||||
{
|
||||
lazy val interactiveTest = interactiveTask { Some("Child interactive task should not be called.") }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import sbt._
|
||||
|
||||
class TestProject(info: ProjectInfo) extends ParentProject(info)
|
||||
{
|
||||
val subA = project("a", "A", new SubA(_))
|
||||
lazy val interactiveTest = task { Some("Parent task should not be called") }
|
||||
|
||||
class SubA(info: ProjectInfo) extends DefaultProject(info)
|
||||
{
|
||||
lazy val interactiveTest = interactiveTask { Some("Child task should not be called.") }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import sbt._
|
||||
|
||||
class TestProject(info: ProjectInfo) extends ParentProject(info) with Marker
|
||||
{
|
||||
val subA = project("a", "A", new SubA(_))
|
||||
lazy val interactiveTest = task { mark() }
|
||||
|
||||
class SubA(info: ProjectInfo) extends DefaultProject(info) with Marker
|
||||
{
|
||||
lazy val interactiveTest = task { mark() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#Project properties
|
||||
#Fri Jan 30 20:49:57 EST 2009
|
||||
project.name=Interactive Actions Test
|
||||
project.version=1.0
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import sbt._
|
||||
|
||||
trait Marker extends NotNull
|
||||
{ self: Project =>
|
||||
def toMark: Path = "ran"
|
||||
def mark() =
|
||||
{
|
||||
if(toMark.exists)
|
||||
Some("Already ran")
|
||||
else
|
||||
FileUtilities.touch(toMark, log)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import sbt._
|
||||
|
||||
class TestProject(info: ProjectInfo) extends DefaultProject(info) with Marker
|
||||
{
|
||||
lazy val interactiveTest = task { mark() }
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# This test verifies the behavior of actions declared interactive
|
||||
|
||||
# Single project, non-interactive task
|
||||
> interactive-test [success]
|
||||
$ exists ran [success]
|
||||
$ delete ran [success]
|
||||
|
||||
# Single project, interactive task
|
||||
$ copy-file changes/TestProject2.scala project/build/src/TestProject.scala [success]
|
||||
$ reload [success]
|
||||
> interactive-test [success]
|
||||
$ exists ran [success]
|
||||
$ delete ran [success]
|
||||
|
||||
# Multi-project, single interactive task on parent project
|
||||
$ copy-file changes/TestProject3.scala project/build/src/TestProject.scala [success]
|
||||
$ reload [success]
|
||||
> interactive-test [success]
|
||||
$ exists ran [success]
|
||||
$ delete ran [success]
|
||||
|
||||
# Multi-project, single interactive task on child project
|
||||
$ copy-file changes/TestProject4.scala project/build/src/TestProject.scala [success]
|
||||
$ reload [success]
|
||||
> interactive-test [failure]
|
||||
|
||||
# Multi-project, two interactive tasks with same name, which is allowed because it is defined on parent
|
||||
$ copy-file changes/TestProject5.scala project/build/src/TestProject.scala [success]
|
||||
$ reload [success]
|
||||
> interactive-test [success]
|
||||
$ exists "ran" [success]
|
||||
$ delete "ran" [success]
|
||||
|
||||
# Multi-project, interactive on subproject + non-interactive on parent, which cannot be run from parent
|
||||
$ copy-file changes/TestProject6.scala project/build/src/TestProject.scala [success]
|
||||
$ reload [success]
|
||||
> interactive-test [failure]
|
||||
|
||||
# Multi-project, two non-interactive tasks with same name, which is allowed
|
||||
$ copy-file changes/TestProject7.scala project/build/src/TestProject.scala [success]
|
||||
$ reload [success]
|
||||
> interactive-test [success]
|
||||
$ exists "ran" [success]
|
||||
$ exists "a/ran" [success]
|
||||
$ delete "ran" [success]
|
||||
$ delete "a/ran" [success]
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
project.organization=test
|
||||
project.name=Interpreter Project Test
|
||||
project.version=1.0
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import sbt._
|
||||
|
||||
class TestProject(info: ProjectInfo) extends DefaultProject(info)
|
||||
{
|
||||
override def useMavenConfigurations = true
|
||||
val sc = "org.scala-tools.testing" % "scalacheck" % "1.5" % "test->default"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue