34

I have written a Java GUI using SWT. I package the application using an ANT script (fragment below).

<jar destfile="./build/jars/swtgui.jar" filesetmanifest="mergewithoutmain">
  <manifest>
    <attribute name="Main-Class" value="org.swtgui.MainGui" />
    <attribute name="Class-Path" value="." />
  </manifest>
  <fileset dir="./build/classes" includes="**/*.class" />
  <zipfileset excludes="META-INF/*.SF" src="lib/org.eclipse.swt.win32.win32.x86_3.5.2.v3557f.jar" />
</jar>

This produces a single jar which on Windows I can just double click to run my GUI. The downside is that I have had to explicitly package the windows SWT package into my jar.

I would like to be able to run my application on other platforms (primarily Linux and OS X). The simplest way to do it would be to create platform specific jars which packaged the appropriate SWT files into separate JARs.

Is there a better way to do this? Is it possible to create a single JAR which would run on multiple platforms?

mchr
  • 6,161
  • 6
  • 52
  • 74

5 Answers5

29

I've just run into the same problem. I haven't tried it yet, but I plan to include versions of swt.jar for all platforms and load the correct one dynamically in the start of the main method.

UPDATE: It worked. build.xml includes all jars:

<zipfileset dir="/home/aromanov/workspace/foo/lib" includes="swt_linux_gtk_x86.jar"/>
<zipfileset dir="/home/aromanov/workspace/foo/lib" includes="swt_macosx_x86.jar"/>
<zipfileset dir="/home/aromanov/workspace/foo/lib" includes="swt_win32_x86.jar"/>
<zipfileset dir="/home/aromanov/workspace/foo/lib" includes="swt_linux_gtk_x64.jar"/>
<zipfileset dir="/home/aromanov/workspace/foo/lib" includes="swt_macosx_x64.jar"/>
<zipfileset dir="/home/aromanov/workspace/foo/lib" includes="swt_win32_x64.jar"/>

and my main method starts with calling this:

private void loadSwtJar() {
    String osName = System.getProperty("os.name").toLowerCase();
    String osArch = System.getProperty("os.arch").toLowerCase();
    String swtFileNameOsPart = 
        osName.contains("win") ? "win32" :
        osName.contains("mac") ? "macosx" :
        osName.contains("linux") || osName.contains("nix") ? "linux_gtk" :
        ""; // throw new RuntimeException("Unknown OS name: "+osName)

    String swtFileNameArchPart = osArch.contains("64") ? "x64" : "x86";
    String swtFileName = "swt_"+swtFileNameOsPart+"_"+swtFileNameArchPart+".jar";

    try {
        URLClassLoader classLoader = (URLClassLoader) getClass().getClassLoader();
        Method addUrlMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
        addUrlMethod.setAccessible(true);

        URL swtFileUrl = new URL("rsrc:"+swtFileName); // I am using Jar-in-Jar class loader which understands this URL; adjust accordingly if you don't
        addUrlMethod.invoke(classLoader, swtFileUrl);
    }
    catch(Exception e) {
        throw new RuntimeException("Unable to add the SWT jar to the class path: "+swtFileName, e);
    }
}

[EDIT] For those looking for the "jar-in-jar classloader": It's included in Eclipse's JDT (the Java IDE built on Eclipse). Open org.eclipse.jdt.ui_*version_number*.jar with an archiver and you will find a file jar-in-jar-loader.zip inside.

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • Where can we find the "Jar-in-Jar" classloader? – Aaron Digulla Jul 23 '10 at 14:06
  • Or the latest version in your Eclipse install. – Alexey Romanov Jul 23 '10 at 15:13
  • How can I find the JAR in my Eclipse 3.6 install? I tried to figure out the name from the bug report but couldn't find anything that seemed to match from `org.eclipse.jdt.*` – Aaron Digulla Jul 26 '10 at 10:19
  • 1
    Open `org.eclipse.jdt.ui_*version_number*.jar` with an archiver and it should have the file `jar-in-jar-loader.zip` inside. (I have 3.5.1 installed here, but I expect 3.6 shouldn't be different.) – Alexey Romanov Jul 26 '10 at 13:29
  • Cool solution, exactly what I was looking for. However implementing this have proved to be tougher than expected. – posdef Oct 18 '10 at 11:44
  • 1
    i tried your code but i get this exception: java.net.MalformedURLException: unknown protocol: rsrc at java.net.URL.(URL.java:574) Done. ..can you help what the problem? – othman Aug 03 '11 at 00:45
  • @othman: You need to be using the jar-in-jar classloader (see the last sentence) – Alexey Romanov Aug 03 '11 at 04:25
  • @ Alexey can you look at my answer above and tell what's going wrong with my code? -thanks – othman Aug 04 '11 at 04:27
  • 2
    @Alexey - You may be interested to see that I have now packaged your solution into an ant task - http://mchr3k.github.com/swtjar/ – mchr Aug 20 '11 at 19:36
  • How is the jar structured in this solution? Does the main .jar have all the application classes, along with all the swt .jars? – Rahat Ahmed Feb 02 '12 at 23:25
  • @RahatAhmed Yes, in my original version. – Alexey Romanov Feb 03 '12 at 06:47
  • @AlexeyRomanov, each binary is around 5 Mb. For a Java client side having 30 extra MB is too much. Does this approach works for downloading a required binary from a webserver with keeping it for later use? – Nikolay Kuznetsov Oct 26 '12 at 16:59
  • @NikolayKuznetsov Yes, no reason it shouldn't. – Alexey Romanov Oct 27 '12 at 05:31
  • The issue with this approach is that it depends on the JVM the code is ran on. On 64 bits machines, you can run 32 bit JVM and it'll report 32 bit. When you try to load 32 swt jars you'll get a InvocationTargetException because you try to load a 32 bit swt on a 64 bit machine. This only works as long as your JVM matches your computer (i.e. 32 bit JVM on 32 bit computer). I have issues when users run a 32 bit JVM on a 64 bit machine. Still trying to figure out how to fix that. – javydreamercsw Jan 30 '13 at 17:08
  • @Krishna http://download.eclipse.org/eclipse/downloads/drops4/R-4.3-201306052000/#SWT – Alexey Romanov May 29 '14 at 11:13
  • @AlexeyRomanov i have already download that in that i am not able to find swt_macosx_x64.jar there i can find swt.jar and swt-debug.jar that i have to include in path? – Kishan Bheemajiyani May 29 '14 at 11:17
  • @Krishna You need to rename swt.jar from each download to names you'll use. – Alexey Romanov May 29 '14 at 11:18
  • @AlexeyRomanov rename into which name that u have maintain inthat i mean in your ans?? – Kishan Bheemajiyani May 29 '14 at 11:20
  • @Krishna Rename swt.jar from MacOS download into swt_macosx_x64.jar, swt.jar from Windows x64 download into swt_win32_x64.jar, etc. – Alexey Romanov May 29 '14 at 11:53
  • @AlexeyRomanov so will it makework if u rename and include in that? – Kishan Bheemajiyani May 29 '14 at 12:01
  • @javydreamercsw You can use `sun.arch.data.model` to get the JVM architecture. http://stackoverflow.com/questions/2062020/how-can-i-tell-if-im-running-in-64-bit-jvm-or-32-bit-jvm-from-within-a-program – Rossiar Sep 04 '15 at 10:47
19

I have a working implementation which is now referenced from the SWT FAQ.

This approach is now available to use as an ANT task: SWTJar

[EDIT] SWTJar has now been updated to use Alexey Romanov's solution as described above.

build.xml

First I build a jar containing all of my application classes.

<!-- UI (Stage 1) -->   
<jarjar jarfile="./build/tmp/intrace-ui-wrapper.jar">
  <fileset dir="./build/classes" includes="**/shared/*.class" />
  <fileset dir="./build/classes" includes="**/client/gui/**/*.class" />
  <zipfileset excludes="META-INF/*.MF" src="lib/miglayout-3.7.3.1-swt.jar"/>
</jarjar>

Next, I build a jar to contain all of the following:

  • JARs
    • The jar which I just built
    • All the SWT jars
  • Classes
    • The "Jar-In-Jar" classloader classes
    • A special loader class - see below

Here is the fragment from build.xml.

<!-- UI (Stage 2) -->
<jarjar jarfile="./build/jars/intrace-ui.jar">
  <manifest>
    <attribute name="Main-Class" value="org.intrace.client.loader.TraceClientLoader" />
    <attribute name="Class-Path" value="." />
  </manifest>
  <fileset dir="./build/classes" includes="**/client/loader/*.class" />
  <fileset dir="./build/tmp" includes="intrace-ui-wrapper.jar" />
  <fileset dir="./lib" includes="swt-*.jar" />
  <zipfileset excludes="META-INF/*.MF" src="lib/jar-in-jar-loader.jar"/>
</jarjar>

TraceClientLoader.java

This loader class uses the jar-in-jar-loader to create a ClassLoader which loads classes from two jars.

  • The correct SWT jar
  • The application wrapper jar

Once we have this classloader we can launch the actual application main method using reflection.

public class TraceClientLoader
{
  public static void main(String[] args) throws Throwable
  {    
    ClassLoader cl = getSWTClassloader();
    Thread.currentThread().setContextClassLoader(cl);    
    try
    {
      try
      {
        System.err.println("Launching InTrace UI ...");
        Class<?> c = Class.forName("org.intrace.client.gui.TraceClient", true, cl);
        Method main = c.getMethod("main", new Class[]{args.getClass()});
        main.invoke((Object)null, new Object[]{args});
      }
      catch (InvocationTargetException ex)
      {
        if (ex.getCause() instanceof UnsatisfiedLinkError)
        {
          System.err.println("Launch failed: (UnsatisfiedLinkError)");
          String arch = getArch();
          if ("32".equals(arch))
          {
            System.err.println("Try adding '-d64' to your command line arguments");
          }
          else if ("64".equals(arch))
          {
            System.err.println("Try adding '-d32' to your command line arguments");
          }
        }
        else
        {
          throw ex;
        }
      }
    }
    catch (ClassNotFoundException ex)
    {
      System.err.println("Launch failed: Failed to find main class - org.intrace.client.gui.TraceClient");
    }
    catch (NoSuchMethodException ex)
    {
      System.err.println("Launch failed: Failed to find main method");
    }
    catch (InvocationTargetException ex)
    {
      Throwable th = ex.getCause();
      if ((th.getMessage() != null) &&
          th.getMessage().toLowerCase().contains("invalid thread access"))
      {
        System.err.println("Launch failed: (SWTException: Invalid thread access)");
        System.err.println("Try adding '-XstartOnFirstThread' to your command line arguments");
      }
      else
      {
        throw th;
      }
    }
  }

  private static ClassLoader getSWTClassloader()
  {
    ClassLoader parent = TraceClientLoader.class.getClassLoader();    
    URL.setURLStreamHandlerFactory(new RsrcURLStreamHandlerFactory(parent));
    String swtFileName = getSwtJarName();      
    try
    {
      URL intraceFileUrl = new URL("rsrc:intrace-ui-wrapper.jar");
      URL swtFileUrl = new URL("rsrc:" + swtFileName);
      System.err.println("Using SWT Jar: " + swtFileName);
      ClassLoader cl = new URLClassLoader(new URL[] {intraceFileUrl, swtFileUrl}, parent);

      try
      {
        // Check we can now load the SWT class
        Class.forName("org.eclipse.swt.widgets.Layout", true, cl);
      }
      catch (ClassNotFoundException exx)
      {
        System.err.println("Launch failed: Failed to load SWT class from jar: " + swtFileName);
        throw new RuntimeException(exx);
      }

      return cl;
    }
    catch (MalformedURLException exx)
    {
      throw new RuntimeException(exx);
    }                   
  }

  private static String getSwtJarName()
  {
    // Detect OS
    String osName = System.getProperty("os.name").toLowerCase();    
    String swtFileNameOsPart = osName.contains("win") ? "win" : osName
        .contains("mac") ? "osx" : osName.contains("linux")
        || osName.contains("nix") ? "linux" : "";
    if ("".equals(swtFileNameOsPart))
    {
      throw new RuntimeException("Launch failed: Unknown OS name: " + osName);
    }

    // Detect 32bit vs 64 bit
    String swtFileNameArchPart = getArch();

    String swtFileName = "swt-" + swtFileNameOsPart + swtFileNameArchPart
        + "-3.6.2.jar";
    return swtFileName;
  }

  private static String getArch()
  {
    // Detect 32bit vs 64 bit
    String jvmArch = System.getProperty("os.arch").toLowerCase();
    String arch = (jvmArch.contains("64") ? "64" : "32");
    return arch;
  }
}

[EDIT] As stated above, for those looking for the "jar-in-jar classloader": It's included in Eclipse's JDT (the Java IDE built on Eclipse). Open org.eclipse.jdt.ui_*version_number*.jar with an archiver and you will find a file jar-in-jar-loader.zip inside. I renamed this to jar-in-jar-loader.jar.

intrace-ui.jar - this is the jar which I built using the process described above. You should be able to run this single jar on any of win32/64, linux32/64 and osx32/64.

[EDIT] This answer is now referenced from the SWT FAQ.

mchr
  • 6,161
  • 6
  • 52
  • 74
  • I couldn't find a way to contact you on your website, but I cannot get your swtjar task to work. I get the error `D:\My Dropbox\Java\kEllyIRClient\swtjar-build.xml:16: The archive swtjar.jar doesn't exist`. The taskdef has the right classpath path, because if you change it, it complains about that. I have no clue how to get this to work, and I'd much rather do it using your task, than manually implement the reflection stuff. – Rahat Ahmed Feb 02 '12 at 23:16
  • Can you post a link to your complete build.xml (or at least the target which you are having problem with?). Can you also give me details about the layout of your files/folders on disk? Let me know your email address and I will try and help you directly. – mchr Feb 03 '12 at 13:06
  • This is the question that I posted: http://stackoverflow.com/questions/9107509/unable-to-make-ant-script-with-swtjar/9112334#comment11467319_9112334 but the script has changed since then. My email is rahatarmanahmed@gmail.com – Rahat Ahmed Feb 03 '12 at 13:31
  • I've posted an answer on your SO question. – mchr Feb 03 '12 at 15:34
  • Sorry, can't get this work. I'm using step-by-step instructions from here: https://code.google.com/a/eclipselabs.org/p/timeme/wiki/SWT but I'm getting Exception in thread "main" java.lang.NoClassDefFoundError: org/eclipse/jface/window/ApplicationWindow – Alex Aug 16 '14 at 21:33
  • As I understand - my application doesn't see jface and other libraries. I have read somewhere that this is because SWTloader knows nothing about included libs. I can change manifest inside builded jar to run app bypassing SWTloader and app starting well, but my app can't see libs from manifest when it is started from SWTloder... – Alex Aug 16 '14 at 21:44
  • I downloaded the swtjar.jar but when I start it with java -XstartOnFirstThread -jar swtjar.jar I get an error stating "kein Hauptmanifestattribut, in swtjar.jar" (translates to "no main manifest attribute in swtjar.jar") – MetalSnake Apr 29 '15 at 09:48
10

if you are not looking to roll up everything into a single jar file and use jar-in-jar then you can also solve this problem by including named SWT jars for each target platform in your deployed application's lib directory:

lib/swt_win_32.jar
lib/swt_win_64.jar
lib/swt_linux_32.jar
lib/swt_linux_64.jar

and loading the correct one dynamically at runtime by inspecting the Java system properties "os.name" and "os.arch" at runtime using System.getProperty(String name) to create the correct jar filename.

You can then use a slightly naughty bit of reflection (OO purists look away now!) by invoking the normally protected method URLClassloader.addURL(URL url) to add the correct jar to the system classloader's classpath before the first SWT class is needed.

If you can stand the code-smell I've put a working example here http://www.chrisnewland.com/select-correct-swt-jar-for-your-os-and-jvm-at-runtime-191

ChrisWhoCodes
  • 630
  • 7
  • 10
1

It's very strange that all the answers here just advise to package all SWT JARs into a single giant application JAR file. IMHO, this is strictly against the purpose of SWT: there is an SWT library for each platform, so it is supposed to package only the appropriate SWT library for each platform. That's very easy to do, just define 5 build profiles in your ANT build: win32, win64, linux32, linux64 and mac64 (you can do mac32 as well, but all modern Macs are 64-bit).

Anyway, if you want to have good application integration into the OS, then you'll have to do some OS-specific things and you're with build profiles again. For desktop application it's inconvenient to have one app package for all platforms both for the developer and for his users.

afrish
  • 3,167
  • 4
  • 30
  • 38
0

Replace bold selected text in src="lib/org.eclipse.swt.win32.win32.x86_3.5.2.v3557f.jar" with linux-specified swt jar-file

  • This is the way which I was thinking of but this leaves me with platform specific jars. I'd love to be able to produce a single cross platform jar but I suspect that may not be possible. – mchr Apr 24 '10 at 23:23
  • Unfortunately, this is impossible – user10679754 Apr 25 '10 at 17:08
  • That's what I suspected. Oh well, I can live with one binary per platform - I still only have to write the code once :) (Although I know I need to test on all platforms) – mchr Apr 26 '10 at 16:34