Creazione di un file jar da un file Scala
Domanda
Sono nuovo di Scala e non conosco Java. Voglio creare un file jar da un semplice file Scala. Quindi ho il mio HelloWorld.scala, genera un HelloWorld.jar.
manifest.mf:
Main-Class: HelloWorld
Nella console corro:
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)
Soluzione
Struttura della directory di esempio:
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
Per utilizzare correttamente l'opzione -jar , sono necessarie due voci nel file META-INF / MANIFEST.MF : la classe principale; URL relativi a eventuali dipendenze. Le note di documentazione:
-jar
Esegue un programma incapsulato in a File JAR. Il primo argomento è il nome di un file JAR invece di a nome della classe di avvio. Per questo opzione di lavoro, il manifest del Il file JAR deve contenere una riga del file form Main-Class: classname. Qui, classname identifica la classe che ha il vuoto statico pubblico principale (String [] args) che funge da tuo punto di partenza dell'applicazione. Vedi il Pagina di riferimento dello strumento Jar e Jar traccia del tutorial Java per informazioni su come lavorare con Jar file e manifest di file Jar.
Quando si utilizza questa opzione, il file JAR è la fonte di tutte le classi di utenti, e le altre impostazioni del percorso della classe utente vengono ignorate.
(Note: i file JAR possono essere controllati con la maggior parte delle applicazioni ZIP; probabilmente trascuro la gestione degli spazi nei nomi delle directory nello script batch; Runner code code versione 2.7.4.final.)
Per completezza, uno 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
Altri suggerimenti
Poiché gli script Scala richiedono l'installazione delle librerie Scala, dovrai includere il runtime Scala insieme al tuo JAR.
Esistono molte strategie per farlo, come jar jar , ma alla fine il problema stai vedendo è che il processo Java che hai avviato non riesce a trovare i JAR Scala.
Per un semplice script autonomo, consiglierei di usare jar jar, altrimenti dovresti iniziare a cercare uno strumento di gestione delle dipendenze o richiedere agli utenti di installare Scala in JDK .
Ho finito con sbt assembly , è davvero semplice da usare. Ho aggiunto un file chiamato assembly.sbt
nella directory project/
alla radice del progetto con un solo liner (nota che potrebbe essere necessario modificare la versione).
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.11.2")
Quindi esegui l'attività assembly
in sbt
:
> assembly
O semplicemente 'sbt assembly' nella directory root del progetto
$ sbt assembly
Prima eseguirà i test e quindi genererà il nuovo jar nella directory target/
(dato che il mio build.sbt
elenca già tutte le mie dipendenze).
Nel mio caso, rendo solo quel file eseguibile .jar
, rinomino per rimuovere l'estensione ed è pronto per la spedizione!
Inoltre, se stai utilizzando uno strumento da riga di comando, non dimenticare di aggiungere un pagina man (odio gli script senza le pagine man appropriate o con la documentazione di testo semplice multipagina che non è nemmeno inserita in un cercapersone per te).
Puoi anche usare maven e il plugin maven-scala. Una volta installato maven, puoi semplicemente fare il pacchetto mvn e questo creerà il tuo vaso per te.
Ho provato a riprodurre il metodo di MyDowell. Finalmente sono riuscito a farlo funzionare. Tuttavia trovo che la risposta sia corretta, sebbene un po 'troppo complicata per un principiante (in particolare la struttura della directory è inutilmente complicata).
Posso riprodurre questo risultato con mezzi molto semplicistici. Per cominciare c'è solo una directory che contiene tre file:
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
prima compilazione helloworld.scala:
scalac helloworld.scala
quindi crea il vaso:
\progra~1\java\jdk18~1.0_4\bin\jar -cfm helloworld.jar MANIFEST.MF .
ora puoi eseguirlo con:
java -jar helloworld.jar
Ho trovato questa soluzione semplice perché quella originale non funzionava. Più tardi ho scoperto che non perché è sbagliato, ma a causa di un banale errore: se non chiudo la seconda riga in MANIFEST.MF con una nuova riga, questa riga verrà ignorata. Mi ci è voluta un'ora per scoprirlo e prima ho provato tutte le altre cose, trovando questa soluzione molto semplice.
Non voglio scrivere perché e come è piuttosto mostrare la soluzione che ha funzionato nel mio caso (tramite la riga di comando di Ubuntu Linux):
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
Ho modificato lo script bash aggiungendo un po 'di intelligenza tra cui la generazione auto-manifest.
Questo script presuppone che l'oggetto principale abbia lo stesso nome del file in cui si trova (case sensitive). Inoltre, il nome della directory corrente deve essere uguale al nome dell'oggetto principale oppure il nome dell'oggetto principale deve essere fornito come parametro della riga di comando. Avvia questo script dalla directory principale del tuo progetto. Modifica le variabili in alto come richiesto.
Ricorda che lo script genererà le cartelle bin e dist e ELIMINERÀ tutti i contenuti esistenti nel 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 che può causare un problema simile (anche se non è il problema nella domanda iniziale sopra) è che Java vm sembra richiedere che il metodo principale restituisca void
. In Scala possiamo scrivere qualcosa del tipo ( osserva il segno = nella definizione di main ):
object MainProgram {
def main(args: Array[String]) = {
new GUI(args)
}
}
dove main restituisce effettivamente un oggetto GUI
(ovvero non è MainProgram
), ma il programma funzionerà bene quando lo avvieremo usando il comando scala.
Se impacchettiamo questo codice in un file jar, con Unit
come Main-Class, Java vm si lamenterà che non esiste alcuna funzione principale, poiché il tipo di ritorno del nostro main non è <=> (trovo questa lamentela è alquanto strana, poiché il tipo restituito non fa parte della firma).
Non avremmo problemi se tralasciassimo il segno = nell'intestazione di main o se lo dichiarassimo esplicitamente come <=>.
Se non si desidera utilizzare le strutture sbt, si consiglia l'uso di un makefile.
Ecco un esempio in cui il pacchetto foo è sostituito da foo.bar.myApp per completezza.
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