Pimp your p2 profile

Pimp your p2 profile

Once is an oddity.  Twice is a trend…  Well, at least it starts looking like maybe there is a trend.  In the last while the need to setup custom p2 profiles has come up a couple times for me. What do I mean by that? Well, Profiles are the data structure that p2 uses to track the set of software installed to make an executable application. Your Eclipse IDE is a profile for example. Profiles contain Installable Units (IUs).  Each IU describes some thing that can be installed (bundle, feature, executable, command line argument, …) into a profile. The relationship between IUs is captured using provided and required capabilities and IUs themselves refer to artifacts (the actual bundles) and have touchpoint data used to configure the artifacts into a runtime.

OK, enough of the metadata lesson, what’s this all about? I had two problems.

  1. The need to ensure that certain IUs (e.g. bundles) were only installed in particular types of profiles
  2. The need to set dynamically determined properties and command line args for a profile

Both of these (and more) can be addressed by pimping your profile using the pimpProfile() method below.

private void pimpProfile(IProfile profile) throws CoreException {
    IInstallableUnit iu = createIU();
    Operand[] operands = new Operand[] {new InstallableUnitOperand(null, iu)};
    IEngine engine = getEngine();
    engine.perform(profile, new DefaultPhaseSet(), operands, null, null);
}

The code dynamically creates an IU and the proceeds to install that IU using the engine.  The engine is the p2 infrastructure that actually performs install/uninstall/update operations. The default engine is available as an OSGi service of type org.eclipse.equinox.internal.provisional.p2.engine.IEngine.  You should implement getEngine() using your favorite services coding pattern (hint:  the cool kids are using Declarative Services).

Normally you would use the p2 planner to make sure all the dependencies are satisfied and then modify the profile but here we know what we want to do — spoof up an IU and install it.

There are a few different kinds of operands but here we want to install an IU so the IU operand makes sense.  IU operands are old/new pairs.  That is, the first arg in the constructor is the IU currently in the profile, the second is what you want to be in the profile when the operation is finished.  So install is {null, iu}, uninstall is {iu, null} and update is {iu.1, iu.2} — the pair expresses the desired transition.

Now for the IU…  Depending on which problem above we are looking to solve we need a different IU.  Of course you can install any number of IUs but here let’s keep it simple and do these separately. In scenario 1 we are going to add a special capability to our pimped profile and arrange to have all our special IUs require this capability. The net effect being that the special IUs will only ever install into pimped profiles. Here we are going to pimp a server profile to prevent server function from accidentally being installed on clients. See bug 27600 for another example usecase.

In createIU() below we first create an IU descriptor and then spoof up a serverside capability. The name and namespace of the capability don’t actually matter as long as they are unique. Having created the capability we add it to the IU descriptor and instantiate and return the IU. That’s it — the profile now says it is capable of supporting serverside function.

private IInstallableUnit createIU() {
    InstallableUnitDescription iu = new MetadataFactory.InstallableUnitDescription();
    String id = "server-side";
    Version version = Version.createOSGi(0, 0, 0);
    iu.setId(id);
    iu.setVersion(version);
    ArrayList capabilities = new ArrayList();
    capabilities.add(MetadataFactory.createProvidedCapability(id, id, version));
    iu.addProvidedCapabilities(capabilities);
    return MetadataFactory.createInstallableUnit(iu);
}

Now to mark our server-side function. The key here is to make the server-side function require the server-side function. That is, make it require the server-side capability that we just added to the profile.

To do this, just create a p2.inf file as shown below and put it in the relevant server-side elements (root of features or META-INF dir of bundles). When the feature or bundle is published to a p2 repo, the indicated requirement is added to the corresponding IU. In practice you only need to annotate the top level IU that is being installed so that it fails to resolve on non-pimped profiles.

requires.1.namespace=server-side
requires.1.name=server-side
requires.1.range=[0.0.0,0.0.0]>

There is doc coming on p2.inf for the Galileo release but in the mean time see one of the original bugs.

Scenario 2 requires the setting of a dynamically determined property.  This came up in a system admin tool that created profiles and allowed the users to set some values that had to be presented as command line args. Here we use VM args just for fun. You can tweak program args, configuration properties, …

The createIU() method below is largely the same as the previous one but now rather than creating a capability we create some touchpoint data — specifically some action calls to add VM args. You can find out more about action on the p2 wiki.

private IInstallableUnit createIU() {
    InstallableUnitDescription iu = new MetadataFactory.InstallableUnitDescription();
    String time = Long.toString(System.currentTimeMillis());
    iu.setId("property.setter");
    iu.setVersion(Version.createOSGi(0, 0, 0, time));
    Map touchpointData = new HashMap();
    String id = getID();  // put your code here
    String data = "addJvmArg(jvmArg:-DmyProperty" + "=" + id + ");";
    touchpointData.put("configure", data);
    data = "removeJvmArg(jvmArg:-DmyProperty" + "=" + id + ");";
    touchpointData.put("unconfigure", data);
    iu.addTouchpointData(MetadataFactory.createTouchpointData(touchpointData));
    ITouchpointType touchpoint = MetadataFactory.createTouchpointType(
        "org.eclipse.equinox.p2.osgi", 
        new Version(1, 0, 0));
    iu.setTouchpointType(touchpoint);
    return MetadataFactory.createInstallableUnit(iu);
}

With this IU installed, the launched profile will have the given JVM arg set.

You can get similar effects by manually creating the relevant IUs, publishing them to a repo and then installing them like any other IU — but that’s boring and error prone. Using this approach you can create a custom installer that sets up all manner of things in the profiles it manages.

2 Comments