Crear un archivo jar a partir de un archivo Scala
Pregunta
Soy nuevo en Scala y no conozco Java. Quiero crear un archivo jar a partir de un archivo Scala simple. Entonces tengo mi HelloWorld.scala, generar un HelloWorld.jar.
Manifiesto.mf:
Main-Class: HelloWorld
En la consola ejecuto:
fsc HelloWorld.scala
jar -cvfm HelloWorld.jar Manifest.mf HelloWorld\$.class HelloWorld.class
java -jar HelloWorld.jar
=> "Exception in thread "main" java.lang.NoClassDefFoundError: HelloWorld/jar"
java -cp HelloWorld.jar HelloWorld
=> Exception in thread "main" java.lang.NoClassDefFoundError: scala/ScalaObject
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:675)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:124)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:260)
at java.net.URLClassLoader.access$100(URLClassLoader.java:56)
at java.net.URLClassLoader$1.run(URLClassLoader.java:195)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
at java.lang.ClassLoader.loadClass(ClassLoader.java:316)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:280)
at java.lang.ClassLoader.loadClass(ClassLoader.java:251)
at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:374)
at hoppity.main(HelloWorld.scala)
Solución
Estructura de directorio de muestra:
X:\scala\bin
X:\scala\build.bat
X:\scala\MANIFEST.MF
X:\scala\src
X:\scala\src\foo
X:\scala\src\foo\HelloWorld.scala
HelloWorld.scala:
//file: foo/HelloWorld.scala
package foo {
object HelloWorld {
def main(args: Array[String]) {
println("Hello, world!")
}
}
}
MANIFEST.MF:
Main-Class: foo.HelloWorld
Class-Path: scala-library.jar
build.bat:
@ECHO OFF
IF EXIST hellow.jar DEL hellow.jar
IF NOT EXIST scala-library.jar COPY %SCALA_HOME%\lib\scala-library.jar .
CALL scalac -sourcepath src -d bin src\foo\HelloWorld.scala
CD bin
jar -cfm ..\hellow.jar ..\MANIFEST.MF *.*
CD ..
java -jar hellow.jar
Para utilizar con éxito el modificador -jar , necesita dos entradas en el archivo META-INF / MANIFEST.MF : la clase principal; URL relativas a cualquier dependencia. Las notas de documentación:
-jar
Ejecuta un programa encapsulado en un Archivo JAR. El primer argumento es el nombre de un archivo JAR en lugar de un Nombre de la clase de inicio. Para esto opción de trabajar, el manifiesto de la El archivo JAR debe contener una línea de form Main-Class: nombre de clase. Aquí, nombre de clase identifica la clase que tiene el público estático vacío principal (Cadena [] método) que sirve como su punto de partida de la aplicación. Ver el Página de referencia de la herramienta Jar y Jar rastro del tutorial de Java para información sobre trabajar con Jar archivos y manifiestos de archivo Jar.
Cuando usa esta opción, el archivo JAR es la fuente de todas las clases de usuarios, y otras configuraciones de ruta de clase de usuario se ignoran.
(Notas: los archivos JAR se pueden inspeccionar con la mayoría de las aplicaciones ZIP; probablemente descuido el manejo de espacios en los nombres de directorio en el script por lotes; Scala code runner versión 2.7.4.final.)
Para completar, un script bash equivalente:
#!/bin/bash
if [ ! $SCALA_HOME ]
then
echo ERROR: set a SCALA_HOME environment variable
exit
fi
if [ ! -f scala-library.jar ]
then
cp $SCALA_HOME/lib/scala-library.jar .
fi
scalac -sourcepath src -d bin src/foo/HelloWorld.scala
cd bin
jar -cfm ../hellow.jar ../MANIFEST.MF *
cd ..
java -jar hellow.jar
Otros consejos
Debido a que los scripts de Scala requieren que se instalen las bibliotecas de Scala, deberá incluir el tiempo de ejecución de Scala junto con su JAR.
Hay muchas estrategias para hacerlo, como jar jar , pero en última instancia el problema lo que está viendo es que el proceso Java que ha comenzado no puede encontrar los JAR de Scala.
Para una secuencia de comandos independiente simple, recomendaría usar jar jar, de lo contrario, debe comenzar a buscar una herramienta de administración de dependencias o solicitar a los usuarios que instalen Scala en el JDK .
Terminé usando sbt assembly , es realmente fácil de usar. Agregué un archivo llamado assembly.sbt
en el directorio project/
en la raíz del proyecto con una sola línea (tenga en cuenta que es posible que deba cambiar su versión).
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.11.2")
Luego simplemente ejecute la tarea assembly
en sbt
:
> assembly
O simplemente 'sbt assembly' en el directorio raíz del proyecto
$ sbt assembly
Primero ejecutará sus pruebas y luego generará el nuevo jar en el directorio target/
(dado que mi build.sbt
ya enumera todas mis dependencias).
En mi caso, solo hago que el archivo .jar
sea ejecutable, cambie el nombre para eliminar la extensión y ¡está listo para enviar!
Además, si está utilizando una herramienta de línea de comandos, no olvide agregar un página de manual (odio las secuencias de comandos sin páginas de manual adecuadas o con documentación de texto plano de varias páginas que ni siquiera está en un buscapersonas para usted).
También puede usar maven y el complemento maven-scala. Una vez que configure maven, puede hacer mvn package y creará su jar para usted.
Traté de reproducir el método MyDowell. Finalmente pude hacerlo funcionar. Sin embargo, encuentro que la respuesta, aunque correcta, es demasiado complicada para un novato (en particular, la estructura del directorio es innecesariamente complicada).
Puedo reproducir este resultado con medios muy simplistas. Para empezar, solo hay un directorio que contiene tres archivos:
helloworld.scala
MANIFEST.MF
scala-library.jar
helloworld.scala
object HelloWorld
{
def main(args: Array[String])
{
println("Hello, world!")
}
}
MANIFEST.MF:
Main-Class: HelloWorld
Class-Path: scala-library.jar
primera compilación helloworld.scala:
scalac helloworld.scala
luego crea el jar:
\progra~1\java\jdk18~1.0_4\bin\jar -cfm helloworld.jar MANIFEST.MF .
ahora puedes ejecutarlo con:
java -jar helloworld.jar
Encontré esta solución simple porque la original no funcionaba. Más tarde descubrí que no porque esté mal, sino por un error trivial: si no cierro la segunda línea en MANIFEST.MF con una nueva línea, entonces esta línea será ignorada. Esto me llevó una hora descubrirlo y probé todas las demás cosas antes, en el proceso de encontrar esta solución muy simple.
No quiero escribir por qué y cómo, sino mostrar la solución que funcionó en mi caso (a través de la línea de comandos de Linux Ubuntu):
1)
mkdir scala-jar-example
cd scala-jar-example
2)
nano Hello.scala
object Hello extends App { println("Hello, world") }
3)
nano build.sbt
import AssemblyKeys._
assemblySettings
name := "MyProject"
version := "1.0"
scalaVersion := "2.11.0"
3)
mkdir project
cd project
nano plugins.sbt
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.9.1")
4)
cd ../
sbt assembly
5)
java -jar target/target/scala-2.11/MyProject-assembly-1.0.jar
>> Hello, world
Modifiqué el script bash agregando algo de inteligencia, incluida la generación de auto-manifiesto.
Este script supone que el objeto principal se llama igual que el archivo en el que se encuentra (distingue entre mayúsculas y minúsculas). Además, el nombre del directorio actual debe ser igual al nombre del objeto principal o el nombre del objeto principal debe proporcionarse como un parámetro de línea de comando. Inicie este script desde el directorio raíz de su proyecto. Modifique las variables en la parte superior según sea necesario.
Tenga en cuenta que el script generará las carpetas bin y dist y BORRARÁ cualquier contenido existente en bin.
#!/bin/bash
SC_DIST_PATH=dist
SC_SRC_PATH=src
SC_BIN_PATH=bin
SC_INCLUDE_LIB_JAR=scala-library.jar
SC_MANIFEST_PATH=MANIFEST.MF
SC_STARTING_PATH=$(pwd)
if [[ ! $SCALA_HOME ]] ; then
echo "ERROR: set a SCALA_HOME environment variable"
exit 1
fi
if [[ ! -f $SCALA_HOME/lib/$SC_INCLUDE_LIB_JAR ]] ; then
echo "ERROR: Cannot find Scala Libraries!"
exit 1
fi
if [[ -z "$1" ]] ; then
SC_APP=$(basename $SC_STARTING_PATH)
else
SC_APP=$1
fi
[[ ! -d $SC_DIST_PATH ]] && mkdir $SC_DIST_PATH
if [[ ! -d $SC_BIN_PATH ]] ; then
mkdir "$SC_BIN_PATH"
else
rm -r "$SC_BIN_PATH"
if [[ -d $SC_BIN_PATH ]] ; then
echo "ERROR: Cannot remove temp compile directory: $SC_BIN_PATH"
exit 1
fi
mkdir "$SC_BIN_PATH"
fi
if [[ ! -d $SC_SRC_PATH ]] || [[ ! -d $SC_DIST_PATH ]] || [[ ! -d $SC_BIN_PATH ]] ; then
echo "ERROR: Directory not found!: $SC_SRC_PATH or $SC_DIST_PATH or $SC_BIN_PATH"
exit 1
fi
if [[ ! -f $SC_DIST_PATH/$SC_INCLUDE_LIB_JAR ]] ; then
cp "$SCALA_HOME/lib/$SC_INCLUDE_LIB_JAR" "$SC_DIST_PATH"
fi
SCALA_MAIN=$(find ./$SC_SRC_PATH -name "$SC_APP.scala")
COMPILE_STATUS=$?
SCALA_MAIN_COUNT=$(echo "$SCALA_MAIN" | wc -l)
if [[ $SCALA_MAIN_COUNT != "1" ]] || [[ ! $COMPILE_STATUS == 0 ]] ; then
echo "Main source file not found or too many exist!: $SC_APP.scala"
exit 1
fi
if [[ -f $SC_DIST_PATH/$SC_APP.jar ]] ; then
rm "$SC_DIST_PATH/$SC_APP.jar"
if [[ -f $SC_DIST_PATH/$SC_APP.jar ]] ; then
echo "Unable to remove existing distribution!: $SC_DIST_PATH/$SC_APP.jar"
exit 1
fi
fi
if [[ ! -f $SC_MANIFEST_PATH ]] ; then
LEN_BASE=$(echo $(( $(echo "./$SC_SRC_PATH" |wc -c) - 0 )))
SC_MAIN_CLASS=$(echo $SCALA_MAIN |cut --complement -c1-$LEN_BASE)
SC_MAIN_CLASS=${SC_MAIN_CLASS%%.*}
SC_MAIN_CLASS=$(echo $SC_MAIN_CLASS |awk '{gsub( "/", "'"."'"); print}')
echo $(echo "Main-Class: "$SC_MAIN_CLASS) > $SC_MANIFEST_PATH
echo $(echo "Class-Path: "$SC_INCLUDE_LIB_JAR) >> $SC_MANIFEST_PATH
fi
scalac -sourcepath $SC_SRC_PATH -d $SC_BIN_PATH $SCALA_MAIN
COMPILE_STATUS=$?
if [[ $COMPILE_STATUS != "0" ]] ; then
echo "Compile Failed!"
exit 1
fi
cd "$SC_BIN_PATH"
jar -cfm ../$SC_DIST_PATH/$SC_APP.jar ../$SC_MANIFEST_PATH *
COMPILE_STATUS=$?
cd "$SC_STARTING_PATH"
if [[ $COMPILE_STATUS != "0" ]] || [[ ! -f $SC_DIST_PATH/$SC_APP.jar ]] ; then
echo "JAR Build Failed!"
exit 1
fi
echo " "
echo "BUILD COMPLETE!... TO LAUNCH: java -jar $SC_DIST_PATH/$SC_APP.jar"
echo " "
Una cosa que puede causar un problema similar (aunque no es el problema en la pregunta inicial anterior) es que Java vm parece exigir que el método principal devuelva void
. En Scala podemos escribir algo como ( observar el = -sign en la definición de main ):
object MainProgram {
def main(args: Array[String]) = {
new GUI(args)
}
}
donde main realmente devuelve un objeto GUI
(es decir, no es MainProgram
), pero el programa se ejecutará bien cuando lo iniciemos usando el comando scala.
Si empaquetamos este código en un archivo jar, con Unit
como Main-Class, Java vm se quejará de que no hay una función principal, ya que el tipo de retorno de nuestro main no es <=> (encuentro esta queja es algo extraña, ya que el tipo de retorno no es parte de la firma).
No tendríamos problemas si omitiéramos el = -sign en el encabezado de main, o si lo declaramos explícitamente como <=>.
Si no desea utilizar las instalaciones de sbt, le recomiendo el uso de un archivo make.
Aquí hay un ejemplo donde el paquete foo se reemplaza por foo.bar.myApp para completarlo.
makefile
NAME=HelloWorld
JARNAME=helloworld
PACKAGE=foo.bar.myApp
PATHPACK=$(subst .,/,$(PACKAGE))
.DUMMY: default
default: $(NAME)
.DUMMY: help
help:
@echo "make [$(NAME)]"
@echo "make [jar|runJar]"
@echo "make [clean|distClean|cleanAllJars|cleanScalaJar|cleanAppJar]"
.PRECIOUS: bin/$(PATHPACK)/%.class
bin/$(PATHPACK)/%.class: src/$(PATHPACK)/%.scala
scalac -sourcepath src -d bin $<
scala-library.jar:
cp $(SCALA_HOME)/lib/scala-library.jar .
.DUMMY: runjar
runJar: jar
java -jar $(JARNAME).jar
.DUMMY: jar
jar: $(JARNAME).jar
MANIFEST.MF:
@echo "Main-Class: $(PACKAGE).$(NAME)" > $@
@echo "Class-Path: scala-library.jar" >> $@
$(JARNAME).jar: scala-library.jar bin/$(PATHPACK)/$(NAME).class \
MANIFEST.MF
(cd bin && jar -cfm ../$(JARNAME).jar ../MANIFEST.MF *)
%: bin/$(PATHPACK)/%.class
scala -cp bin $(PACKAGE).$@
.DUMMY: clean
clean:
rm -R -f bin/* MANIFEST.MF
cleanAppJar:
rm -f $(JARNAME).jar
cleanScalaJar:
rm -f scala-library.jar
cleanAllJars: cleanAppJar cleanScalaJar
distClean cleanDist: clean cleanAllJars