FELIX-1262: add local Bnd source to apply temporary patches

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@793527 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/CircularDependencyException.java b/bundleplugin/src/main/java/aQute/bnd/build/CircularDependencyException.java
new file mode 100644
index 0000000..f08255b
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/CircularDependencyException.java
@@ -0,0 +1,10 @@
+package aQute.bnd.build;
+
+public class CircularDependencyException extends Exception {
+    public CircularDependencyException(String string) {
+        super(string);
+    }
+
+    private static final long serialVersionUID = 1L;
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/Container.java b/bundleplugin/src/main/java/aQute/bnd/build/Container.java
new file mode 100644
index 0000000..97a9eb1
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/Container.java
@@ -0,0 +1,131 @@
+package aQute.bnd.build;
+
+import java.io.*;
+import java.util.*;
+
+public class Container {
+    public enum TYPE {
+        REPO, PROJECT, EXTERNAL, LIBRARY, ERROR
+    }
+
+    final File                file;
+    final TYPE                type;
+    final String              bsn;
+    final String              version;
+    final String              error;
+    final Project             project;
+    final Map<String, String> attributes;
+
+    Container(Project project, String bsn, String version, TYPE type,
+            File source, String error, Map<String, String> attributes) {
+        this.bsn = bsn;
+        this.version = version;
+        this.type = type;
+        this.file = source != null ? source : new File("/" + bsn + ":"
+                + version + ":" + type);
+        this.project = project;
+        this.error = error;
+        if (attributes == null || attributes.isEmpty())
+            this.attributes = Collections.emptyMap();
+        else
+            this.attributes = attributes;
+    }
+
+    public Container(Project project, File file) {
+        this(project, file.getName(), "project", TYPE.PROJECT, file, null, null);
+    }
+
+    public Container(File file) {
+        this(null, file.getName(), "project", TYPE.EXTERNAL, file, null, null);
+    }
+
+    public File getFile() {
+        return file;
+    }
+
+    public String getBundleSymbolicName() {
+        return bsn;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public TYPE getType() {
+        return type;
+    }
+
+    public String getError() {
+        return error;
+    }
+
+    public boolean equals(Object other) {
+        if (other instanceof Container)
+            return file.equals(((Container) other).file);
+        else
+            return false;
+    }
+
+    public int hashCode() {
+        return file.hashCode();
+    }
+
+    public Project getProject() {
+        return project;
+    }
+
+    /**
+     * Must show the file name or the error formatted as a file name
+     * 
+     * @return
+     */
+    public String toString() {
+        if (getError() != null)
+            return "/error/" + getError();
+        else
+            return getFile().getAbsolutePath();
+    }
+
+    public Map<String, String> getAttributes() {
+        return attributes;
+    }
+
+    /**
+     * Return the this if this is anything else but a library. If it is a
+     * library, return the members. This could work recursively, e.g., libraries
+     * can point to libraries.
+     * 
+     * @return
+     * @throws Exception
+     */
+    public List<Container> getMembers() throws Exception {
+        List<Container> result = project.newList();
+
+        // Are ww a library? If no, we are the result
+        if (getType() != TYPE.LIBRARY)
+            result.add(this);
+        else {
+            // We are a library, parse the file. This is
+            // basically a specification clause per line.
+            // I.e. you can do bsn; version, bsn2; version. But also
+            // spread it out over lines.
+            InputStream in = new FileInputStream(file);
+            BufferedReader rd = new BufferedReader(new InputStreamReader(in,
+                    "UTF-8"));
+            try {
+                String line;
+                while ((line = rd.readLine()) != null) {
+                    line = line.trim();
+                    if (!line.startsWith("#") && line.length() > 0) {
+                        List<Container> list = project.getBundles(
+                                Workspace.STRATEGY_EXACT, line);
+                        result.addAll(list);
+                    }
+                }
+            } finally {
+                in.close();
+            }
+        }
+        return result;
+    }
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/Project.java b/bundleplugin/src/main/java/aQute/bnd/build/Project.java
new file mode 100644
index 0000000..61c0a27
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/Project.java
@@ -0,0 +1,1067 @@
+package aQute.bnd.build;
+
+import java.io.*;
+import java.util.*;
+import java.util.jar.*;
+
+import aQute.bnd.help.*;
+import aQute.bnd.service.*;
+import aQute.bnd.service.action.*;
+import aQute.bnd.test.*;
+import aQute.lib.osgi.*;
+import aQute.lib.osgi.eclipse.*;
+import aQute.libg.sed.*;
+import aQute.service.scripting.*;
+
+/**
+ * This class is NOT threadsafe
+ * 
+ * @author aqute
+ * 
+ */
+public class Project extends Processor {
+
+    final static String         DEFAULT_ACTIONS = "build; label='Build', test; label='Test', clean; label='Clean', release; label='Release', refreshAll; label=Refresh";
+    public final static String  BNDFILE         = "bnd.bnd";
+    public final static String  BNDCNF          = "cnf";
+    final Workspace             workspace;
+    long                        time;
+    int                         count;
+    boolean                     preparedPaths;
+    final Collection<Project>   dependson       = new LinkedHashSet<Project>();
+    final Collection<Container> buildpath       = new LinkedHashSet<Container>();
+    final Collection<Container> runpath         = new LinkedHashSet<Container>();
+    final Collection<File>      sourcepath      = new LinkedHashSet<File>();
+    final Collection<File>      allsourcepath   = new LinkedHashSet<File>();
+    final Collection<Container> bootclasspath   = new LinkedHashSet<Container>();
+    final Collection<Container> runbundles      = new LinkedHashSet<Container>();
+    File                        output;
+    File                        target;
+    boolean                     inPrepare;
+
+    public Project(Workspace workspace, File projectDir, File buildFile)
+            throws Exception {
+        super(workspace);
+        this.workspace = workspace;
+        setFileMustExist(false);
+        setProperties(buildFile);
+        assert workspace != null;
+        // For backward compatibility reasons, we also read
+        readBuildProperties();
+    }
+
+    public Project(Workspace workspace, File buildDir) throws Exception {
+        this(workspace, buildDir, new File(buildDir, BNDFILE));
+    }
+
+    private void readBuildProperties() throws Exception {
+        try {
+            File f = getFile("build.properties");
+            if (f.isFile()) {
+                Properties p = loadProperties(f);
+                for (Enumeration<?> e = p.propertyNames(); e.hasMoreElements();) {
+                    String key = (String) e.nextElement();
+                    String newkey = key;
+                    if (key.indexOf('$') >= 0) {
+                        newkey = getReplacer().process(key);
+                    }
+                    setProperty(newkey, p.getProperty(key));
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public static Project getUnparented(File propertiesFile) throws Exception {
+        propertiesFile = propertiesFile.getAbsoluteFile();
+        Workspace workspace = new Workspace(propertiesFile.getParentFile());
+        Project project = new Project(workspace, propertiesFile.getParentFile());
+        project.setProperties(propertiesFile);
+        project.setFileMustExist(true);
+        return project;
+    }
+
+    public synchronized boolean isValid() {
+        return getBase().isDirectory() && getPropertiesFile().isFile();
+    }
+
+    /**
+     * Return a new builder that is nicely setup for this project. Please close
+     * this builder after use.
+     * 
+     * @param parent
+     *            The project builder to use as parent, use this project if null
+     * @return
+     * @throws Exception
+     */
+    public synchronized ProjectBuilder getBuilder(ProjectBuilder parent)
+            throws Exception {
+
+        ProjectBuilder builder;
+
+        if (parent == null)
+            builder = new ProjectBuilder(this);
+        else
+            builder = new ProjectBuilder(parent);
+
+        builder.setBase(getBase());
+
+        for (Container file : getBuildpath()) {
+            builder.addClasspath(file.getFile());
+        }
+
+        for (Container file : getBootclasspath()) {
+            builder.addClasspath(file.getFile());
+        }
+
+        for (File file : getAllsourcepath()) {
+            builder.addSourcepath(file);
+        }
+        return builder;
+    }
+
+    public synchronized int getChanged() {
+        return count;
+    }
+
+    public synchronized void setChanged() {
+        // if (refresh()) {
+        preparedPaths = false;
+        count++;
+        // }
+    }
+
+    public Workspace getWorkspace() {
+        return workspace;
+    }
+
+    public String toString() {
+        return getBase().getName();
+    }
+
+    /**
+     * Set up all the paths
+     */
+
+    public synchronized void prepare() throws Exception {
+        if (inPrepare)
+            throw new CircularDependencyException(toString());
+        if (!preparedPaths) {
+            inPrepare = true;
+            try {
+                dependson.clear();
+                buildpath.clear();
+                sourcepath.clear();
+                allsourcepath.clear();
+                bootclasspath.clear();
+                runpath.clear();
+                runbundles.clear();
+
+                // We use a builder to construct all the properties for
+                // use.
+                setProperty("basedir", getBase().getAbsolutePath());
+
+                // If a bnd.bnd file exists, we read it.
+                // Otherwise, we just do the build properties.
+                if (!getPropertiesFile().isFile()
+                        && new File(getBase(), ".classpath").isFile()) {
+                    // Get our Eclipse info, we might depend on other projects
+                    // though ideally this should become empty and void
+                    doEclipseClasspath();
+                }
+
+                // Calculate our source directory
+
+                File src = new File(getBase(), getProperty("src", "src"));
+                if (src.isDirectory()) {
+                    sourcepath.add(src);
+                    allsourcepath.add(src);
+                } else
+                    sourcepath.add(getBase());
+
+                // Set default bin directory
+                output = getFile(getProperty("bin", "bin")).getAbsoluteFile();
+                if (output.isDirectory()) {
+                    if (!buildpath.contains(output))
+                        buildpath.add(new Container(this, output));
+                } else {
+                    if (!output.exists())
+                        output.mkdirs();
+                    if (!output.isDirectory())
+                        error("Can not find output directory: " + output);
+                }
+
+                // Where we store all our generated stuff.
+                target = getFile(getProperty("target", "generated"));
+
+                // We might have some other projects we want build
+                // before we do anything, but these projects are not in
+                // our path. The -dependson allows you to build them before.
+
+                List<Project> dependencies = new ArrayList<Project>();
+
+                String dp = getProperty(Constants.DEPENDSON);
+                Set<String> requiredProjectNames = parseHeader(dp).keySet();
+                for (String p : requiredProjectNames) {
+                    Project required = getWorkspace().getProject(p);
+                    if (required == null)
+                        error("No such project " + required + " on "
+                                + Constants.DEPENDSON);
+                    else {
+                        dependencies.add(required);
+                    }
+
+                }
+
+                // We have two paths that consists of repo files, projects,
+                // or some other stuff. The doPath routine adds them to the
+                // path and extracts the projects so we can build them before.
+
+                doPath(buildpath, dependencies, parseBuildpath(), bootclasspath);
+                doPath(runpath, dependencies, parseTestpath(), bootclasspath);
+                doPath(runbundles, dependencies, parseTestbundles(), null);
+
+                // We now know all dependend projects. But we also depend
+                // on whatever those projects depend on. This creates an
+                // ordered list without any duplicates. This of course assumes
+                // that there is no circularity. However, this is checked
+                // by the inPrepare flag, will throw an exception if we
+                // are circular.
+
+                Set<Project> done = new HashSet<Project>();
+                done.add(this);
+                allsourcepath.addAll(sourcepath);
+
+                for (Project project : dependencies)
+                    project.traverse(dependson, done);
+
+                for (Project project : dependson) {
+                    allsourcepath.addAll(project.getSourcepath());
+                }
+                if (isOk())
+                    preparedPaths = true;
+            } finally {
+                inPrepare = false;
+            }
+        }
+    }
+
+    private void traverse(Collection<Project> dependencies, Set<Project> visited)
+            throws Exception {
+        if (visited.contains(this))
+            return;
+
+        visited.add(this);
+
+        for (Project project : getDependson())
+            project.traverse(dependencies, visited);
+
+        dependencies.add(this);
+    }
+
+    /**
+     * Iterate over the entries and place the projects on the projects list and
+     * all the files of the entries on the resultpath.
+     * 
+     * @param resultpath
+     *            The list that gets all the files
+     * @param projects
+     *            The list that gets any projects that are entries
+     * @param entries
+     *            The input list of classpath entries
+     */
+    private void doPath(Collection<Container> resultpath,
+            Collection<Project> projects, Collection<Container> entries,
+            Collection<Container> bootclasspath) {
+        for (Container cpe : entries) {
+            if (cpe.getError() != null)
+                error(cpe.getError());
+            else {
+                if (cpe.getType() == Container.TYPE.PROJECT) {
+                    projects.add(cpe.getProject());
+                }
+                if (bootclasspath != null
+                        && cpe.getBundleSymbolicName().startsWith("ee.")
+                        || cpe.getAttributes().containsKey("boot"))
+                    bootclasspath.add(cpe);
+                else
+                    resultpath.add(cpe);
+            }
+        }
+    }
+
+    /**
+     * Parse the list of bundles that are a prerequisite to this project.
+     * 
+     * Bundles are listed in repo specific names. So we just let our repo
+     * plugins iterate over the list of bundles and we get the highest version
+     * from them.
+     * 
+     * @return
+     */
+
+    private List<Container> parseBuildpath() throws Exception {
+        return getBundles(Constants.STRATEGY_LOWEST,
+                getProperty(Constants.BUILDPATH));
+    }
+
+    private List<Container> parseTestpath() throws Exception {
+        return getBundles(Constants.STRATEGY_HIGHEST,
+                getProperty(Constants.RUNPATH));
+    }
+
+    private List<Container> parseTestbundles() throws Exception {
+        return getBundles(Constants.STRATEGY_HIGHEST,
+                getProperty(Constants.RUNBUNDLES));
+    }
+
+    /**
+     * Analyze the header and return a list of files that should be on the
+     * build, test or some other path. The list is assumed to be a list of bsns
+     * with a version specification. The special case of version=project
+     * indicates there is a project in the same workspace. The path to the
+     * output directory is calculated. The default directory ${bin} can be
+     * overridden with the output attribute.
+     * 
+     * @param strategy
+     *            STRATEGY_LOWEST or STRATEGY_HIGHEST
+     * @param spec
+     *            The header
+     * @return
+     */
+    public List<Container> getBundles(int strategy, String spec)
+            throws Exception {
+        List<Container> result = new ArrayList<Container>();
+        Map<String, Map<String, String>> bundles = parseHeader(spec);
+
+        try {
+            for (Iterator<Map.Entry<String, Map<String, String>>> i = bundles
+                    .entrySet().iterator(); i.hasNext();) {
+                Map.Entry<String, Map<String, String>> entry = i.next();
+                String bsn = entry.getKey();
+                Map<String, String> attrs = entry.getValue();
+
+                Container found = null;
+
+                String versionRange = attrs.get("version");
+
+                if (versionRange != null && versionRange.equals("project")) {
+                    Project project = getWorkspace().getProject(bsn);
+                    if (project.exists()) {
+                        File f = project.getOutput();
+                        found = new Container(project, bsn, "project",
+                                Container.TYPE.PROJECT, f, null, attrs);
+                    } else {
+                        error(
+                                "Reference to project that does not exist in workspace\n"
+                                        + "  Project       %s\n"
+                                        + "  Specification %s", bsn, spec);
+                        continue;
+                    }
+                } else if (versionRange != null && versionRange.equals("file")) {
+                    File f = getFile(bsn);
+                    String error = null;
+                    if (!f.exists())
+                        error = "File does not exist";
+                    if (f.getName().endsWith(".lib")) {
+                        found = new Container(this, bsn, "file",
+                                Container.TYPE.LIBRARY, f, error, attrs);
+                    } else {
+                        found = new Container(this, bsn, "file",
+                                Container.TYPE.EXTERNAL, f, error, attrs);
+                    }
+                } else {
+                    found = getBundle(bsn, versionRange, strategy, attrs);
+                }
+
+                if (found != null) {
+                    List<Container> libs = found.getMembers();
+                    for (Container cc : libs) {
+                        if (result.contains(cc))
+                            warning("Multiple bundles with the same final URL: "
+                                    + cc);
+
+                        result.add(cc);
+                    }
+                } else {
+                    // Oops, not a bundle in sight :-(
+                    Container x = new Container(this, bsn, versionRange,
+                            Container.TYPE.ERROR, null, bsn + ";version="
+                                    + versionRange + " not found", attrs);
+                    result.add(x);
+                    warning("Can not find URL for bsn " + bsn);
+                }
+            }
+        } catch (Exception e) {
+            error("While tring to get the bundles from " + spec, e);
+            e.printStackTrace();
+        }
+        return result;
+    }
+
+    public long getTime() {
+        return time;
+    }
+
+    public Collection<Project> getDependson() throws Exception {
+        prepare();
+        return dependson;
+    }
+
+    public Collection<Container> getBuildpath() throws Exception {
+        prepare();
+        return buildpath;
+    }
+
+    public Collection<Container> getRunpath() throws Exception {
+        prepare();
+        return runpath;
+    }
+
+    public Collection<Container> getRunbundles() throws Exception {
+        prepare();
+        return runbundles;
+    }
+
+    public Collection<File> getSourcepath() throws Exception {
+        prepare();
+        return sourcepath;
+    }
+
+    public Collection<File> getAllsourcepath() throws Exception {
+        prepare();
+        return allsourcepath;
+    }
+
+    public Collection<Container> getBootclasspath() throws Exception {
+        prepare();
+        return bootclasspath;
+    }
+
+    public File getOutput() throws Exception {
+        prepare();
+        return output;
+    }
+
+    private void doEclipseClasspath() throws Exception {
+        EclipseClasspath eclipse = new EclipseClasspath(this, getWorkspace()
+                .getBase(), getBase());
+        eclipse.setRecurse(false);
+
+        // We get the file directories but in this case we need
+        // to tell ant that the project names
+        for (File dependent : eclipse.getDependents()) {
+            Project required = workspace.getProject(dependent.getName());
+            dependson.add(required);
+        }
+        for (File f : eclipse.getClasspath()) {
+            buildpath.add(new Container(f));
+        }
+        for (File f : eclipse.getBootclasspath()) {
+            bootclasspath.add(new Container(f));
+        }
+        sourcepath.addAll(eclipse.getSourcepath());
+        allsourcepath.addAll(eclipse.getAllSources());
+        output = eclipse.getOutput();
+    }
+
+    public String _p_dependson(String args[]) throws Exception {
+        return list(args, toFiles(getDependson()));
+    }
+
+    private Collection<?> toFiles(Collection<Project> projects) {
+        List<File> files = new ArrayList<File>();
+        for (Project p : projects) {
+            files.add(p.getBase());
+        }
+        return files;
+    }
+
+    public String _p_buildpath(String args[]) throws Exception {
+        return list(args, getBuildpath());
+    }
+
+    public String _p_testpath(String args[]) throws Exception {
+        return list(args, getRunpath());
+    }
+
+    public String _p_sourcepath(String args[]) throws Exception {
+        return list(args, getSourcepath());
+    }
+
+    public String _p_allsourcepath(String args[]) throws Exception {
+        return list(args, getAllsourcepath());
+    }
+
+    public String _p_bootclasspath(String args[]) throws Exception {
+        return list(args, getBootclasspath());
+    }
+
+    public String _p_output(String args[]) throws Exception {
+        if (args.length != 1)
+            throw new IllegalArgumentException(
+                    "${output} should not have arguments");
+        return getOutput().getAbsolutePath();
+    }
+
+    private String list(String[] args, Collection<?> list) {
+        if (args.length > 3)
+            throw new IllegalArgumentException(
+                    "${"
+                            + args[0]
+                            + "[;<separator>]} can only take a separator as argument, has "
+                            + Arrays.toString(args));
+
+        String separator = ",";
+        if (args.length == 2) {
+            separator = args[1];
+        }
+
+        return join(list, separator);
+    }
+
+    protected Object[] getMacroDomains() {
+        return new Object[] { workspace };
+    }
+
+    public File release(Jar jar) throws Exception {
+    	String name = getProperty(Constants.RELEASEREPO);
+    	return release(name, jar);
+    }
+
+    /**
+     * Release
+     * @param name The repository name
+     * @param jar
+     * @return
+     * @throws Exception
+     */
+    public File release(String name, Jar jar) throws Exception {
+        List<RepositoryPlugin> plugins = getPlugins(RepositoryPlugin.class);
+        RepositoryPlugin rp = null;
+        for (RepositoryPlugin plugin : plugins) {
+        	if (!plugin.canWrite()) {
+        		continue;
+        	}
+            if (name == null) {
+        		rp = plugin;
+        		break;
+            } else if (name.equals(plugin.getName())){
+        		rp = plugin;
+        		break;
+            }
+        }
+
+        if (rp != null) {
+            try {
+                return rp.put(jar);
+            } catch (Exception e) {
+                error("Deploying " + jar.getName() + " on " + rp.getName(), e);
+            } finally {
+                jar.close();
+            }
+        }
+        return null;
+   	
+    }
+    
+    public void release(boolean test) throws Exception {
+    	String name = getProperty(Constants.RELEASEREPO);
+    	release(name, test);
+    }
+    
+    /**
+     * Release
+     * @param name The respository name
+     * @param test Run testcases
+     * @throws Exception
+     */
+    public void release(String name, boolean test) throws Exception {
+        File[] jars = build(test);
+        // If build fails jars will be null
+        if (jars == null) {
+        	return;
+        }
+        for (File jar : jars) {
+            Jar j = new Jar(jar);
+            release(name, j);
+            j.close();
+        }
+    	
+    }
+    
+    /**
+     * Get a bundle from one of the plugin repositories.
+     * 
+     * @param bsn
+     *            The bundle symbolic name
+     * @param range
+     *            The version range
+     * @param lowest
+     *            set to LOWEST or HIGHEST
+     * @return the file object that points to the bundle or null if not found
+     * @throws Exception
+     *             when something goes wrong
+     */
+    public Container getBundle(String bsn, String range, int strategy,
+            Map<String, String> attrs) throws Exception {
+        List<RepositoryPlugin> plugins = getPlugins(RepositoryPlugin.class);
+
+        // If someone really wants the latest, lets give it to them.
+        // regardless of they asked for a lowest strategy
+        if (range != null && range.equals("latest"))
+            strategy = STRATEGY_HIGHEST;
+
+        for (RepositoryPlugin plugin : plugins) {
+            File[] results = plugin.get(bsn, range);
+            if (results != null && results.length > 0) {
+                File f = results[strategy == STRATEGY_LOWEST ? 0
+                        : results.length - 1];
+
+                if (f.getName().endsWith("lib"))
+                    return new Container(this, bsn, range,
+                            Container.TYPE.LIBRARY, f, null, attrs);
+                else
+                    return new Container(this, bsn, range, Container.TYPE.REPO,
+                            f, null, attrs);
+            }
+        }
+
+        return new Container(this, bsn, range, Container.TYPE.ERROR, null, bsn
+                + ";version=" + range + " Not found in " + plugins, null);
+    }
+
+    /**
+     * Deploy the file (which must be a bundle) into the repository.
+     * 
+     * @param name The repository name
+     * @param file
+     *            bundle
+     */
+    public void deploy(String name, File file) throws Exception {
+        List<RepositoryPlugin> plugins = getPlugins(RepositoryPlugin.class);
+        RepositoryPlugin rp = null;
+        for (RepositoryPlugin plugin : plugins) {
+        	if (!plugin.canWrite()) {
+        		continue;
+        	}
+            if (name == null) {
+        		rp = plugin;
+        		break;
+            } else if (name.equals(plugin.getName())){
+        		rp = plugin;
+        		break;
+            }
+        }
+
+        if (rp != null) {
+            Jar jar = new Jar(file);
+            try {
+                rp.put(jar);
+                return;
+            } catch (Exception e) {
+                error("Deploying " + file + " on " + rp.getName(), e);
+            } finally {
+                jar.close();
+            }
+            return;
+        }
+        trace("No repo found " + file);
+        throw new IllegalArgumentException("No repository found for " + file);
+    }
+
+    /**
+     * Deploy the file (which must be a bundle) into the repository.
+     * 
+     * @param file
+     *            bundle
+     */
+    public void deploy(File file) throws Exception {
+    	String name = getProperty(Constants.DEPLOYREPO);
+    	deploy(name, file);
+    }
+    /**
+     * Macro access to the repository
+     * 
+     * ${repo;<bsn>[;<version>[;<low|high>]]}
+     */
+
+    public String _repo(String args[]) throws Exception {
+        if (args.length < 2)
+            throw new IllegalArgumentException(
+                    "Too few arguments for repo, syntax=: ${repo ';'<bsn> [ ; <version> ]}");
+
+        String bsns = args[1];
+        String version = null;
+        int strategy = Constants.STRATEGY_HIGHEST;
+
+        if (args.length > 2) {
+            version = args[2];
+            if (args.length == 4) {
+                if (args[3].equalsIgnoreCase("HIGHEST"))
+                    strategy = Constants.STRATEGY_HIGHEST;
+                else if (args[3].equalsIgnoreCase("LOWEST"))
+                    strategy = STRATEGY_LOWEST;
+                else
+                    error("${repo;<bsn>;<version>;<'highest'|'lowest'>} macro requires a strategy of 'highest' or 'lowest', and is "
+                            + args[3]);
+            }
+        }
+
+        Collection<String> parts = split(bsns);
+        List<String> paths = new ArrayList<String>();
+
+        for (String bsn : parts) {
+            Container jar = getBundle(bsn, version, strategy, null);
+            if (jar.getError() == null) {
+                paths.add(jar.getFile().getAbsolutePath());
+            } else {
+                error("The ${repo} macro could not find " + bsn
+                        + " in the repo, because " + jar.getError() + "\n"
+                        + "Repositories     : "
+                        + getPlugins(RepositoryPlugin.class) + "\n"
+                        + "Strategy         : " + strategy + "\n"
+                        + "Bsn              : " + bsn + ";version=" + version);
+            }
+        }
+        return join(paths);
+    }
+
+    public File getTarget() throws Exception {
+        prepare();
+        return target;
+    }
+
+    public File[] build(boolean underTest) throws Exception {
+        ProjectBuilder builder = getBuilder(null);
+        if (underTest)
+            builder.setProperty(Constants.UNDERTEST, "true");
+        Jar jars[] = builder.builds();
+        File files[] = new File[jars.length];
+
+        File target = getTarget();
+        target.mkdirs();
+
+        for (int i = 0; i < jars.length; i++) {
+            Jar jar = jars[i];
+            try {
+                String bsn = jar.getName();
+                files[i] = new File(target, bsn + ".jar");
+                String msg = "";
+                if (!files[i].exists()
+                        || files[i].lastModified() < jar.lastModified()) {
+                    reportNewer(files[i].lastModified(), jar);
+                    files[i].delete();
+                    jar.write(files[i]);
+                } else {
+                    msg = "(not modified since "
+                            + new Date(files[i].lastModified()) + ")";
+                }
+                trace(jar.getName() + " (" + files[i].getName() + ") "
+                        + jar.getResources().size() + " " + msg);
+            } finally {
+                jar.close();
+            }
+        }
+        getInfo(builder);
+        builder.close();
+        if (isOk())
+            return files;
+        else
+            return null;
+    }
+
+    private void reportNewer(long lastModified, Jar jar) {
+        if (isTrue(getProperty(Constants.REPORTNEWER))) {
+            StringBuilder sb = new StringBuilder();
+            String del = "Newer than " + new Date(lastModified);
+            for (Map.Entry<String, Resource> entry : jar.getResources()
+                    .entrySet()) {
+                if (entry.getValue().lastModified() > lastModified) {
+                    sb.append(del);
+                    del = ", \n     ";
+                    sb.append(entry.getKey());
+                }
+            }
+            if (sb.length() > 0)
+                warning(sb.toString());
+        }
+    }
+
+    /**
+     * Refresh if we are based on stale data. This also implies our workspace.
+     */
+    public boolean refresh() {
+        boolean changed = false;
+        if (isCnf()) {
+            changed = workspace.refresh();
+        }
+        return super.refresh() || changed;
+    }
+
+    public boolean isCnf() {
+        return getBase().getName().equals(Workspace.CNFDIR);
+    }
+
+    public void propertiesChanged() {
+        super.propertiesChanged();
+        preparedPaths = false;
+    }
+
+    public String getName() {
+        return getBase().getName();
+    }
+
+    public Map<String, Action> getActions() {
+        Map<String, Action> all = newMap();
+        Map<String, Action> actions = newMap();
+        fillActions(all);
+        getWorkspace().fillActions(all);
+
+        for (Map.Entry<String, Action> action : all.entrySet()) {
+            String key = getReplacer().process(action.getKey());
+            if (key != null && key.trim().length() != 0)
+                actions.put(key, action.getValue());
+        }
+        return actions;
+    }
+
+    public void fillActions(Map<String, Action> all) {
+        Map<String, Map<String, String>> actions = parseHeader(getProperty(
+                "-actions", DEFAULT_ACTIONS));
+        for (Map.Entry<String, Map<String, String>> entry : actions.entrySet()) {
+            String key = Processor.removeDuplicateMarker(entry.getKey());
+            Action action;
+
+            if (entry.getValue().get("script") != null) {
+                // TODO check for the type
+                action = new ScriptAction(entry.getValue().get("type"), entry
+                        .getValue().get("script"));
+            } else {
+                action = new ReflectAction(key);
+            }
+            String label = entry.getValue().get("label");
+            all.put(label, action);
+        }
+    }
+
+    public void release() throws Exception {
+        release(false);
+    }
+
+    /**
+     * Release.
+     * @param name The repository name
+     * @throws Exception
+     */
+    public void release(String name) throws Exception {
+        release(name, false);
+    }
+
+    public void clean() throws Exception {
+        File target = getTarget();
+        if (target.isDirectory() && target.getParentFile() != null) {
+            delete(target);
+        }
+    }
+
+    public File[] build() throws Exception {
+        return build(false);
+    }
+
+    public boolean test() throws Exception {
+        boolean ok = true;
+        String testbundles = getProperty(TESTBUNDLES);
+
+        if (testbundles == null) {
+            File jars[] = build(true);
+            for (File jar : jars)
+                ok &= test(jar);
+
+        } else {
+            List<Container> containers = getBundles(STRATEGY_HIGHEST,
+                    testbundles);
+            for (Container container : containers) {
+                if (container.getError() == null) {
+                    File jar = container.getFile();
+                    ok &= test(jar);
+                } else
+                    error(container.getError());
+            }
+        }
+        return ok;
+    }
+
+    public boolean test(File f) throws Exception {
+        ProjectLauncher pl = new ProjectLauncher(this);
+        pl.setReport(getProperty("target") + "/" + f.getName().replace(".jar", ".xml"));
+        int errors = pl.run(f);
+        getInfo(pl);
+        if (errors == 0) {
+            trace("ok");
+            return true;
+        } else {
+            error("Failed: " + normalize(f) + ", " + errors + " test"
+                    + (errors > 1 ? "s" : "") + " failures, see "
+                    + normalize(pl.getTestreport()));
+            return false;
+        }
+    }
+
+    private void delete(File target) {
+        if (target.getParentFile() == null)
+            throw new IllegalArgumentException("Can not delete root!");
+        if (!target.exists())
+            return;
+
+        if (target.isDirectory()) {
+            File sub[] = target.listFiles();
+            for (File s : sub)
+                delete(s);
+        }
+        target.delete();
+    }
+
+    /**
+     * This methods attempts to turn any jar into a valid jar. If this is a
+     * bundle with manifest, a manifest is added based on defaults. If it is a
+     * bundle, but not r4, we try to add the r4 headers.
+     * 
+     * @param name
+     * @param in
+     * @return
+     * @throws Exception
+     */
+    public Jar getValidJar(File f) throws Exception {
+        Jar jar = new Jar(f);
+        Manifest manifest = jar.getManifest();
+        if (manifest == null) {
+            trace("Wrapping with all defaults");
+            Builder b = new Builder(this);
+            b.addClasspath(jar);
+            b.setProperty("Bnd-Message", "Wrapped from " + f.getAbsolutePath()
+                    + "because lacked manifest");
+            b.setProperty(Constants.EXPORT_PACKAGE, "*");
+            b.setProperty(Constants.IMPORT_PACKAGE, "*;resolution:=optional");
+            jar = b.build();
+        } else if (manifest.getMainAttributes().getValue(
+                Constants.BUNDLE_MANIFESTVERSION) == null) {
+            trace("Not a release 4 bundle, wrapping with manifest as source");
+            Builder b = new Builder(this);
+            b.addClasspath(jar);
+            b.setProperty(Constants.PRIVATE_PACKAGE, "*");
+            b.mergeManifest(manifest);
+            String imprts = manifest.getMainAttributes().getValue(
+                    Constants.IMPORT_PACKAGE);
+            if (imprts == null)
+                imprts = "";
+            else
+                imprts += ",";
+            imprts += "*;resolution=optional";
+
+            b.setProperty(Constants.IMPORT_PACKAGE, imprts);
+            b.setProperty("Bnd-Message", "Wrapped from " + f.getAbsolutePath()
+                    + "because had incomplete manifest");
+            jar = b.build();
+        }
+        return jar;
+    }
+
+    public String _project(String args[]) {
+        return getBase().getAbsolutePath();
+    }
+
+    public void bump(String mask) throws IOException {
+        Sed sed = new Sed(getReplacer(), getPropertiesFile());
+        sed
+                .replace(
+                        "(Bundle-Version\\s*(:|=)\\s*)(([0-9]+(\\.[0-9]+(\\.[0-9]+)?)?))",
+                        "$1${version;" + mask + ";$3}");
+        sed.doIt();
+        refresh();
+    }
+
+    public void bump() throws IOException {
+        bump(getProperty(BUMPPOLICY, "=+0"));
+    }
+
+    public void action(String command) throws Exception {
+        Action a = new ReflectAction(command);
+        a.execute(this, command);
+    }
+
+    public String _findfile(String args[]) {
+        File f = getFile(args[1]);
+        List<String> files = new ArrayList<String>();
+        tree(files, f, "", Instruction.getPattern(args[2]));
+        return join(files);
+    }
+
+    void tree(List<String> list, File current, String path, Instruction instr) {
+        if (path.length() > 0)
+            path = path + "/";
+
+        String subs[] = current.list();
+        for (String sub : subs) {
+            File f = new File(current, sub);
+            if (f.isFile()) {
+                if (instr.matches(sub) && !instr.isNegated())
+                    list.add(path + sub);
+            } else
+                tree(list, f, path + sub, instr);
+        }
+    }
+
+    public void refreshAll() {
+        workspace.refresh();
+        refresh();
+    }
+
+    @SuppressWarnings("unchecked")
+    public void script(String type, String script) throws Exception {
+        // TODO check tyiping
+        List<Scripter> scripters = getPlugins(Scripter.class);
+        if (scripters.isEmpty()) {
+            error(
+                    "Can not execute script because there are no scripters registered: %s",
+                    script);
+            return;
+        }
+        Map x = (Map) getProperties();
+        scripters.get(0)
+                .eval((Map<String, Object>) x, new StringReader(script));
+    }
+    
+    public String _repos(String args[]) throws Exception {
+        List<RepositoryPlugin> repos = getPlugins(RepositoryPlugin.class);
+        List<String> names = new ArrayList<String>();
+        for ( RepositoryPlugin rp : repos )
+            names.add(rp.getName());
+        return join(names,", ");        
+    }
+    
+    public String _help(String args[]) throws Exception {
+        if ( args.length == 1)
+            return "Specify the option or header you want information for";
+        
+        Syntax syntax = Syntax.HELP.get(args[1]);
+        if  (syntax == null )
+            return "No help for " + args[1];
+        
+        String what = null;
+        if ( args.length> 2)
+            what = args[2];
+     
+        if ( what == null || what.equals("lead"))
+            return syntax.getLead();
+        if ( what == null || what.equals("example"))
+            return syntax.getExample();
+        if ( what == null || what.equals("pattern"))
+            return syntax.getPattern();
+        if ( what == null || what.equals("values"))
+            return syntax.getValues();
+        
+        return "Invalid type specified for help: lead, example, pattern, values";
+    }
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/ProjectBuilder.java b/bundleplugin/src/main/java/aQute/bnd/build/ProjectBuilder.java
new file mode 100644
index 0000000..9289b8b
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/ProjectBuilder.java
@@ -0,0 +1,34 @@
+package aQute.bnd.build;
+
+import aQute.lib.osgi.*;
+
+@SuppressWarnings("unchecked")
+public class ProjectBuilder extends Builder {
+    Project project;
+
+    public ProjectBuilder(Project project) {
+        super(project);
+        this.project = project;
+    }
+
+    public ProjectBuilder(ProjectBuilder builder) {
+        super(builder);
+        this.project = builder.project;
+    }
+
+
+    /** 
+     * We put our project and our workspace on the macro path.
+     */
+    protected Object [] getMacroDomains() {
+        return new Object[] {project, project.getWorkspace()};
+    }
+
+    public Builder getSubBuilder() throws Exception {
+        return project.getBuilder(this);
+    }
+
+    public Project getProject() {
+        return project;
+    }
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/ReflectAction.java b/bundleplugin/src/main/java/aQute/bnd/build/ReflectAction.java
new file mode 100644
index 0000000..55c2861
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/ReflectAction.java
@@ -0,0 +1,22 @@
+package aQute.bnd.build;
+
+import java.lang.reflect.*;
+
+import aQute.bnd.service.action.*;
+
+public class ReflectAction implements Action {
+    String  what;
+    
+    public ReflectAction(String what) {
+        this.what = what;
+    }
+    
+    public void execute(Project project, String action) throws Exception {
+        Method m = project.getClass().getMethod(what);
+        m.invoke(project);
+    }
+
+    public String toString() {
+        return "ra:" + what;
+    }
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/ScriptAction.java b/bundleplugin/src/main/java/aQute/bnd/build/ScriptAction.java
new file mode 100644
index 0000000..8ca55fd
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/ScriptAction.java
@@ -0,0 +1,18 @@
+package aQute.bnd.build;
+
+import aQute.bnd.service.action.*;
+
+public class ScriptAction implements Action {
+    final String script;
+    final String type;
+    
+    public ScriptAction(String type, String script) {
+        this.script = script;
+        this.type = type;
+    }
+
+    public void execute(Project project, String action) throws Exception {
+        project.script(type, script);
+    }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/Workspace.java b/bundleplugin/src/main/java/aQute/bnd/build/Workspace.java
new file mode 100644
index 0000000..b607492
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/Workspace.java
@@ -0,0 +1,109 @@
+package aQute.bnd.build;
+
+import java.io.*;
+import java.lang.ref.*;
+import java.util.*;
+
+import aQute.bnd.service.action.*;
+import aQute.lib.osgi.*;
+
+public class Workspace extends Processor {
+    public final static int                    STRATEGY_HIGHEST = 1;
+    public final static int                    STRATEGY_EXACT   = 0;
+    public final static int                    STRATEGY_LOWEST  = -1;
+    public static final String                 BUILDFILE        = "build.bnd";
+    public static final String                 CNFDIR           = "cnf";
+
+    static Map<File, WeakReference<Workspace>> cache            = newHashMap();
+    final Map<String, Project>                 models           = newHashMap();
+    final Map<String, Action>                  commands         = newMap();
+
+    /**
+     * This static method finds the workspace and creates a project (or returns
+     * an existing project)
+     * 
+     * @param projectDir
+     * @return
+     */
+    public static Project getProject(File projectDir) throws Exception {
+        projectDir = projectDir.getAbsoluteFile();
+        assert projectDir.isDirectory();
+
+        Workspace ws = getWorkspace(projectDir.getParentFile());
+        return ws.getProject(projectDir.getName());
+    }
+
+    public static Workspace getWorkspace(File workspaceDir) throws Exception {
+        workspaceDir = workspaceDir.getAbsoluteFile();
+        synchronized (cache) {
+            WeakReference<Workspace> wsr = cache.get(workspaceDir);
+            Workspace ws;
+            if (wsr == null || (ws = wsr.get()) == null) {
+                ws = new Workspace(workspaceDir);
+                cache.put(workspaceDir, new WeakReference<Workspace>(ws));
+            }
+            return ws;
+        }
+    }
+
+    public Workspace(File dir) throws Exception {
+        dir = dir.getAbsoluteFile();
+        dir.mkdirs();
+        assert dir.isDirectory();
+
+        File buildDir = new File(dir, CNFDIR).getAbsoluteFile();
+        File buildFile = new File(buildDir, BUILDFILE).getAbsoluteFile();
+        if (!buildFile.isFile())
+            warning("No Build File in " + dir);
+        setProperties(buildFile, dir);
+    }
+
+    public Project getProject(String bsn) throws Exception {
+        synchronized (models) {
+            Project project = models.get(bsn);
+            if (project != null)
+                return project;
+
+            File projectDir = getFile(bsn);
+            project = new Project(this, projectDir);
+            models.put(bsn, project);
+            return project;
+        }
+    }
+
+    public boolean isPresent(String name) {
+        return models.containsKey(name);
+    }
+
+    public Collection<Project> getCurrentProjects() {
+        return models.values();
+    }
+
+    public boolean refresh() {
+        if (super.refresh()) {
+            for (Project project : getCurrentProjects()) {
+                project.propertiesChanged();
+            }
+            return true;
+        }
+        return false;
+    }
+
+    public String _workspace(String args[]) {
+        return getBase().getAbsolutePath();
+    }
+
+    public void addCommand(String menu, Action action) {
+        commands.put(menu, action);
+    }
+
+    public void removeCommand(String menu) {
+        commands.remove(menu);
+    }
+
+    public void fillActions(Map<String, Action> all) {
+        all.putAll(commands);
+    }
+    
+    
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/help/Syntax.java b/bundleplugin/src/main/java/aQute/bnd/help/Syntax.java
new file mode 100644
index 0000000..8dde3da
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/help/Syntax.java
@@ -0,0 +1,482 @@
+package aQute.bnd.help;
+
+import java.util.*;
+import java.util.regex.*;
+
+import aQute.lib.osgi.*;
+
+public class Syntax implements Constants {
+    final String                            header;
+    final String                            lead;
+    final String                            example;
+    final Pattern                           pattern;
+    final String                            values;
+    final Syntax[]                          children;
+
+    static Syntax                           version              = new Syntax(
+                                                                         VERSION_ATTRIBUTE,
+                                                                         "A version range to select the version of an export definition. The default value is 0.0.0 .",
+                                                                         "version=\"[1.2,3.0)\"",
+                                                                         null,
+                                                                         Verifier.VERSIONRANGE);
+    static Syntax                           bundle_symbolic_name = new Syntax(
+                                                                         BUNDLE_SYMBOLIC_NAME_ATTRIBUTE,
+                                                                         "The bundle symbolic name of the exporting bundle.",
+                                                                         "bundle-symbolic-name=com.acme.foo.daffy",
+                                                                         null,
+                                                                         Verifier.SYMBOLICNAME);
+
+    static Syntax                           bundle_version       = new Syntax(
+                                                                         BUNDLE_VERSION_ATTRIBUTE,
+                                                                         "a version range to select the bundle version of the exporting bundle. The default value is 0.0.0.",
+                                                                         "bundle-version=1.3",
+                                                                         null,
+                                                                         Verifier.VERSIONRANGE);
+
+    static Syntax                           path_version         = new Syntax(
+                                                                         VERSION_ATTRIBUTE,
+                                                                         "Specifies the range in the repository, project, or file",
+                                                                         "version=project",
+                                                                         "project,type",
+                                                                         Pattern
+                                                                                 .compile("project|type|"
+                                                                                         + Verifier.VERSIONRANGE
+                                                                                                 .toString()));
+
+    static Syntax[]                         syntaxes             = new Syntax[] {
+            new Syntax(
+                    BUNDLE_ACTIVATION_POLICY,
+                    "The Bundle-ActivationPolicy specifies how the framework should activate the bundle once started. ",
+                    "Bundle-ActivationPolicy: lazy", "lazy", Pattern
+                            .compile("lazy")),
+
+            new Syntax(
+                    BUNDLE_ACTIVATOR,
+                    "The Bundle-Activator header specifies the name of the class used to start and stop the bundle. ",
+                    "Bundle-Activator: com.acme.foo.Activator",
+                    "${classes;implementing;org.osgi.framework.BundleActivator}",
+                    Verifier.FQNPATTERN),
+            new Syntax(
+                    BUNDLE_CATEGORY,
+                    "The Bundle-Category header holds a comma-separated list of category names",
+                    "Bundle-Category: test",
+                    "osgi,test,game,util,eclipse,netbeans,jdk,specification",
+                    null),
+            new Syntax(
+                    BUNDLE_CLASSPATH,
+                    "The Bundle-ClassPath header defines a comma-separated list of JAR file path names or directories (inside the bundle) containing classes and resources. The period (’.’) specifies the root directory of the bundle’s JAR. The period is also the default.",
+                    "Bundle-Classpath: /lib/libnewgen.so, .", null,
+                    Verifier.PATHPATTERN),
+            new Syntax(
+                    BUNDLE_CONTACTADDRESS,
+                    "The Bundle-ContactAddress header provides the contact address of the vendor. ",
+                    "Bundle-ContactAddress: 2400 Oswego Road, Austin, TX 74563",
+                    null, null),
+            new Syntax(
+                    BUNDLE_COPYRIGHT,
+                    "The Bundle-Copyright header contains the copyright specification for this bundle. ",
+                    "Bundle-Copyright: OSGi (c) 2002", null, null),
+            new Syntax(
+                    BUNDLE_DESCRIPTION,
+                    "The Bundle-Description header defines a short description of this bundle.",
+                    "Bundle-Description: Ceci ce n'est pas une bundle", null,
+                    null),
+
+            new Syntax(
+                    BUNDLE_DOCURL,
+                    "The Bundle-DocURL headers must contain a URL pointing to documentation about this bundle.",
+                    "Bundle-DocURL: http://www.aQute.biz/Code/Bnd", null,
+                    Verifier.URLPATTERN),
+
+            new Syntax(
+                    BUNDLE_ICON,
+                    "The optional Bundle-Icon header provides a list of (relative) URLs to icons representing this bundle in different sizes. ",
+                    "Bundle-Icon: /icons/bnd.png;size=64", "/icons/bundle.png",
+                    Verifier.URLPATTERN, new Syntax("size",
+                            "Icons size in pixels, e.g. 64", "64",
+                            "16,32,48,64,128", Verifier.NUMBERPATTERN)),
+
+            new Syntax(
+                    BUNDLE_LICENSE,
+                    "The Bundle-License header provides an optional machine readable form of license information. The purpose of this header is to automate some of the license processing required by many organizations",
+                    "Bundle License: http://www.opensource.org/licenses/jabberpl.php",
+                    "http://www.apache.org/licenses/LICENSE-2.0,<<EXTERNAL>>",
+                    Pattern.compile("(" + Verifier.URLPATTERN
+                            + "|<<EXTERNAL>>)"), new Syntax(
+                            DESCRIPTION_ATTRIBUTE,
+                            "Human readable description of the license",
+                            "description=\"Described the license here\"", null,
+                            Verifier.ANYPATTERN), new Syntax(LINK_ATTRIBUTE,
+                            "", "", null, Verifier.URLPATTERN)),
+            new Syntax(
+                    BUNDLE_LOCALIZATION,
+                    "The Bundle-Localization header contains the location in the bundle where localization files can be found. The default value is OSGI-INF/l10n/bundle. Translations are by default therefore OSGI-INF/l10n/bundle_de.properties, OSGI-INF/l10n/bundle_nl.properties, etc.",
+                    "Bundle-Localization: OSGI-INF/l10n/bundle",
+                    "OSGI-INF/l10n/bundle", Verifier.URLPATTERN),
+            new Syntax(
+                    BUNDLE_MANIFESTVERSION,
+                    "This header is set by bnd automatically to 2. The Bundle-ManifestVersion header defines that the bundle follows the rules of this specification. The Bundle-ManifestVersion header determines whether the bundle follows the rules of this specification.",
+                    "# Bundle-ManifestVersion: 2", "2", Verifier.NUMBERPATTERN),
+            new Syntax(
+                    BUNDLE_NAME,
+                    "This header will be derived from the  Bundle-SymbolicName if not set. The Bundle-Name header defines a readable name for this bundle. This should be a short, human-readable name that can contain spaces.",
+                    "Bundle-Name: My Bundle", null, Verifier.ANYPATTERN),
+            new Syntax(
+                    BUNDLE_NATIVECODE,
+                    "The Bundle-NativeCode header contains a specification of native code libraries contained in this bundle. ",
+                    "Bundle-NativeCode: /lib/http.DLL; osname = QNX; osversion = 3.1",
+                    null,
+                    Verifier.PATHPATTERN,
+                    new Syntax(OSNAME_ATTRIBUTE,
+                            "The name of the operating system", "osname=MacOS",
+                            Processor.join(Verifier.OSNAMES, ","),
+                            Verifier.ANYPATTERN),
+                    new Syntax(OSVERSION_ATTRIBUTE, "Operating System Version",
+                            "osversion=3.1", null, Verifier.ANYPATTERN),
+                    new Syntax(LANGUAGE_ATTRIBUTE, "Language ISO 639 code",
+                            "language=nl", null, Verifier.ISO639),
+                    new Syntax(PROCESSOR_ATTRIBUTE, "Processor name",
+                            "processor=x86", Processor.join(
+                                    Verifier.PROCESSORNAMES, ","),
+                            Verifier.ANYPATTERN),
+                    new Syntax(
+                            SELECTION_FILTER_ATTRIBUTE,
+                            "The value of this attribute must be a filter expression that indicates if the native code clause should be selected or not.",
+                            "selection-filter=\"(com.acme.windowing=win32)\"",
+                            null, Verifier.FILTERPATTERN)),
+            new Syntax(
+                    BUNDLE_REQUIREDEXECUTIONENVIRONMENT,
+                    "The Bundle-RequiredExecutionEnvironment contains a comma-separated list of execution environments that must be present on the Service Platform.",
+                    "Bundle-RequiredExecutionEnvironment: CDC-1.0/Foundation-1.0",
+                    Processor.join(Verifier.EES, ","), Verifier.ANYPATTERN),
+
+            new Syntax(
+                    BUNDLE_SYMBOLICNAME,
+                    "The Bundle-SymbolicName header specifies a non-localizable name for this bundle. The bundle symbolic name together with a version must identify a  unique bundle. The bundle symbolic name should be based on the reverse  domain name convention",
+                    "Bundle-SymbolicName: com.acme.foo.daffy;singleton:=true",
+                    "${p}",
+                    Verifier.SYMBOLICNAME,
+                    new Syntax(
+                            SINGLETON_DIRECTIVE,
+                            " Indicates that the bundle can only have  a single version resolved.  A value of true indicates that the bundle is a singleton bundle. The default value is false. The Framework must resolve at most one  bundle when multiple versions of a singleton bundle with the same symbolic name are installed. Singleton bundles do not affect the resolution of non-singleton bundles with the same symbolic name.",
+                            "false", "true,false", Verifier.TRUEORFALSEPATTERN),
+                    new Syntax(
+                            FRAGMENT_ATTACHMENT_DIRECTIVE,
+                            "Defines how fragments are allowed to be attached, see the fragments in Fragment Bundles on page73. The following values are valid for this directive:",
+                            "", "always|never|resolve-time", Pattern
+                                    .compile("always|never|resolve-time")),
+                    new Syntax(BLUEPRINT_WAIT_FOR_DEPENDENCIES_ATTRIBUTE, "",
+                            "", "true,false", Verifier.TRUEORFALSEPATTERN),
+                    new Syntax(BLUEPRINT_TIMEOUT_ATTRIBUTE, "", "",
+                            "30000,60000,300000", Verifier.NUMBERPATTERN)),
+
+            new Syntax(
+                    BUNDLE_UPDATELOCATION,
+                    "The Bundle-UpdateLocation header specifies a URL where an update for this bundle should come from. If the bundle is updated, this location should be used, if present, to retrieve the updated JAR file.",
+                    "Bundle-UpdateLocation: http://www.acme.com/Firewall/bundle.jar",
+                    null, Verifier.URLPATTERN),
+
+            new Syntax(
+                    BUNDLE_VENDOR,
+                    "The Bundle-Vendor header contains a human-readable description of the bundle vendor. ",
+                    "Bundle-Vendor: OSGi Alliance ", null, null),
+
+            new Syntax(
+                    BUNDLE_VERSION,
+                    "The Bundle-Version header specifies the version of this bundle",
+                    "Bundle-Version: 1.23.4.build200903221000", null,
+                    Verifier.VERSION),
+
+            new Syntax(
+                    DYNAMICIMPORT_PACKAGE,
+                    "The DynamicImport-Package header contains a comma-separated list of package names that should be dynamically imported when needed.",
+                    "DynamicImport-Package: com.acme.plugin.*", "",
+                    Verifier.WILDCARDNAMEPATTERN, version,
+                    bundle_symbolic_name, bundle_version),
+
+            new Syntax(
+                    EXPORT_PACKAGE,
+                    "The Export-Package header contains a declaration of exported packages.",
+                    "Export-Package: org.osgi.util.tracker;version=1.3",
+                    "${packages}",
+                    null,
+                    new Syntax(
+                            NO_IMPORT_DIRECTIVE,
+                            "By default, bnd makes all exports also imports. Adding a -noimport to an exported package will make it export only",
+                            "-noimport:=true", "true,false",
+                            Verifier.TRUEORFALSEPATTERN),
+                    new Syntax(
+                            USES_DIRECTIVE,
+                            "Calculated by bnd: It is a comma-separated list of package names that are used by the exported package",
+                            "Is calculated by bnd", null, null),
+                    new Syntax(
+                            MANDATORY_DIRECTIVE,
+                            "A comma-separated list of attribute names. Note that the use of a comma in the value requires it to be enclosed in double quotes. A bundle importing the package must specify the mandatory attributes, with a value that matches, to resolve to the exported package",
+                            "mandatory=\"bar,foo\"", null, null),
+                    new Syntax(
+                            INCLUDE_DIRECTIVE,
+                            "A comma-separated list of class names that must be visible to an importer",
+                            "include:=\"Qux*\"", null, null),
+                    new Syntax(
+                            EXCLUDE_DIRECTIVE,
+                            "A comma-separated list of class names that must not be visible to an importer",
+                            "exclude:=\"QuxImpl*,BarImpl\"", null,
+                            Verifier.WILDCARDNAMEPATTERN), new Syntax(
+                            IMPORT_DIRECTIVE, "Experimental", "", null, null)
+
+            ),
+            new Syntax(EXPORT_SERVICE, "Deprecated",
+                    "Export-Service: org.osgi.service.log.LogService ",
+                    "${classes;implementing;*}", null),
+            new Syntax(
+                    FRAGMENT_HOST,
+                    "The Fragment-Host header defines the host bundle for this fragment.",
+                    "Fragment-Host: org.eclipse.swt; bundle-version=\"[3.0.0,4.0.0)\"",
+                    null,
+                    null,
+                    new Syntax(
+                            EXTENSION_DIRECTIVE,
+                            " Indicates this extension is a system or boot class path extension. It is only applicable when the Fragment-Host is the System Bundle",
+                            "extension:=framework", "framework,bootclasspath",
+                            Pattern.compile("framework|bootclasspath")),
+                    bundle_version),
+            new Syntax(
+                    IMPORT_PACKAGE,
+                    "This header is normally calculated by bnd, however, you can decorate packages or skip packages. The Import-Package header declares the imported packages for this bundle",
+                    "Import-Package: !com.exotic.*, com.acme.foo;vendor=ACME, *",
+                    "${exported_packages}",
+                    Verifier.WILDCARDNAMEPATTERN,
+                    new Syntax(
+                            REMOVE_ATTRIBUTE_DIRECTIVE,
+                            "Remove the given attributes from matching imported packages",
+                            "-remove-attribute:=foo.*", null,
+                            Verifier.WILDCARDNAMEPATTERN),
+                    new Syntax(
+                            RESOLUTION_DIRECTIVE,
+                            "Indicates that the packages must be resolved if the value is mandatory, which is the default. If mandatory packages cannot be resolved, then the bundle must fail to resolve. A value of optional indicates that the packages are optional",
+                            "resolution:=optional", "mandatory,optional",
+                            Pattern.compile("mandatory|optional")
+
+                    ), version, bundle_symbolic_name, bundle_version),
+
+            new Syntax(
+                    REQUIRE_BUNDLE,
+                    "The Require-Bundle header specifies the required exports from another bundle.",
+                    "Require-Bundle: com.acme.chess",
+                    null,
+                    Verifier.WILDCARDNAMEPATTERN,
+
+                    new Syntax(
+                            VISIBILITY_DIRECTIVE,
+                            " If the value is private (Default), then all visible packages from the required bundles are not re-exported. If the value is reexport then bundles that require this bundle will transitively have access to these required bundle’s exported packages.",
+                            "visibility:=private", "private,reexport", Pattern
+                                    .compile("private|reexport")),
+
+                    new Syntax(
+                            RESOLUTION_DIRECTIVE,
+                            "If the value is mandatory (default) then the required bundle must exist for this bundle to resolve. If the value is optional, the bundle will resolve even if the required bundle does not exist.",
+                            "resolution:=optional", "mandatory,optional",
+                            Pattern.compile("mandatory|optional")),
+
+                    new Syntax(
+                            SPLIT_PACKAGE_DIRECTIVE,
+                            "Indicates how an imported package should be merged when it is split between different exporters. The default is merge-first with warning",
+                            "-split-package:=merge-first",
+                            "merge-first,merge-last,error,first",
+                            Pattern
+                                    .compile("merge-first|merge-last|error|first")),
+                    bundle_version
+
+            ),
+
+            new Syntax(
+                    BUILDPATH,
+                    "Provides the class path for building the jar. The entries are references to the repository",
+                    "-buildpath=osgi;version=4.1", "${repo;bsns}",
+                    Verifier.SYMBOLICNAME, path_version),
+            new Syntax(
+                    BUMPPOLICY,
+                    "Sets the version bump policy. This is a parameter to the ${version} macro.",
+                    "-bumppolicy==+0", "==+,=+0,+00", Pattern
+                            .compile("[=+-0][=+-0][=+-0]")),
+
+            new Syntax(
+                    CONDUIT,
+                    "Allows a bnd file to point to files which will be returned when the bnd file is build",
+                    "-conduit= jar/osgi.jar", null, null),
+
+            new Syntax(
+                    DEPENDSON,
+                    "List of project names that this project directly depends on. These projects are always build ahead of this project",
+                    "-dependson=org.acme.cm", "${projects}", null),
+
+            new Syntax(DEPLOYREPO,
+                    "Specifies to which repo the project should be deployed.",
+                    "-deployrepo=cnf", "${repos}", null),
+
+            new Syntax(
+                    DONOTCOPY,
+                    "Regular expression for names of files and directories that should not be copied when discovered",
+                    "-donotcopy=(CVS|\\.svn)", null, null),
+
+            new Syntax(
+                    EXPORT_CONTENTS,
+                    "Build the JAR in the normal way but use this header for the Export-Package header manifest generation, same format",
+                    "-exportcontents=!*impl*,*;version=3.0", null, null),
+
+            new Syntax(
+                    FAIL_OK,
+                    "Return with an ok status (0) even if the build generates errors",
+                    "-failok=true", "true,false", Verifier.TRUEORFALSEPATTERN),
+
+            new Syntax(
+                    INCLUDE,
+                    "Include files. If an entry starts with '-', it does not have to exist. If it starts with '~', it must not overwrite any existing properties",
+                    "-include: -${java.user}/.bnd", null, null),
+
+            new Syntax(
+                    INCLUDERESOURCE,
+                    "Include resources from the file system. You can specify a directory, or file. All files are copied to the root, unless a destination directory is indicated",
+                    "-includeresource: lib=jar", null, null),
+
+            new Syntax(
+                    MAKE,
+                    "Set patterns for make plugins. These patterns are used to find a plugin that can make a resource that can not be found.",
+                    "-make: (*).jar;type=bnd;  recipe=\"bnd/$1.bnd\"", null,
+                    null, new Syntax("type", "Type name for plugin",
+                            "type=bnd", "bnd", null), new Syntax("recipe",
+                            "Recipe for the plugin, can use back references",
+                            "recipe=\"bnd/$1.bnd\"", "bnd", null)),
+
+            new Syntax(
+                    MANIFEST,
+                    "Directly include a manifest, do not use the calculated manifest",
+                    "-manifest = META-INF/MANIFEST.MF", null, null),
+
+            new Syntax(NOEXTRAHEADERS, "Do not generate housekeeping headers",
+                    "-noextraheaders", "true,false",
+                    Verifier.TRUEORFALSEPATTERN),
+
+            new Syntax(NOUSES,
+                    "Do not calculate the uses: directive on exports",
+                    "-nouses=true", "true,false", Verifier.TRUEORFALSEPATTERN),
+
+            new Syntax(NOPE,
+                    "Dont do anything, return without building any jars",
+                    "-nope=true", "true,false", Verifier.TRUEORFALSEPATTERN),
+
+            new Syntax(
+                    PEDANTIC,
+                    "Warn about things that are not really wrong but still not right",
+                    "-nope=true", "true,false", Verifier.TRUEORFALSEPATTERN),
+
+            new Syntax(
+                    PLUGIN,
+                    "Define the plugins",
+                    "-plugin=aQute.lib.spring.SpringComponent,aQute.lib.deployer.FileRepo;location=${repo}",
+                    null, null),
+
+            new Syntax(
+                    SERVICE_COMPONENT,
+                    "The header for Declarative Services",
+                    "Service-Component=com.acme.Foo?;activate='start'",
+                    null, null),
+
+            new Syntax(POM, "Generate a maven pom", "-pom=true", "true,false",
+                    Verifier.TRUEORFALSEPATTERN),
+
+            new Syntax(RELEASEREPO,
+                    "Specifies to which repo the project should be released.",
+                    "-releaserepo=cnf", "${repos}", null),
+
+            new Syntax(REMOVE_HEADERS,
+                    "Remove all headers that match the regular expressions",
+                    "-removeheaders=FOO_.*,Proprietary", null, null),
+            new Syntax(
+                    RESOURCEONLY,
+                    "Normally bnd warns when the JAR does not contain any classes, this option suppresses this warning",
+                    "-resourceonly=true", "true,false",
+                    Verifier.TRUEORFALSEPATTERN),
+            new Syntax(SOURCES, "Include sources in the jar", "-sources=true",
+                    "true,false", Verifier.TRUEORFALSEPATTERN),
+            new Syntax(
+                    SOURCEPATH,
+                    "List of directory names that used to source sources for -sources",
+                    "-sourcepath:= src, test", null, null),
+            new Syntax(
+                    SUB,
+                    "Build a set of bnd files that use this bnd file as a basis. The list of bnd file can be specified with wildcards",
+                    "-sub=com.acme.*.bnd", null, null),
+            new Syntax(
+                    RUNPROPERTIES,
+                    "Properties that are set as system properties before the framework is started",
+                    "-runproperties= foo=3, bar=4", null, null),
+            new Syntax(RUNSYSTEMPACKAGES,
+                    "Add additional system packages to a framework run",
+                    "-runsystempackages=com.acme.foo,javax.management", null,
+                    null),
+            new Syntax(
+                    RUNBUNDLES,
+                    "Add additional bundles, specified with their bsn and version like in -buildpath, that are started before the project is run",
+                    "-runbundles=osgi;version=\"[4.1,4.2)\", junit.junit, com.acme.foo;version=project",
+                    null, Verifier.SYMBOLICNAME, path_version),
+            new Syntax(
+                    RUNPATH,
+                    "Additional JARs for the VM path, should include the framework",
+                    "-runpath=org.eclipse.osgi;version=3.5", null, null,
+                    path_version),
+            new Syntax(
+                    RUNVM,
+                    "Additional arguments for the VM invokation. Keys that start with a - are added as options, otherwise they are treated as -D properties for the VM",
+                    "-runvm=-Xmax=30", null, null),
+            new Syntax(
+                    VERSIONPOLICY,
+                    "Provides a version policy to imports that are calculated from exports",
+                    "-versionpolicy = \"[${version;==;${@}},${version;+;${@}})\"",
+                    null, null)
+
+                                                                 };
+
+    public final static Map<String, Syntax> HELP                 = new HashMap<String, Syntax>();
+
+    static {
+        for (Syntax s : syntaxes) {
+            HELP.put(s.header, s);
+        }
+    }
+
+    public Syntax(String header, String lead, String example, String values,
+            Pattern pattern, Syntax... children) {
+        this.header = header;
+        this.children = children;
+        this.lead = lead;
+        this.example = example;
+        this.values = values;
+        this.pattern = pattern;
+    }
+
+    public String getLead() {
+        return lead;
+    }
+
+    public String getExample() {
+        return example;
+    }
+
+    public String getValues() {
+        return values;
+    }
+
+    public String getPattern() {
+        return lead;
+    }
+
+    public Syntax[] getChildren() {
+        return children;
+    }
+
+    public String getHeader() {
+        return header;
+    }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/make/Make.java b/bundleplugin/src/main/java/aQute/bnd/make/Make.java
new file mode 100644
index 0000000..463556c
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/make/Make.java
@@ -0,0 +1,101 @@
+package aQute.bnd.make;
+
+import java.util.*;
+import java.util.regex.*;
+
+import aQute.bnd.service.*;
+import aQute.lib.osgi.*;
+
+public class Make {
+    Builder                               builder;
+    Map<Instruction, Map<String, String>> make;
+
+    public Make(Builder builder) {
+        this.builder = builder;
+        // builder.getPlugins().add(new MakeBnd());
+        // builder.getPlugins().add(new MakeCopy());
+    }
+
+    public Resource process(String source) {
+        Map<Instruction, Map<String, String>> make = getMakeHeader();
+        builder.trace("make " + source);
+
+        for (Map.Entry<Instruction, Map<String, String>> entry : make
+                .entrySet()) {
+            Instruction instr = (Instruction) entry.getKey();
+            Matcher m = instr.getMatcher(source);
+            if (m.matches() || instr.isNegated()) {
+                Map<String, String> arguments = replace(m, entry.getValue());
+                List<MakePlugin> plugins = builder.getPlugins(MakePlugin.class);
+                for (MakePlugin plugin : plugins) {
+                    try {
+                        Resource resource = plugin.make(builder,
+                                source, arguments);
+                        if (resource != null) {
+                            builder.trace("Made " + source + " from args "
+                                    + arguments + " with " + plugin);
+                            return resource;
+                        }
+                    } catch (Exception e) {
+                        builder.error("Plugin " + plugin
+                                + " generates error when use in making "
+                                + source + " with args " + arguments, e);
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    private Map<String, String> replace(Matcher m, Map<String, String> value) {
+        Map<String, String> newArgs = Processor.newMap();
+        for (Map.Entry<String, String> entry : value.entrySet()) {
+            String s = entry.getValue();
+            s = replace(m, s);
+            newArgs.put(entry.getKey(), s);
+        }
+        return newArgs;
+    }
+
+    String replace(Matcher m, CharSequence s) {
+        StringBuffer sb = new StringBuffer();
+        int max = '0' + m.groupCount() + 1;
+        for (int i = 0; i < s.length(); i++) {
+            char c = s.charAt(i);
+            if (c == '$' && i < s.length() - 1) {
+                c = s.charAt(++i);
+                if (c >= '0' && c <= max) {
+                    int index = c - '0';
+                    String replacement = m.group(index);
+                    if (replacement != null)
+                        sb.append(replacement);
+                } else {
+                    if (c == '$')
+                        i++;
+                    sb.append(c);
+                }
+            } else
+                sb.append(c);
+        }
+        return sb.toString();
+    }
+
+    Map<Instruction, Map<String, String>> getMakeHeader() {
+        if (make != null)
+            return make;
+        make = Processor.newMap();
+
+        String s = builder.getProperty(Builder.MAKE);
+        Map<String, Map<String, String>> make = builder.parseHeader(s);
+        for (Iterator<Map.Entry<String, Map<String, String>>> e = make
+                .entrySet().iterator(); e.hasNext();) {
+            Map.Entry<String, Map<String, String>> entry = e.next();
+            String pattern = Processor.removeDuplicateMarker(entry.getKey());
+            
+            Instruction instr = Instruction.getPattern(pattern);
+            this.make.put(instr, entry.getValue());
+        }
+
+        return this.make;
+    }
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/make/MakeBnd.java b/bundleplugin/src/main/java/aQute/bnd/make/MakeBnd.java
new file mode 100644
index 0000000..d7ae8ed
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/make/MakeBnd.java
@@ -0,0 +1,64 @@
+package aQute.bnd.make;
+
+import java.io.*;
+import java.util.*;
+import java.util.regex.*;
+
+import aQute.bnd.build.*;
+import aQute.bnd.service.*;
+import aQute.lib.osgi.*;
+
+public class MakeBnd implements MakePlugin, Constants {
+    final static Pattern JARFILE = Pattern.compile("(.+)\\.(jar|ipa)");
+
+    public Resource make(Builder builder, String destination,
+            Map<String, String> argumentsOnMake) throws Exception {
+        String type = (String) argumentsOnMake.get("type");
+        if (!"bnd".equals(type))
+            return null;
+
+        String recipe = (String) argumentsOnMake.get("recipe");
+        if (recipe == null) {
+            builder.error("No recipe specified on a make instruction for "
+                    + destination);
+            return null;
+        }
+        File bndfile = builder.getFile(recipe);
+        if (bndfile.isFile()) {
+            // We do not use a parent because then we would
+            // build ourselves again. So we can not blindly
+            // inherit the properties.
+            Builder bchild = builder.getSubBuilder();
+            bchild.removeBundleSpecificHeaders();
+            
+            // We must make sure that we do not include ourselves again!
+            bchild.setProperty(Analyzer.INCLUDE_RESOURCE, "");
+            bchild.setProperties(bndfile, builder.getBase());
+            
+            Jar jar = bchild.build();
+            Jar dot = builder.getTarget();
+
+            if (builder.hasSources()) {
+                for (String key : jar.getResources().keySet()) {
+                    if (key.startsWith("OSGI-OPT/src"))
+                        dot.putResource(key, (Resource) jar.getResource(key));
+                }
+            }
+            builder.getInfo(bchild, bndfile.getName() +": ");
+            String debug = bchild.getProperty(DEBUG);
+            if (Processor.isTrue(debug)) {
+                if ( builder instanceof ProjectBuilder ) {
+                    ProjectBuilder pb = (ProjectBuilder) builder;
+                    File target = pb.getProject().getTarget();
+                    target.mkdirs();
+                    String bsn = bchild.getBsn();
+                    File output = new File(target, bsn+".jar");
+                    jar.write(output);
+                }
+            }
+            return new JarResource(jar);
+        } else
+            return null;
+    }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/make/MakeCopy.java b/bundleplugin/src/main/java/aQute/bnd/make/MakeCopy.java
new file mode 100644
index 0000000..3d5e4c8
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/make/MakeCopy.java
@@ -0,0 +1,45 @@
+package aQute.bnd.make;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+
+import aQute.bnd.service.*;
+import aQute.lib.osgi.*;
+
+public class MakeCopy implements MakePlugin {
+
+    public Resource make(Builder builder, String destination,
+            Map<String, String> argumentsOnMake) throws Exception {
+        String type = argumentsOnMake.get("type");
+        if (!type.equals("copy"))
+            return null;
+
+        String from = argumentsOnMake.get("from");
+        if (from == null) {
+            String content = argumentsOnMake.get("content");
+            if (content == null)
+                throw new IllegalArgumentException(
+                        "No 'from' or 'content' field in copy "
+                                + argumentsOnMake);
+            return new EmbeddedResource(content.getBytes("UTF-8"),0);
+        } else {
+
+            File f = builder.getFile(from);
+            if (f.isFile())
+                return new FileResource(f);
+            else {
+                try {
+                    URL url = new URL(from);
+                    return new URLResource(url);
+                } catch(MalformedURLException mfue) {
+                    // We ignore this
+                }
+                throw new IllegalArgumentException(
+                        "Copy source does not exist " + from
+                                + " for destination " + destination);
+            }
+        }
+    }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/make/ServiceComponent.java b/bundleplugin/src/main/java/aQute/bnd/make/ServiceComponent.java
new file mode 100644
index 0000000..a641c0b
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/make/ServiceComponent.java
@@ -0,0 +1,408 @@
+package aQute.bnd.make;
+
+import java.io.*;
+import java.util.*;
+import java.util.regex.*;
+
+import aQute.bnd.service.*;
+import aQute.lib.filter.*;
+import aQute.lib.osgi.*;
+import aQute.libg.version.*;
+
+/**
+ * This class is an analyzer plugin. It looks at the properties and tries to
+ * find out if the Service-Component header contains the bnd shortut syntax. If
+ * not, the header is copied to the output, if it does, an XML file is created
+ * and added to the JAR and the header is modified appropriately.
+ */
+public class ServiceComponent implements AnalyzerPlugin {
+    public final static String      NAMESPACE_STEM                 = "http://www.osgi.org/xmlns/scr";
+    public final static String      JIDENTIFIER                    = "<<identifier>>";
+    public final static String      COMPONENT_FACTORY              = "factory:";
+    public final static String      COMPONENT_SERVICEFACTORY       = "servicefactory:";
+    public final static String      COMPONENT_IMMEDIATE            = "immediate:";
+    public final static String      COMPONENT_ENABLED              = "enabled:";
+    public final static String      COMPONENT_DYNAMIC              = "dynamic:";
+    public final static String      COMPONENT_MULTIPLE             = "multiple:";
+    public final static String      COMPONENT_PROVIDE              = "provide:";
+    public final static String      COMPONENT_OPTIONAL             = "optional:";
+    public final static String      COMPONENT_PROPERTIES           = "properties:";
+    public final static String      COMPONENT_IMPLEMENTATION       = "implementation:";
+
+    // v1.1.0
+    public final static String      COMPONENT_VERSION              = "version:";
+    public final static String      COMPONENT_CONFIGURATION_POLICY = "configuration-policy:";
+    public final static String      COMPONENT_MODIFIED             = "modified:";
+    public final static String      COMPONENT_ACTIVATE             = "activate:";
+    public final static String      COMPONENT_DEACTIVATE           = "deactivate:";
+
+    public final static String[]    componentDirectives            = new String[] {
+            COMPONENT_FACTORY, COMPONENT_IMMEDIATE, COMPONENT_ENABLED,
+            COMPONENT_DYNAMIC, COMPONENT_MULTIPLE, COMPONENT_PROVIDE,
+            COMPONENT_OPTIONAL, COMPONENT_PROPERTIES, COMPONENT_IMPLEMENTATION,
+            COMPONENT_SERVICEFACTORY, COMPONENT_VERSION,
+            COMPONENT_CONFIGURATION_POLICY, COMPONENT_MODIFIED,
+            COMPONENT_ACTIVATE, COMPONENT_DEACTIVATE       };
+
+    public final static Set<String> SET_COMPONENT_DIRECTIVES       = new HashSet<String>(
+                                                                    Arrays
+                                                                            .asList(componentDirectives));
+
+    public final static Set<String> SET_COMPONENT_DIRECTIVES_1_1   = //
+                                                            new HashSet<String>(
+                                                                    Arrays
+                                                                            .asList(
+                                                                                    COMPONENT_VERSION,
+                                                                                    COMPONENT_CONFIGURATION_POLICY,
+                                                                                    COMPONENT_MODIFIED,
+                                                                                    COMPONENT_ACTIVATE,
+                                                                                    COMPONENT_DEACTIVATE));
+
+    public boolean analyzeJar(Analyzer analyzer) throws Exception {
+
+        ComponentMaker m = new ComponentMaker(analyzer);
+
+        Map<String, Map<String, String>> l = m.doServiceComponent();
+
+        if (!l.isEmpty())
+            analyzer.setProperty(Constants.SERVICE_COMPONENT, Processor
+                    .printClauses(l, ""));
+
+        analyzer.getInfo(m, "Service Component");
+        m.close();
+        return false;
+    }
+
+    private static class ComponentMaker extends Processor {
+        Analyzer analyzer;
+
+        ComponentMaker(Analyzer analyzer) {
+            super(analyzer);
+            this.analyzer = analyzer;
+        }
+
+        Map<String, Map<String, String>> doServiceComponent() throws Exception {
+            String header = getProperty(SERVICE_COMPONENT);
+            return doServiceComponent(header);
+        }
+
+        /**
+         * Check if a service component header is actually referring to a class.
+         * If so, replace the reference with an XML file reference. This makes
+         * it easier to create and use components.
+         * 
+         * @throws UnsupportedEncodingException
+         * 
+         */
+        public Map<String, Map<String, String>> doServiceComponent(
+                String serviceComponent) throws IOException {
+            Map<String, Map<String, String>> list = newMap();
+            Map<String, Map<String, String>> sc = parseHeader(serviceComponent);
+            Map<String, String> empty = Collections.emptyMap();
+
+            for (Iterator<Map.Entry<String, Map<String, String>>> i = sc
+                    .entrySet().iterator(); i.hasNext();) {
+                Map.Entry<String, Map<String, String>> entry = i.next();
+                String name = entry.getKey();
+                Map<String, String> info = entry.getValue();
+                if (name == null) {
+                    error("No name in Service-Component header: " + info);
+                    continue;
+                }
+                if (name.indexOf("*") >= 0 || analyzer.getJar().exists(name)) {
+                    // Normal service component, we do not process them
+                    list.put(name, info);
+                } else {
+                    String impl = name;
+
+                    if (info.containsKey(COMPONENT_IMPLEMENTATION))
+                        impl = info.get(COMPONENT_IMPLEMENTATION);
+
+                    if (!analyzer.checkClass(impl)) {
+                        error("Not found Service-Component header: " + name);
+                    } else {
+                        // We have a definition, so make an XML resources
+                        Resource resource = createComponentResource(name, info);
+                        analyzer.getJar().putResource(
+                                "OSGI-INF/" + name + ".xml", resource);
+                        list.put("OSGI-INF/" + name + ".xml", empty);
+                    }
+                }
+            }
+            return list;
+        }
+
+        /**
+         * Create the resource for a DS component.
+         * 
+         * @param list
+         * @param name
+         * @param info
+         * @throws UnsupportedEncodingException
+         */
+        Resource createComponentResource(String name, Map<String, String> info)
+                throws IOException {
+            String namespace = getNamespace(info);
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            PrintWriter pw = new PrintWriter(new OutputStreamWriter(out,
+                    "UTF-8"));
+            pw.println("<?xml version='1.0' encoding='utf-8'?>");
+            pw.print("<component name='" + name + "'");
+            if (namespace != null) {
+                pw.print(" xmlns='" + namespace + "'");
+            }
+
+            doAttribute(pw, info.get(COMPONENT_FACTORY), "factory");
+            doAttribute(pw, info.get(COMPONENT_IMMEDIATE), "immediate",
+                    "false", "true");
+            doAttribute(pw, info.get(COMPONENT_ENABLED), "enabled", "true",
+                    "false");
+            doAttribute(pw, info.get(COMPONENT_CONFIGURATION_POLICY),
+                    "configuration-policy", "optional", "require", "ignore");
+            doAttribute(pw, info.get(COMPONENT_ACTIVATE), "activate",
+                    JIDENTIFIER);
+            doAttribute(pw, info.get(COMPONENT_DEACTIVATE), "deactivate",
+                    JIDENTIFIER);
+            doAttribute(pw, info.get(COMPONENT_MODIFIED), "modified",
+                    JIDENTIFIER);
+
+            pw.println(">");
+
+            // Allow override of the implementation when people
+            // want to choose their own name
+            String impl = (String) info.get(COMPONENT_IMPLEMENTATION);
+            pw.println("  <implementation class='"
+                    + (impl == null ? name : impl) + "'/>");
+
+            String provides = info.get(COMPONENT_PROVIDE);
+            boolean servicefactory = Boolean.getBoolean(info
+                    .get(COMPONENT_SERVICEFACTORY)
+                    + "");
+            provides(pw, provides, servicefactory);
+            properties(pw, info);
+            reference(info, pw);
+            pw.println("</component>");
+            pw.close();
+            byte[] data = out.toByteArray();
+            out.close();
+            return new EmbeddedResource(data, 0);
+        }
+
+        private void doAttribute(PrintWriter pw, String value, String name,
+                String... matches) {
+            if (value != null) {
+                if (matches.length != 0) {
+                    if (matches.length == 1 && matches[0].equals(JIDENTIFIER)) {
+                        if (!Verifier.isIdentifier(value))
+                            error(
+                                    "Component attribute %s has value %s but is not a Java identifier",
+                                    name, value);
+                    } else {
+
+                        if (!Verifier.isMember(value, matches))
+                            error(
+                                    "Component attribute %s has value %s but is not a member of %s",
+                                    name, value, Arrays.toString(matches));
+                    }
+                }
+                pw.print(" ");
+                pw.print(name);
+                pw.print("='");
+                pw.print(value);
+                pw.print("'");
+            }
+        }
+
+        /**
+         * Check if we need to use the v1.1 namespace (or later).
+         * 
+         * @param info
+         * @return
+         */
+        private String getNamespace(Map<String, String> info) {
+            String version = info.get(COMPONENT_VERSION);
+            if (version != null) {
+                try {
+                    Version v = new Version(version);
+                    return NAMESPACE_STEM + "/v" + v;
+                } catch (Exception e) {
+                    error("version: specified on component header but not a valid version: "
+                            + version);
+                    return null;
+                }
+            }
+            for (String key : info.keySet()) {
+                if (SET_COMPONENT_DIRECTIVES_1_1.contains(key)) {
+                    return NAMESPACE_STEM + "/v1.1.0";
+                }
+            }
+            return null;
+        }
+
+        /**
+         * Print the Service-Component properties element
+         * 
+         * @param pw
+         * @param info
+         */
+        void properties(PrintWriter pw, Map<String, String> info) {
+            Collection<String> properties = split(info
+                    .get(COMPONENT_PROPERTIES));
+            for (Iterator<String> p = properties.iterator(); p.hasNext();) {
+                String clause = p.next();
+                int n = clause.indexOf('=');
+                if (n <= 0) {
+                    error("Not a valid property in service component: "
+                            + clause);
+                } else {
+                    String type = null;
+                    String name = clause.substring(0, n);
+                    if (name.indexOf('@') >= 0) {
+                        String parts[] = name.split("@");
+                        name = parts[1];
+                        type = parts[0];
+                    }
+                    String value = clause.substring(n + 1).trim();
+                    // TODO verify validity of name and value.
+                    pw.print("<property name='");
+                    pw.print(name);
+                    pw.print("'");
+
+                    if (type != null) {
+                        if (VALID_PROPERTY_TYPES.matcher(type).matches()) {
+                            pw.print(" type='");
+                            pw.print(type);
+                            pw.print("'");
+                        } else {
+                            warning("Invalid property type '" + type
+                                    + "' for property " + name);
+                        }
+                    }
+
+                    String parts[] = value.split("\\s*(\\||\\n)\\s*");
+                    if (parts.length > 1) {
+                        pw.println(">");
+                        for (String part : parts) {
+                            pw.println(part);
+                        }
+                        pw.println("</property>");
+                    } else {
+                        pw.print(" value='");
+                        pw.print(parts[0]);
+                        pw.print("'/>");
+                    }
+                }
+            }
+        }
+
+        /**
+         * @param pw
+         * @param provides
+         */
+        void provides(PrintWriter pw, String provides, boolean servicefactory) {
+            if (provides != null) {
+                if (!servicefactory)
+                    pw.println("  <service>");
+                else
+                    pw.println("  <service servicefactory='true'>");
+
+                StringTokenizer st = new StringTokenizer(provides, ",");
+                while (st.hasMoreTokens()) {
+                    String interfaceName = st.nextToken();
+                    pw.println("    <provide interface='" + interfaceName
+                            + "'/>");
+                    if (!analyzer.checkClass(interfaceName))
+                        error("Component definition provides a class that is neither imported nor contained: "
+                                + interfaceName);
+                }
+                pw.println("  </service>");
+            }
+        }
+
+        public final static Pattern REFERENCE = Pattern.compile("([^(]+)(\\(.+\\))?");
+
+        /**
+         * @param info
+         * @param pw
+         */
+
+        void reference(Map<String, String> info, PrintWriter pw) {
+            Collection<String> dynamic = new ArrayList<String>(split(info.get(COMPONENT_DYNAMIC)));
+            Collection<String> optional =  new ArrayList<String>(split(info.get(COMPONENT_OPTIONAL)));
+            Collection<String> multiple = new ArrayList<String>(split(info.get(COMPONENT_MULTIPLE)));
+
+            for (Iterator<Map.Entry<String, String>> r = info.entrySet()
+                    .iterator(); r.hasNext();) {
+                Map.Entry<String, String> ref = r.next();
+                String referenceName = (String) ref.getKey();
+                String target = null;
+                String interfaceName = (String) ref.getValue();
+                if (interfaceName == null || interfaceName.length() == 0) {
+                    error("Invalid Interface Name for references in Service Component: "
+                            + referenceName + "=" + interfaceName);
+                }
+                char c = interfaceName.charAt(interfaceName.length() - 1);
+                if ("?+*~".indexOf(c) >= 0) {
+                    if (c == '?' || c == '*' || c == '~')
+                        optional.add(referenceName);
+                    if (c == '+' || c == '*')
+                        multiple.add(referenceName);
+                    if (c == '+' || c == '*' || c == '?')
+                        dynamic.add(referenceName);
+                    interfaceName = interfaceName.substring(0, interfaceName
+                            .length() - 1);
+                }
+
+                if (referenceName.endsWith(":")) {
+                    if (!SET_COMPONENT_DIRECTIVES.contains(referenceName))
+                        error("Unrecognized directive in Service-Component header: "
+                                + referenceName);
+                    continue;
+                }
+
+                Matcher m = REFERENCE.matcher(interfaceName);
+                if (m.matches()) {
+                    interfaceName = m.group(1);
+                    target = m.group(2);
+                }
+
+                if (!analyzer.checkClass(interfaceName))
+                    error("Component definition refers to a class that is neither imported nor contained: "
+                            + interfaceName);
+
+                pw.print("  <reference name='" + referenceName
+                        + "' interface='" + interfaceName + "'");
+
+                String cardinality = optional.contains(referenceName) ? "0"
+                        : "1";
+                cardinality += "..";
+                cardinality += multiple.contains(referenceName) ? "n" : "1";
+                if (!cardinality.equals("1..1"))
+                    pw.print(" cardinality='" + cardinality + "'");
+
+                if (Character.isLowerCase(referenceName.charAt(0))) {
+                    String z = referenceName.substring(0, 1).toUpperCase()
+                            + referenceName.substring(1);
+                    pw.print(" bind='set" + z + "'");
+                    pw.print(" unbind='unset" + z + "'");
+                    // TODO Verify that the methods exist
+                }
+
+                if (dynamic.contains(referenceName)) {
+                    pw.print(" policy='dynamic'");
+                }
+
+                if (target != null) {
+                    Filter filter = new Filter(target);
+                    if (filter.verify() == null)
+                        pw.print(" target='" + filter.toString() + "'");
+                    else
+                        error("Target for " + referenceName
+                                + " is not a correct filter: " + target + " "
+                                + filter.verify());
+                }
+                pw.println("/>");
+            }
+        }
+    }
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/service/AnalyzerPlugin.java b/bundleplugin/src/main/java/aQute/bnd/service/AnalyzerPlugin.java
new file mode 100644
index 0000000..3efe8ee
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/service/AnalyzerPlugin.java
@@ -0,0 +1,20 @@
+package aQute.bnd.service;
+
+import aQute.lib.osgi.*;
+
+public interface AnalyzerPlugin {
+
+    /**
+     * This plugin is called after analysis. The plugin is free to modify the
+     * jar and/or change the classpath information (see referred, contained).
+     * This plugin is called after analysis of the JAR but before manifest
+     * generation.
+     * 
+     * @param analyzer
+     * @return true if the classpace has been modified so that the bundle
+     *         classpath must be reanalyzed
+     * @throws Exception
+     */
+
+    boolean analyzeJar(Analyzer analyzer) throws Exception;
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/service/MakePlugin.java b/bundleplugin/src/main/java/aQute/bnd/service/MakePlugin.java
new file mode 100644
index 0000000..20a1849
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/service/MakePlugin.java
@@ -0,0 +1,21 @@
+package aQute.bnd.service;
+
+import java.util.*;
+
+import aQute.lib.osgi.*;
+
+public interface MakePlugin {
+
+    /**
+     * This plugin is called when Include-Resource detects a reference to a resource
+     * that it can not find in the file system.
+     * 
+     * @param builder   The current builder
+     * @param source    The source string (i.e. the place where bnd looked)
+     * @param arguments Any arguments on the clause in Include-Resource
+     * @return          A resource or null if no resource could be made
+     * @throws Exception
+     */
+    Resource make(Builder builder, String source, Map<String,String> arguments) throws Exception;
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/service/Plugin.java b/bundleplugin/src/main/java/aQute/bnd/service/Plugin.java
new file mode 100644
index 0000000..065fac8
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/service/Plugin.java
@@ -0,0 +1,31 @@
+package aQute.bnd.service;
+
+import java.util.*;
+
+import aQute.libg.reporter.*;
+
+/**
+ * An optional interface for plugins. If a plugin implements this interface then
+ * it can receive the reminaing attributes and directives given in its clause as
+ * well as the reporter to use.
+ * 
+ */
+public interface Plugin {
+    /**
+     * Give the plugin the remaining properties.
+     * 
+     * When a plugin is declared, the clause can contain extra properties.
+     * All the properties and directives are given to the plugin to use.
+     * 
+     * @param map attributes and directives for this plugin's clause
+     */
+    void setProperties(Map<String,String> map);
+    
+    /**
+     * Set the current reporter. This is called at init time. This plugin
+     * should report all errors and warnings to this reporter.
+     * 
+     * @param processor
+     */
+    void setReporter(Reporter processor);
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/service/Refreshable.java b/bundleplugin/src/main/java/aQute/bnd/service/Refreshable.java
new file mode 100644
index 0000000..e5e62e9
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/service/Refreshable.java
@@ -0,0 +1,8 @@
+package aQute.bnd.service;
+
+import java.io.*;
+
+public interface Refreshable {
+    boolean refresh();
+    File getRoot();
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/service/RepositoryPlugin.java b/bundleplugin/src/main/java/aQute/bnd/service/RepositoryPlugin.java
new file mode 100644
index 0000000..14282ec
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/service/RepositoryPlugin.java
@@ -0,0 +1,59 @@
+package aQute.bnd.service;
+
+import java.io.*;
+import java.util.*;
+
+import aQute.lib.osgi.*;
+import aQute.libg.version.*;
+
+public interface RepositoryPlugin {
+    /**
+     * Return a URL to a matching version of the given bundle.
+     * 
+     * @param bsn
+     *            Bundle-SymbolicName of the searched bundle
+     * @param range
+     *            Version range for this bundle,"latest" if you only want the
+     *            latest, or null when you want all.
+     * @return A list of URLs sorted on version, lowest version is at index 0.
+     *         null is returned when no files with the given bsn ould be found.
+     * @throws Exception
+     *             when anything goes wrong
+     */
+    File[] get(String bsn, String range) throws Exception;
+    
+    /**
+     * Answer if this repository can be used to store files.
+     * 
+     * @return true if writable
+     */
+    boolean canWrite();
+    
+    /**
+     * Put a JAR file in the repository.
+     * 
+     * @param jar
+     * @throws Exception
+     */
+    File  put(Jar jar) throws Exception;
+    
+    /**
+     * Return a list of bsns that are present in the repository.
+     * 
+     * @param regex if not null, match against the bsn and if matches, return otherwise skip
+     * @return A list of bsns that match the regex parameter or all if regex is null
+     */
+    List<String> list(String regex);
+    
+    /**
+     * Return a list of versions.
+     */
+    
+    List<Version> versions(String bsn);
+    
+    /**
+     * @return The name of the repository
+     */
+    String getName();
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/service/SignerPlugin.java b/bundleplugin/src/main/java/aQute/bnd/service/SignerPlugin.java
new file mode 100644
index 0000000..aaef646
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/service/SignerPlugin.java
@@ -0,0 +1,15 @@
+package aQute.bnd.service;
+
+import aQute.lib.osgi.*;
+
+public interface SignerPlugin {
+    /**
+     * Sign the current jar. The alias is the given certificate 
+     * keystore.
+     * 
+     * @param builder   The current builder that contains the jar to sign
+     * @param alias     The keystore certificate alias
+     * @throws Exception When anything goes wrong
+     */
+    void sign(Builder builder, String alias) throws Exception;
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/service/action/Action.java b/bundleplugin/src/main/java/aQute/bnd/service/action/Action.java
new file mode 100644
index 0000000..9fd42ed
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/service/action/Action.java
@@ -0,0 +1,25 @@
+package aQute.bnd.service.action;
+
+import aQute.bnd.build.*;
+
+public interface Action {
+    /**
+     * A String[] that specifies a menu entry. The entry
+     * can be hierarchical by separating the parts with a ':'.
+     * 
+     * <pre>
+     *  A:B:C
+     *  A:B:D
+     *  
+     *      A
+     *      |
+     *      B
+     *     / \
+     *    C   D
+     *    
+     * </pre>
+     */
+    String ACTION_MENU  = "bnd.action.menu";
+
+    void execute( Project project, String action) throws Exception;
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/test/ProjectLauncher.java b/bundleplugin/src/main/java/aQute/bnd/test/ProjectLauncher.java
new file mode 100644
index 0000000..dd3f117
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/test/ProjectLauncher.java
@@ -0,0 +1,336 @@
+package aQute.bnd.test;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import java.util.regex.*;
+
+import aQute.bnd.build.*;
+import aQute.lib.osgi.*;
+import aQute.libg.header.*;
+
+public class ProjectLauncher extends Processor {
+    final Project project;
+    static File   runtime;
+    String        report;
+
+    public ProjectLauncher(Project project) {
+        super(project);
+        this.project = project;
+    }
+
+    /**
+     * Calculate the classpath. We include our own runtime.jar which includes
+     * the test framework and we include the first of the test frameworks
+     * specified.
+     */
+    public String[] getClasspath() {
+        try {
+            List<String> classpath = new ArrayList<String>();
+            classpath.add(getRuntime().getAbsolutePath());
+
+            for (Container c : project.getRunpath()) {
+                if (c.getType() != Container.TYPE.ERROR) {
+                    if (!c.getFile().getName().startsWith("ee."))
+                        classpath.add(c.getFile().getAbsolutePath());
+                } else {
+                    error("Invalid entry on the " + Constants.RUNPATH + ": "
+                            + c);
+                }
+            }
+            return classpath.toArray(new String[classpath.size()]);
+        } catch (Exception e) {
+            error("Calculating class path", e);
+        }
+        return null;
+    }
+
+    /**
+     * Extract the runtime on the file system so we can refer to it. in the
+     * remote VM.
+     * 
+     * @return
+     */
+    public static File getRuntime() {
+        if (runtime == null) {
+            try {
+                URL url = ProjectLauncher.class
+                        .getResource("aQute.runtime.jar");
+                if (url == null)
+                    throw new IllegalStateException(
+                            "Can not find my runtime.jar");
+
+                runtime = File.createTempFile("aQute.runtime", ".jar");
+                // runtime.deleteOnExit();
+                FileOutputStream out = new FileOutputStream(runtime);
+                InputStream in = url.openStream();
+                copy(in, out);
+                out.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        return runtime;
+    }
+
+    public void doRunbundles(List<String> programArguments) throws Exception {
+        // we are going to use this for running, so we want to
+        // use the "best" version. For compile, we should
+        // use the lowest version.
+        Collection<Container> testbundles = project.getRunbundles();
+
+        for (Container c : testbundles) {
+            if (c.getError() != null)
+                error("Invalid bundle on " + Constants.RUNBUNDLES + " "
+                        + c.getError());
+            else {
+                // Do we need to build any sub projects?
+                if (c.getVersion() != null && c.getVersion().equals("project")) {
+                    Project sub = c.getProject();
+                    sub.clear();
+                    File[] outputs = sub.build(false);
+                    for (File f : outputs) {
+                        programArguments.add("-bundle");
+                        programArguments.add(f.getAbsolutePath());
+                    }
+                    getInfo(sub);
+                } else {
+                    programArguments.add("-bundle");
+                    programArguments.add(c.getFile().getAbsolutePath());
+                }
+            }
+        }
+    }
+
+    private void doRunpath(List<String> programArguments) throws Exception {
+        Collection<Container> testpath = project.getRunpath();
+        Container found = null;
+        for (Container c : testpath) {
+            if (c.getAttributes().containsKey("framework")) {
+                if (found != null) {
+                    warning("Specifying multiple framework classes on the "
+                            + Constants.RUNPATH + "\n" + "Previous found: "
+                            + found.getProject() + " " + found.getAttributes()
+                            + "\n" + "Now found     : " + c.getProject() + " "
+                            + c.getAttributes());
+                }
+                programArguments.add("-framework");
+                programArguments.add(c.getAttributes().get("framework"));
+                found = c;
+            }
+            if (c.getAttributes().containsKey("factory")) {
+                if (found != null) {
+                    warning("Specifying multiple framework factories on the "
+                            + Constants.RUNPATH + "\n" + "Previous found: "
+                            + found.getProject() + " " + found.getAttributes()
+                            + "\n" + "Now found     : " + c.getProject() + " "
+                            + c.getAttributes());
+                }
+                programArguments.add("-framework");
+                programArguments.add(c.getAttributes().get("factory"));
+                found = c;
+            }
+            String exports = c.getAttributes().get("export");
+            if (exports != null) {
+                String parts[] = exports.split("\\s*,\\s*");
+                for (String p : parts) {
+                    programArguments.add("-export");
+                    programArguments.add(p);
+                }
+            }
+        }
+
+        doSystemPackages(programArguments);
+        doRunProperties(programArguments);
+    }
+
+    private void doRunProperties(List<String> programArguments) {
+        Map<String, String> properties = OSGiHeader
+                .parseProperties(getProperty(RUNPROPERTIES));
+        for (Map.Entry<String, String> entry : properties.entrySet()) {
+            programArguments.add("-set");
+            programArguments.add(entry.getKey());
+            programArguments.add(entry.getValue());
+        }
+    }
+
+    private void doSystemPackages(List<String> programArguments) {
+        Map<String, Map<String, String>> systemPackages = parseHeader(getProperty(RUNSYSTEMPACKAGES));
+        for (Map.Entry<String, Map<String, String>> entry : systemPackages
+                .entrySet()) {
+            programArguments.add("-export");
+            StringBuffer sb = new StringBuffer();
+            sb.append(entry.getKey());
+            printClause(entry.getValue(), null, sb);
+            programArguments.add(sb.toString());
+        }
+    }
+
+    private void doStorage(List<String> programArguments) throws Exception {
+        File tmp = new File(project.getTarget(), "fwtmp");
+        tmp.mkdirs();
+        tmp.deleteOnExit();
+
+        programArguments.add("-storage");
+        programArguments.add(tmp.getAbsolutePath());
+    }
+
+    /**
+     * Utility to copy a file from a resource.
+     * 
+     * @param in
+     * @param out
+     * @throws IOException
+     */
+    private static void copy(InputStream in, FileOutputStream out)
+            throws IOException {
+        byte buf[] = new byte[8192];
+        int size = in.read(buf);
+        while (size > 0) {
+            out.write(buf, 0, size);
+            size = in.read(buf);
+        }
+        in.close();
+    }
+
+    private Process launch(File[] targets) throws Exception {
+        List<String> arguments = newList();
+        List<String> vmArguments = newList();
+
+        vmArguments.add(getProperty("java", "java"));
+        doClasspath(vmArguments);
+
+        getArguments(targets, vmArguments, arguments);
+
+        vmArguments.add("aQute.junit.runtime.Target");
+        arguments.add("-report");
+        arguments.add(getTestreport());
+
+        List<String> all = newList();
+        all.addAll(vmArguments);
+        all.addAll(arguments);
+
+        System.out.println("Cmd: " + all);
+
+        String[] cmdarray = all.toArray(new String[all.size()]);
+        if (getErrors().size() > 0)
+            return null;
+
+        return Runtime.getRuntime().exec(cmdarray, null, project.getBase());
+    }
+/*
+    static Pattern ARGUMENT = Pattern.compile("[-a-zA-Z0-9\\._]+");
+    private void doVMArguments(List<String> arguments) {
+        Map<String,String> map = OSGiHeader.parseProperties( getProperty(RUNVM));
+        for ( String key : map.keySet() ) {
+            if ( ARGUMENT.matcher(key).matches())
+                if ( key.startsWith("-"))
+                    arguments.add(key);
+                else
+                    arguments.add("-D" + key.trim() + "=" + map.get(key));
+            else
+                warning("VM Argument is not a proper property key: " + key );
+        }
+    }
+*/
+    private void doVMArguments(List<String> arguments) {
+        Map<String, String> map = OSGiHeader
+                .parseProperties(getProperty(RUNVM));
+        for (String key : map.keySet()) {
+            if (key.startsWith("-"))
+                arguments.add(key);
+            else
+                arguments.add("-D" + key.trim() + "=" + map.get(key));
+        }
+    }
+    private void doClasspath(List<String> arguments) {
+        Collection<String> cp = Arrays.asList(getClasspath());
+        if (!cp.isEmpty()) {
+            arguments.add("-classpath");
+            arguments.add(join(cp, File.pathSeparator));
+        }
+    }
+
+    public int run(File f) throws Exception {
+        final Process process = launch(new File[] { f });
+
+        Thread killer = new Thread() {
+            public void run() {
+                process.destroy();
+            }
+        };
+        Runtime.getRuntime().addShutdownHook(killer);
+        Streamer sin = new Streamer(process.getInputStream(), System.out);
+        Streamer serr = new Streamer(process.getErrorStream(), System.out);
+        try {
+            sin.start();
+            serr.start();
+            return process.waitFor();
+        } finally {
+            Runtime.getRuntime().removeShutdownHook(killer);
+            sin.join();
+            serr.join();
+        }
+    }
+
+    static class Streamer extends Thread {
+        final InputStream  in;
+        final OutputStream out;
+
+        Streamer(InputStream in, OutputStream out) {
+            this.in = in;
+            this.out = out;
+        }
+
+        public void run() {
+            try {
+                int c;
+                while ((c = in.read()) > 0) {
+                    this.out.write(c);
+                }
+            } catch (IOException ioe) {
+                // Ignore
+            }
+        };
+
+    };
+
+    public String getTestreport() {
+        if (report != null)
+            return report;
+        return report = getProperty(Constants.TESTREPORT,
+                "${target}/test-report.xml");
+
+    }
+
+    public void getArguments(List<String> vmArguments,
+            List<String> programArguments, boolean undertest) throws Exception {
+        File files[] = project.build(undertest);
+        getInfo(project);
+        if (files == null)
+            return;
+
+        getArguments(files, vmArguments, programArguments);
+    }
+
+    public void getArguments(File files[], List<String> vmArguments,
+            List<String> programArguments) throws Exception {
+        doVMArguments(vmArguments);
+        doStorage(programArguments);
+        doRunpath(programArguments);
+        doRunbundles(programArguments);
+        for (File file : files) {
+            programArguments.add("-target");
+            programArguments.add(file.getAbsolutePath());
+        }
+    }
+
+    public String getReport() {
+        return report;
+    }
+
+    public void setReport(String report) {
+        this.report = report;
+    }
+
+}