How do you create a MANIFEST.MF that's available when you're testing and running from a jar in production?
Asked Answered
C

9

21

I've spent far too much time trying to figure this out. This should be the simplest thing and everyone who distributes Java applications in jars must have to deal with it.

I just want to know the proper way to add versioning to my Java app so that I can access the version information when I'm testing, e.g. debugging in Eclipse and running from a jar.

Here's what I have in my build.xml:

<target name="jar" depends = "compile">
    <property name="version.num" value="1.0.0"/>
    <buildnumber file="build.num"/>
    <tstamp>
        <format property="TODAY" pattern="yyyy-MM-dd HH:mm:ss" />
    </tstamp>

    <manifest file="${build}/META-INF/MANIFEST.MF">
        <attribute name="Built-By" value="${user.name}" />
        <attribute name="Built-Date" value="${TODAY}" />                   
        <attribute name="Implementation-Title" value="MyApp" />
        <attribute name="Implementation-Vendor" value="MyCompany" />                
        <attribute name="Implementation-Version" value="${version.num}-b${build.number}"/>                              
    </manifest>

    <jar destfile="${build}/myapp.jar" basedir="${build}" excludes="*.jar" />                   
</target>

This creates /META-INF/MANIFEST.MF and I can read the values when I'm debugging in Eclipse thusly:

public MyClass()
{
    try
    {                        
        InputStream stream = getClass().getResourceAsStream("/META-INF/MANIFEST.MF");
        Manifest manifest = new Manifest(stream);            

        Attributes attributes = manifest.getMainAttributes();

        String implementationTitle = attributes.getValue("Implementation-Title");
        String implementationVersion = attributes.getValue("Implementation-Version");
        String builtDate = attributes.getValue("Built-Date");
        String builtBy = attributes.getValue("Built-By");
   }
   catch (IOException e)
   {            
        logger.error("Couldn't read manifest.");
   }        

}

But, when I create the jar file, it loads the manifest of another jar (presumably the first jar loaded by the application - in my case, activation.jar).

Also, the following code doesn't work either although all the proper values are in the manifest file.

    Package thisPackage = getClass().getPackage();
    String implementationVersion = thisPackage.getImplementationVersion();

Any ideas?

Cornuted answered 17/9, 2008 at 15:28 Comment(0)
A
11

You can get the manifest for an arbitrary class in an arbitrary jar without parsing the class url (which could be brittle). Just locate a resource that you know is in the jar you want, and then cast the connection to JarURLConnection.

If you want the code to work when the class is not bundled in a jar, add an instanceof check on the type of URL connection returned. Classes in an unpacked class hierarchy will return a internal Sun FileURLConnection instead of the JarUrlConnection. Then you can load the Manifest using one of the InputStream methods described in other answers.

@Test
public void testManifest() throws IOException {
    URL res = org.junit.Assert.class.getResource(org.junit.Assert.class.getSimpleName() + ".class");
    JarURLConnection conn = (JarURLConnection) res.openConnection();
    Manifest mf = conn.getManifest();
    Attributes atts = mf.getMainAttributes();
    for (Object v : atts.values()) {
        System.out.println(v);
    }
}
Anglesite answered 18/6, 2010 at 16:55 Comment(3)
Nice, thanks. I tried everything here, and of the (currently 3) answers that acknowledge that Class.getResource() often finds the wrong manifest (in the wrong jar) and offer a solution to find the right manifest, I like this one best.Inexpert
If you want something specific use atts.getValue("Attribute-Name") or atts.get(new Attributes.Name("Attribute-Name")), non-generic code FTW!Egg
FYI, if used from within an IDE e.g. for testing, the type cast to JarURLConnection throws an exception with the bootstrap class loader. However, once the class is included in the built JAR, it works. Thanks.Marv
R
2

You want to use this:

Enumeration<URL> resources = Thread.currentThread().getContextClassLoader().getResources("META-INF/MANIFEST.MF");

You can parse the URL to figure out WHICH jar the manifest if from and then read the URL via getInputStream() to parse the manifest.

Rabat answered 18/9, 2008 at 16:44 Comment(1)
This is problematic if on the ContextClassLoader's classpath you have multiple sources with manifest files.Ogata
R
2

ClassLoader.getResource(String) will load the first manifest it finds on the classpath, which may be the manifest for some other JAR file. Thus, you can either enumerate all the manifests to find the one you want or use some other mechanism, such as a properties file with a unique name.

Rikki answered 5/3, 2009 at 16:6 Comment(1)
You're right that getResource() often finds the wrong manifest; I'm experiencing that; I tried your link to ClassLoader.getResources() to enumerate all manifests, but getResources("/META-INF/MANIFEST.MF") returns nothing, and getResources("") returns more than nothing but less than useful. I suspect I'm using the wrong class loader, but then "enumerate all manifests" devolves to "enumerate all ClassLoaders"!Inexpert
F
1

You can access the manifest (or any other) file within a jar if you use the same class loader to as was used to load the classes.

this.getClass().getClassLoader().getResourceAsStream( ... ) ;

If you are multi-threaded use the following:

Thread.currentThread().getContextClassLoader().getResourceAsStream( ... ) ;

This is also a realy useful technique for including a default configuration file within the jar.

Fumigate answered 17/9, 2008 at 16:17 Comment(0)
C
1

Here's what I've found that works:

packageVersion.java:

package com.company.division.project.packageversion;

import java.io.IOException;
import java.io.InputStream;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

public class packageVersion
{
    void printVersion()
    {
        try
        {         
            InputStream stream = getClass().getResourceAsStream("/META-INF/MANIFEST.MF");

            if (stream == null)
            {
                System.out.println("Couldn't find manifest.");
                System.exit(0);
            }

            Manifest manifest = new Manifest(stream);

            Attributes attributes = manifest.getMainAttributes();

            String impTitle = attributes.getValue("Implementation-Title");
            String impVersion = attributes.getValue("Implementation-Version");
            String impBuildDate = attributes.getValue("Built-Date");
            String impBuiltBy = attributes.getValue("Built-By");

            if (impTitle != null)
            {
                System.out.println("Implementation-Title:   " + impTitle);
            }            
            if (impVersion != null)
            {
                System.out.println("Implementation-Version: " + impVersion);
            }
            if (impBuildDate != null)
            {
                System.out.println("Built-Date: " + impBuildDate);
            }
            if (impBuiltBy != null)
            {
                System.out.println("Built-By:   " + impBuiltBy);
            }

            System.exit(0);
        }
        catch (IOException e)
        {            
            System.out.println("Couldn't read manifest.");
        }        
    }

    /**
     * @param args
     */
    public static void main(String[] args)
    {
        packageVersion version = new packageVersion();
        version.printVersion();        
    }

}

Here's the matching build.xml:

<project name="packageVersion" default="run" basedir=".">

    <property name="src" location="src"/>
    <property name="build" location="bin"/>
    <property name="dist" location="dist"/>

    <target name="init">
        <tstamp>
            <format property="TIMESTAMP" pattern="yyyy-MM-dd HH:mm:ss" />
        </tstamp>
        <mkdir dir="${build}"/>
        <mkdir dir="${build}/META-INF"/>
    </target>

    <target name="compile" depends="init">
        <javac debug="on" srcdir="${src}" destdir="${build}"/>
    </target>

    <target name="dist" depends = "compile">        
        <mkdir dir="${dist}"/>      
        <property name="version.num" value="1.0.0"/>
        <buildnumber file="build.num"/>
        <manifest file="${build}/META-INF/MANIFEST.MF">
            <attribute name="Built-By" value="${user.name}" />
            <attribute name="Built-Date" value="${TIMESTAMP}" />                                
            <attribute name="Implementation-Vendor" value="Company" />
            <attribute name="Implementation-Title" value="PackageVersion" />
            <attribute name="Implementation-Version" value="${version.num} (b${build.number})"/>
            <section name="com/company/division/project/packageversion">
                <attribute name="Sealed" value="false"/>
            </section>          
        </manifest>     
        <jar destfile="${dist}/packageversion-${version.num}.jar" basedir="${build}" manifest="${build}/META-INF/MANIFEST.MF"/>                 
    </target>

    <target name="clean">
        <delete dir="${build}"/>
        <delete dir="${dist}"/>
    </target>

    <target name="run" depends="dist">      
        <java classname="com.company.division.project.packageversion.packageVersion">
            <arg value="-h"/>
            <classpath>
                <pathelement location="${dist}/packageversion-${version.num}.jar"/>
                <pathelement path="${java.class.path}"/>
            </classpath>
        </java>
    </target>

</project>
Cornuted answered 29/9, 2008 at 16:28 Comment(2)
In a real-world application that uses any third-party libraries, this is exceedingly likely to return the wrong manifest.Corporal
Class name should begin in uppercase. It should be PackageVersion instead of packageVersion.Dibasic
M
1

I've found the comment by McDowell to be true - which MANIFEST.MF file gets picked up depends on the classpath and might not be the one wanted. I use this

String cp = PCAS.class.getResource(PCAS.class.getSimpleName() + ".class").toString();
cp = cp.substring(0, cp.indexOf(PCAS.class.getPackage().getName())) 
              +  "META-INF/MANIFEST.MF";
Manifest mf = new Manifest((new URL(cp)).openStream());

which I adapted from link text

Mclin answered 18/6, 2010 at 11:30 Comment(1)
This almost works for me, though the link to where you got this from is broken now. I say "almost" because Class.getPackage() returns a dot-separated name (org.foo.bar), and Class.getSimpleName() returns a slash-separated name (org/foo/bar). Because of this, I like gibbss' answer which avoids having to parse the class url.Inexpert
O
0

Just don't use the manifest. Create a foo.properties.original file, with a content such as version=@VERSION@

And in ther same task you are jaring you can do a copy to copu foo.properties.original and then

Omaomaha answered 17/9, 2008 at 15:38 Comment(0)
V
0

I will also usually use a version file. I will create one file per jar since each jar could have its own version.

Varix answered 17/9, 2008 at 16:11 Comment(0)
R
0

You can use a utility class Manifests from jcabi-manifests that automates finding and parsing of all MANIFEST.MF files available in classpath. Then, you read any attribute with a one liner:

final String name = Manifests.read("Build-By");
final String date = Manifests.read("Build-Date");

Also, check this out: http://www.yegor256.com/2014/07/03/how-to-read-manifest-mf.html

Raseda answered 30/12, 2012 at 9:2 Comment(1)
The solution shown above only works with a single manifest or at least unique attribute values across the manifest files. Just suppose the build date is contained in several JARs and as you say all of them are read. Which of the 'build date' attributes will be returned?Ogata

© 2022 - 2024 — McMap. All rights reserved.