Initial support for WAR deployment.  GWTCompiler happens to be backwards-compatible with the legacy format, but the behavior of the shell differs.  Therefore we created a new entry point, GWTHosted, which is the new GWTShell and runs with WAR assumptions.

Summary of WAR mode:
- Assumes output folder is in WAR format
- Runs Jetty; embeds GWTShellServletFilter to autogenerate modules on demand
- Uses true linking (and the new relinking); dumps resources and generated selection script directly into output folder
- Ignores the <servlet> tag; servlets must be initialized via WEB-INF/web.xml in the output folder

Summary of legacy mode:
- Same behavior as GWT 1.5
- Uses Tomcat (but we want to eventually replace with Jetty); uses GWTShellServlet
- Serves files directly from public path and work directory; uses the old HostedModeLinker to generate the selection script
- <servlet> tag still works

Additionally, a GWT module file now supports a "deploy-to" attribute at the top level, which allows the subdirectory within the output folder for a module to be overridden (the default is the fully-qualified module name).

Patch by: scottb, bruce, bobv (two-pair programming)
Review by: bobv, scottb


git-svn-id: https://google-web-toolkit.googlecode.com/svn/releases/1.6@3890 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/build.xml b/dev/core/build.xml
index bcc6f13..e5090e6 100755
--- a/dev/core/build.xml
+++ b/dev/core/build.xml
@@ -26,6 +26,7 @@
       <zipfileset src="${gwt.tools.lib}/apache/tapestry-util-text-4.0.2.jar" />
       <zipfileset src="${gwt.tools.lib}/apache/ant-1.6.5.jar" />
       <zipfileset src="${gwt.tools.lib}/eclipse/jdt-3.3.1.jar" />
+      <zipfileset src="${gwt.tools.lib}/jetty/jetty-6.1.11.jar" />
       <zipfileset src="${gwt.tools.lib}/tomcat/ant-launcher-1.6.5.jar" />
       <zipfileset src="${gwt.tools.lib}/tomcat/catalina-1.0.jar" />
       <zipfileset src="${gwt.tools.lib}/tomcat/catalina-optional-1.0.jar" />
diff --git a/dev/core/src/com/google/gwt/core/ext/Linker.java b/dev/core/src/com/google/gwt/core/ext/Linker.java
index 5cce5e6..5dc6ff1 100644
--- a/dev/core/src/com/google/gwt/core/ext/Linker.java
+++ b/dev/core/src/com/google/gwt/core/ext/Linker.java
@@ -23,6 +23,19 @@
  * the relative ordering of the Linkers. Exact order of Linker execution will be
  * determined by the order of <code>add-linker</code> tags in the module
  * configuration.
+ * 
+ * <p>
+ * A new instance of a linker is created each time a module is compiled or
+ * during hosted mode when a module first loads (or is refreshed). During a
+ * compile, {@link #link(TreeLogger, LinkerContext, ArtifactSet)} will be called
+ * exactly once, and the artifact set will contain any and all generated
+ * artifacts. . In hosted mode,
+ * {@link #link(TreeLogger, LinkerContext, ArtifactSet)} is called initially,
+ * but with no generated artifacts. If any artifacts are subsequently generated
+ * during the course of running hosted mode,
+ * {@link #relink(TreeLogger, LinkerContext, ArtifactSet)} will be called with
+ * the new artifacts.
+ * </p>
  */
 public abstract class Linker {
   /**
@@ -42,4 +55,26 @@
    */
   public abstract ArtifactSet link(TreeLogger logger, LinkerContext context,
       ArtifactSet artifacts) throws UnableToCompleteException;
+
+  /**
+   * Re-invoke the Linker with newly generated artifacts. Linkers that need to
+   * reference the original artifact set passed into
+   * {@link #link(TreeLogger, LinkerContext, ArtifactSet)} should retain a copy
+   * of the original artifact set in an instance variable.
+   * 
+   * @param logger the TreeLogger to record to
+   * @param context provides access to the Linker's environment
+   * @param newArtifacts an unmodifiable view of the newly generated artifacts
+   * @return the new artifacts that should be propagated through the linker
+   *         chain; it is not necessary to return any artifacts from the
+   *         original link (or previous calls to relink) that have not been
+   *         modified
+   * @throws UnableToCompleteException if compilation violates assumptions made
+   *           by the Linker or for errors encountered by the Linker
+   */
+  @SuppressWarnings("unused")
+  public ArtifactSet relink(TreeLogger logger, LinkerContext context,
+      ArtifactSet newArtifacts) throws UnableToCompleteException {
+    return newArtifacts;
+  }
 }
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/ArtifactSet.java b/dev/core/src/com/google/gwt/core/ext/linker/ArtifactSet.java
index 96435bc..6e865c0 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/ArtifactSet.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/ArtifactSet.java
@@ -177,4 +177,9 @@
   public <T> T[] toArray(T[] a) {
     return treeSet.toArray(a);
   }
+
+  @Override
+  public String toString() {
+    return treeSet.toString();
+  }
 }
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/EmittedArtifact.java b/dev/core/src/com/google/gwt/core/ext/linker/EmittedArtifact.java
index 85c4ed2..96bc46b 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/EmittedArtifact.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/EmittedArtifact.java
@@ -27,6 +27,9 @@
  * be emitted by the compiler into the module's output directory. This type may
  * be extended by Linker providers to provide alternative implementations of
  * {@link #getContents(TreeLogger)}.
+ * 
+ * TODO(bobv): provide a timestamp so we can make the time on output files match
+ * that of input files?
  */
 public abstract class EmittedArtifact extends Artifact<EmittedArtifact> {
 
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java
index fd058b6..290de43 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java
@@ -58,12 +58,10 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.SortedSet;
-import java.util.Stack;
 import java.util.TreeSet;
 
 /**
@@ -107,7 +105,6 @@
 
   private final SortedSet<ConfigurationProperty> configurationProperties;
   private final JJSOptions jjsOptions;
-  private final List<Class<? extends Linker>> linkerClasses;
   private final Map<Class<? extends Linker>, String> linkerShortNames = new HashMap<Class<? extends Linker>, String>();
 
   private final String moduleFunctionName;
@@ -117,17 +114,59 @@
   private final Map<String, StandardCompilationResult> resultsByStrongName = new HashMap<String, StandardCompilationResult>();
   private final SortedSet<SelectionProperty> selectionProperties;
 
+  private final Linker[] linkers;
+
   public StandardLinkerContext(TreeLogger logger, ModuleDef module,
-      JJSOptions jjsOptions) {
+      JJSOptions jjsOptions) throws UnableToCompleteException {
     logger = logger.branch(TreeLogger.DEBUG,
         "Constructing StandardLinkerContext", null);
 
     this.jjsOptions = jjsOptions;
     this.moduleFunctionName = module.getFunctionName();
     this.moduleName = module.getName();
-    this.linkerClasses = new ArrayList<Class<? extends Linker>>(
-        module.getActiveLinkers());
-    linkerClasses.add(module.getActivePrimaryLinker());
+
+    // Sort the linkers into the order they should actually run.
+    List<Class<? extends Linker>> sortedLinkers = new ArrayList<Class<? extends Linker>>();
+
+    // Get all the pre-linkers first.
+    for (Class<? extends Linker> linkerClass : module.getActiveLinkers()) {
+      Order order = linkerClass.getAnnotation(LinkerOrder.class).value();
+      assert (order != null);
+      if (order == Order.PRE) {
+        sortedLinkers.add(linkerClass);
+      }
+    }
+
+    // Get the primary linker.
+    sortedLinkers.add(module.getActivePrimaryLinker());
+
+    // Get all the post-linkers IN REVERSE ORDER.
+    {
+      List<Class<? extends Linker>> postLinkerClasses = new ArrayList<Class<? extends Linker>>();
+      for (Class<? extends Linker> linkerClass : module.getActiveLinkers()) {
+        Order order = linkerClass.getAnnotation(LinkerOrder.class).value();
+        assert (order != null);
+        if (order == Order.POST) {
+          postLinkerClasses.add(linkerClass);
+        }
+      }
+      Collections.reverse(postLinkerClasses);
+      sortedLinkers.addAll(postLinkerClasses);
+    }
+
+    linkers = new Linker[sortedLinkers.size()];
+    int i = 0;
+    for (Class<? extends Linker> linkerClass : sortedLinkers) {
+      try {
+        linkers[i++] = linkerClass.newInstance();
+      } catch (InstantiationException e) {
+        logger.log(TreeLogger.ERROR, "Unable to create Linker", e);
+        throw new UnableToCompleteException();
+      } catch (IllegalAccessException e) {
+        logger.log(TreeLogger.ERROR, "Unable to create Linker", e);
+        throw new UnableToCompleteException();
+      }
+    }
 
     for (Map.Entry<String, Class<? extends Linker>> entry : module.getLinkers().entrySet()) {
       linkerShortNames.put(entry.getValue(), entry.getKey());
@@ -262,77 +301,39 @@
   /**
    * Run the linker stack.
    */
-  public ArtifactSet invokeLinkerStack(TreeLogger logger)
+  public ArtifactSet invokeLink(TreeLogger logger)
       throws UnableToCompleteException {
     ArtifactSet workingArtifacts = new ArtifactSet(artifacts);
-    Stack<Linker> linkerStack = new Stack<Linker>();
 
-    EnumSet<Order> phasePre = EnumSet.of(Order.PRE, Order.PRIMARY);
-    EnumSet<Order> phasePost = EnumSet.of(Order.POST);
-
-    // Instantiate instances of the Linkers
-    for (Class<? extends Linker> clazz : linkerClasses) {
-      Linker linker;
-
-      // Create an instance of the Linker
-      try {
-        linker = clazz.newInstance();
-        linkerStack.push(linker);
-      } catch (InstantiationException e) {
-        logger.log(TreeLogger.ERROR, "Unable to create LinkerContextShim", e);
-        throw new UnableToCompleteException();
-      } catch (IllegalAccessException e) {
-        logger.log(TreeLogger.ERROR, "Unable to create LinkerContextShim", e);
-        throw new UnableToCompleteException();
-      }
-
-      // Detemine if we need to invoke the Linker in the current link phase
-      Order order = clazz.getAnnotation(LinkerOrder.class).value();
-      if (!phasePre.contains(order)) {
-        continue;
-      }
-
-      // The primary Linker is guaranteed to be last in the order
-      if (order == Order.PRIMARY) {
-        assert linkerClasses.get(linkerClasses.size() - 1).equals(clazz);
-      }
-
+    for (Linker linker : linkers) {
       TreeLogger linkerLogger = logger.branch(TreeLogger.TRACE,
           "Invoking Linker " + linker.getDescription(), null);
-
       workingArtifacts.freeze();
       try {
         workingArtifacts = linker.link(linkerLogger, this, workingArtifacts);
-      } catch (Exception e) {
+      } catch (Throwable e) {
         linkerLogger.log(TreeLogger.ERROR, "Failed to link", e);
         throw new UnableToCompleteException();
       }
     }
+    return workingArtifacts;
+  }
 
-    // Pop the primary linker off of the stack
-    linkerStack.pop();
+  public ArtifactSet invokeRelink(TreeLogger logger,
+      ArtifactSet newlyGeneratedArtifacts) throws UnableToCompleteException {
+    ArtifactSet workingArtifacts = new ArtifactSet(newlyGeneratedArtifacts);
 
-    // Unwind the stack
-    while (!linkerStack.isEmpty()) {
-      Linker linker = linkerStack.pop();
-      Class<? extends Linker> linkerType = linker.getClass();
-
-      // See if the Linker should be run in the current phase
-      Order order = linkerType.getAnnotation(LinkerOrder.class).value();
-      if (phasePost.contains(order)) {
-        TreeLogger linkerLogger = logger.branch(TreeLogger.TRACE,
-            "Invoking Linker " + linker.getDescription(), null);
-
-        workingArtifacts.freeze();
-        try {
-          workingArtifacts = linker.link(linkerLogger, this, workingArtifacts);
-        } catch (Exception e) {
-          linkerLogger.log(TreeLogger.ERROR, "Failed to link", e);
-          throw new UnableToCompleteException();
-        }
+    for (Linker linker : linkers) {
+      TreeLogger linkerLogger = logger.branch(TreeLogger.TRACE,
+          "Invoking relink on Linker " + linker.getDescription(), null);
+      workingArtifacts.freeze();
+      try {
+        workingArtifacts = linker.relink(linkerLogger, this, workingArtifacts);
+      } catch (Throwable e) {
+        linkerLogger.log(TreeLogger.ERROR, "Failed to relink", e);
+        throw new UnableToCompleteException();
       }
     }
-
     return workingArtifacts;
   }
 
@@ -402,11 +403,20 @@
     return out.toString();
   }
 
+  /**
+   * Writes artifacts into output directories in the standard way.
+   * 
+   * @param logger logs the operation
+   * @param artifacts the set of artifacts to write
+   * @param outputPath the output path for deployable artifacts
+   * @param extraPath optional extra path for non-deployable artifacts
+   * @throws UnableToCompleteException
+   */
   public void produceOutputDirectory(TreeLogger logger, ArtifactSet artifacts,
-      File moduleOutDir, File moduleAuxDir) throws UnableToCompleteException {
+      File outputPath, File extraPath) throws UnableToCompleteException {
 
-    logger = logger.branch(TreeLogger.INFO, "Linking compilation into "
-        + moduleOutDir.getPath(), null);
+    logger = logger.branch(TreeLogger.TRACE, "Linking compilation into "
+        + outputPath.getPath(), null);
 
     for (EmittedArtifact artifact : artifacts.find(EmittedArtifact.class)) {
       TreeLogger artifactLogger = logger.branch(TreeLogger.DEBUG,
@@ -414,13 +424,18 @@
 
       File outFile;
       if (artifact.isPrivate()) {
-        outFile = new File(getLinkerAuxDir(moduleAuxDir, artifact.getLinker()),
-            artifact.getPartialPath());
+        if (extraPath == null) {
+          continue;
+        }
+        outFile = new File(getExtraPathForLinker(extraPath,
+            artifact.getLinker()), artifact.getPartialPath());
       } else {
-        outFile = new File(moduleOutDir, artifact.getPartialPath());
+        outFile = new File(outputPath, artifact.getPartialPath());
       }
 
-      assert !outFile.exists() : "Attempted to overwrite " + outFile.getPath();
+      // TODO(scottb): figure out how to do a clean.
+      // assert !outFile.exists() : "Attempted to overwrite " +
+      // outFile.getPath();
       Util.copy(logger, artifact.getContents(artifactLogger), outFile);
     }
   }
@@ -429,15 +444,11 @@
    * Creates a linker-specific subdirectory in the module's auxiliary output
    * directory.
    */
-  private File getLinkerAuxDir(File moduleAuxDir,
+  private File getExtraPathForLinker(File extraPath,
       Class<? extends Linker> linkerType) {
-    // The auxiliary directory is create lazily
-    if (!moduleAuxDir.exists()) {
-      moduleAuxDir.mkdirs();
-    }
     assert linkerShortNames.containsKey(linkerType) : linkerType.getName()
         + " unknown";
-    File toReturn = new File(moduleAuxDir, linkerShortNames.get(linkerType));
+    File toReturn = new File(extraPath, linkerShortNames.get(linkerType));
     if (!toReturn.exists()) {
       toReturn.mkdirs();
     }
diff --git a/dev/core/src/com/google/gwt/dev/CompileArgProcessor.java b/dev/core/src/com/google/gwt/dev/CompileArgProcessor.java
new file mode 100644
index 0000000..d19ae45
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/CompileArgProcessor.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev;
+
+import com.google.gwt.dev.util.arg.ArgHandlerLogLevel;
+import com.google.gwt.dev.util.arg.ArgHandlerModuleName;
+import com.google.gwt.dev.util.arg.ArgHandlerOutDir;
+import com.google.gwt.dev.util.arg.ArgHandlerTreeLoggerFlag;
+import com.google.gwt.util.tools.ToolBase;
+
+abstract class CompileArgProcessor extends ToolBase {
+  public CompileArgProcessor(CompileTaskOptions options) {
+    registerHandler(new ArgHandlerLogLevel(options));
+    registerHandler(new ArgHandlerTreeLoggerFlag(options));
+    registerHandler(new ArgHandlerOutDir(options));
+    registerHandler(new ArgHandlerModuleName(options));
+  }
+
+  /*
+   * Overridden to make public.
+   */
+  @Override
+  public final boolean processArgs(String[] args) {
+    return super.processArgs(args);
+  }
+
+  @Override
+  protected abstract String getName();
+}
diff --git a/dev/core/src/com/google/gwt/dev/CompilePerms.java b/dev/core/src/com/google/gwt/dev/CompilePerms.java
index 17b1042..997fb74 100644
--- a/dev/core/src/com/google/gwt/dev/CompilePerms.java
+++ b/dev/core/src/com/google/gwt/dev/CompilePerms.java
@@ -116,7 +116,8 @@
       return true;
     }
   }
-  static class ArgProcessor extends Link.ArgProcessor {
+
+  static class ArgProcessor extends CompileArgProcessor {
     public ArgProcessor(CompilePermsOptions options) {
       super(options);
       registerHandler(new ArgHandlerPerms(options));
@@ -129,7 +130,7 @@
   }
 
   /**
-   * Concrete class to implement all compiler options.
+   * Concrete class to implement compiler perm options.
    */
   static class CompilePermsOptionsImpl extends CompileTaskOptionsImpl implements
       CompilePermsOptions {
diff --git a/dev/core/src/com/google/gwt/dev/CompileTaskOptionsImpl.java b/dev/core/src/com/google/gwt/dev/CompileTaskOptionsImpl.java
index b2f0295..9c093c0 100644
--- a/dev/core/src/com/google/gwt/dev/CompileTaskOptionsImpl.java
+++ b/dev/core/src/com/google/gwt/dev/CompileTaskOptionsImpl.java
@@ -24,14 +24,13 @@
  */
 class CompileTaskOptionsImpl implements CompileTaskOptions {
 
-  public static final String GWT_COMPILER_DIR = ".gwt-tmp" + File.separatorChar
-      + "compiler";
+  public static final String GWT_TMP_DIR = "gwt-tmp";
 
-  private File compilerWorkDir;
   private Type logLevel;
   private String moduleName;
   private File outDir;
   private boolean useGuiLogger;
+  private File workDir;
 
   public CompileTaskOptionsImpl() {
   }
@@ -48,11 +47,7 @@
   }
 
   public File getCompilerWorkDir() {
-    if (compilerWorkDir == null) {
-      compilerWorkDir = new File(getOutDir(), GWT_COMPILER_DIR + File.separator
-          + moduleName);
-    }
-    return compilerWorkDir;
+    return new File(new File(getWorkDir(), getModuleName()), "compiler");
   }
 
   public Type getLogLevel() {
@@ -87,4 +82,14 @@
     this.useGuiLogger = useGuiLogger;
   }
 
-}
\ No newline at end of file
+  /**
+   * TODO: add a command line option to pass files between compile phases?
+   */
+  protected File getWorkDir() {
+    if (workDir == null) {
+      workDir = new File(System.getProperty("java.io.tmpdir"), GWT_TMP_DIR);
+      workDir.mkdirs();
+    }
+    return workDir;
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/CompilerOptions.java b/dev/core/src/com/google/gwt/dev/CompilerOptions.java
index 85b4afc..0b568e4 100644
--- a/dev/core/src/com/google/gwt/dev/CompilerOptions.java
+++ b/dev/core/src/com/google/gwt/dev/CompilerOptions.java
@@ -15,13 +15,11 @@
  */
 package com.google.gwt.dev;
 
-import com.google.gwt.dev.jjs.JJSOptions;
-import com.google.gwt.dev.util.arg.OptionGenDir;
-import com.google.gwt.dev.util.arg.OptionValidateOnly;
+import com.google.gwt.dev.Link.LinkOptions;
+import com.google.gwt.dev.Precompile.PrecompileOptions;
 
 /**
  * The complete set of options for the GWT compiler.
  */
-public interface CompilerOptions extends JJSOptions, CompileTaskOptions,
-    OptionGenDir, OptionValidateOnly {
+public interface CompilerOptions extends PrecompileOptions, LinkOptions {
 }
diff --git a/dev/core/src/com/google/gwt/dev/CompilerOptionsImpl.java b/dev/core/src/com/google/gwt/dev/CompilerOptionsImpl.java
deleted file mode 100644
index dcc29bf..0000000
--- a/dev/core/src/com/google/gwt/dev/CompilerOptionsImpl.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright 2008 Google Inc.
- * 
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- * 
- * http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.google.gwt.dev;
-
-import com.google.gwt.core.ext.TreeLogger.Type;
-import com.google.gwt.dev.jjs.JJSOptionsImpl;
-
-import java.io.File;
-
-/**
- * Concrete class to implement all compiler options.
- */
-public class CompilerOptionsImpl extends JJSOptionsImpl implements
-    CompilerOptions {
-  private File genDir;
-  private Type logLevel;
-  private String moduleName;
-  private File outDir;
-  private boolean useGuiLogger;
-  private boolean validateOnly;
-
-  public CompilerOptionsImpl() {
-  }
-
-  public CompilerOptionsImpl(CompilerOptions other) {
-    copyFrom(other);
-  }
-
-  public void copyFrom(CompilerOptions other) {
-    super.copyFrom(other);
-    setGenDir(other.getGenDir());
-    setLogLevel(other.getLogLevel());
-    setOutDir(other.getOutDir());
-    setUseGuiLogger(other.isUseGuiLogger());
-    setValidateOnly(false);
-  }
-
-  public File getGenDir() {
-    return genDir;
-  }
-
-  public Type getLogLevel() {
-    return logLevel;
-  }
-
-  public String getModuleName() {
-    return moduleName;
-  }
-
-  public File getOutDir() {
-    return outDir;
-  }
-
-  public boolean isUseGuiLogger() {
-    return useGuiLogger;
-  }
-
-  public boolean isValidateOnly() {
-    return validateOnly;
-  }
-
-  public void setGenDir(File genDir) {
-    this.genDir = genDir;
-  }
-
-  public void setLogLevel(Type logLevel) {
-    this.logLevel = logLevel;
-  }
-
-  public void setModuleName(String moduleName) {
-    this.moduleName = moduleName;
-  }
-
-  public void setOutDir(File outDir) {
-    this.outDir = outDir;
-  }
-
-  public void setUseGuiLogger(boolean useGuiLogger) {
-    this.useGuiLogger = useGuiLogger;
-  }
-
-  public void setValidateOnly(boolean validateOnly) {
-    this.validateOnly = validateOnly;
-  }
-}
\ No newline at end of file
diff --git a/dev/core/src/com/google/gwt/dev/GWTCompiler.java b/dev/core/src/com/google/gwt/dev/GWTCompiler.java
index 3f89bfd..9e90603 100644
--- a/dev/core/src/com/google/gwt/dev/GWTCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/GWTCompiler.java
@@ -19,7 +19,10 @@
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.dev.CompilePerms.CompilePermsOptionsImpl;
 import com.google.gwt.dev.CompileTaskRunner.CompileTask;
-import com.google.gwt.dev.Precompile.CompilerOptionsImpl;
+import com.google.gwt.dev.Precompile.PrecompileOptionsImpl;
+import com.google.gwt.dev.util.arg.ArgHandlerExtraDir;
+
+import java.io.File;
 
 /**
  * The main executable entry point for the GWT Java to JavaScript compiler.
@@ -29,6 +32,7 @@
   static final class ArgProcessor extends Precompile.ArgProcessor {
     public ArgProcessor(CompilerOptions options) {
       super(options);
+      registerHandler(new ArgHandlerExtraDir(options));
     }
 
     @Override
@@ -37,6 +41,32 @@
     }
   }
 
+  static class GWTCompilerOptionsImpl extends PrecompileOptionsImpl implements
+      CompilerOptions {
+
+    private File extraDir;
+
+    public GWTCompilerOptionsImpl() {
+    }
+
+    public GWTCompilerOptionsImpl(CompilerOptions other) {
+      copyFrom(other);
+    }
+
+    public void copyFrom(CompilerOptions other) {
+      super.copyFrom(other);
+      setExtraDir(other.getExtraDir());
+    }
+
+    public File getExtraDir() {
+      return extraDir;
+    }
+
+    public void setExtraDir(File extraDir) {
+      this.extraDir = extraDir;
+    }
+  }
+
   public static void main(String[] args) {
     /*
      * NOTE: main always exits with a call to System.exit to terminate any
@@ -44,7 +74,7 @@
      * shutdown AWT related threads, since the contract for their termination is
      * still implementation-dependent.
      */
-    final CompilerOptions options = new CompilerOptionsImpl();
+    final CompilerOptions options = new GWTCompilerOptionsImpl();
     if (new ArgProcessor(options).processArgs(args)) {
       CompileTask task = new CompileTask() {
         public boolean run(TreeLogger logger) throws UnableToCompleteException {
@@ -60,10 +90,10 @@
     System.exit(1);
   }
 
-  private final CompilerOptionsImpl options;
+  private final GWTCompilerOptionsImpl options;
 
   public GWTCompiler(CompilerOptions options) {
-    this.options = new CompilerOptionsImpl(options);
+    this.options = new GWTCompilerOptionsImpl(options);
   }
 
   public boolean run(TreeLogger logger) throws UnableToCompleteException {
diff --git a/dev/core/src/com/google/gwt/dev/GWTHosted.java b/dev/core/src/com/google/gwt/dev/GWTHosted.java
new file mode 100644
index 0000000..c8da35e
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/GWTHosted.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.dev.cfg.ModuleDef;
+import com.google.gwt.dev.cfg.ModuleDefLoader;
+import com.google.gwt.dev.shell.GWTShellServletFilter;
+import com.google.gwt.dev.shell.ServletContainer;
+import com.google.gwt.dev.shell.ShellModuleSpaceHost;
+import com.google.gwt.dev.shell.jetty.JettyLauncher;
+import com.google.gwt.dev.util.PerfLogger;
+import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
+import com.google.gwt.util.tools.ArgHandlerExtra;
+import com.google.gwt.util.tools.ArgHandlerString;
+
+import java.io.PrintWriter;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * The main executable class for the hosted mode shell.
+ */
+public class GWTHosted extends GWTShell {
+
+  /**
+   * Handles the set of modules that can be passed at the end of the command
+   * line.
+   */
+  protected class ArgHandlerModulesExtra extends ArgHandlerExtra {
+
+    private final PrintWriterTreeLogger console = new PrintWriterTreeLogger(
+        new PrintWriter(System.err));
+    {
+      console.setMaxDetail(TreeLogger.WARN);
+    }
+
+    @Override
+    public boolean addExtraArg(String arg) {
+      return addModule(console, arg);
+    }
+
+    @Override
+    public String getPurpose() {
+      return "Specifies the set of modules to host";
+    }
+
+    @Override
+    public String[] getTagArgs() {
+      return new String[] {"module"};
+    }
+  }
+
+  /**
+   * Handles a startup url that can be passed on the command line.
+   */
+  protected class ArgHandlerStartupURLs extends ArgHandlerString {
+
+    @Override
+    public String getPurpose() {
+      return "Automatically launches the specified URL";
+    }
+
+    @Override
+    public String getTag() {
+      return "-startupUrl";
+    }
+
+    @Override
+    public String[] getTagArgs() {
+      return new String[] {"url"};
+    }
+
+    @Override
+    public boolean setString(String arg) {
+      addStartupURL(arg);
+      return true;
+    }
+  }
+
+  public static void main(String[] args) {
+    /*
+     * NOTE: main always exits with a call to System.exit to terminate any
+     * non-daemon threads that were started in Generators. Typically, this is to
+     * shutdown AWT related threads, since the contract for their termination is
+     * still implementation-dependent.
+     */
+    BootStrapPlatform.init();
+    GWTHosted shellMain = new GWTHosted();
+    if (shellMain.processArgs(args)) {
+      shellMain.run();
+    }
+    System.exit(0);
+  }
+
+  private Set<ModuleDef> modules = new HashSet<ModuleDef>();
+
+  private ServletContainer server;
+
+  private GWTShellServletFilter servletFilter;
+
+  public GWTHosted() {
+    super(false, true);
+    registerHandler(new ArgHandlerStartupURLs());
+    registerHandler(new ArgHandlerModulesExtra());
+  }
+
+  public boolean addModule(TreeLogger logger, String moduleName) {
+    try {
+      ModuleDef moduleDef = ModuleDefLoader.loadFromClassPath(logger,
+          moduleName);
+      modules.add(moduleDef);
+      return true;
+    } catch (UnableToCompleteException e) {
+      System.err.println("Unable to load module '" + moduleName + "'");
+      return false;
+    }
+  }
+
+  @Override
+  protected ShellModuleSpaceHost doCreateShellModuleSpaceHost(
+      TreeLogger logger, TypeOracle typeOracle, ModuleDef moduleDef) {
+    return new ShellModuleSpaceHost(logger, typeOracle, moduleDef,
+        options.getGenDir(), options.getShellWorkDir(moduleDef), servletFilter);
+  }
+
+  @Override
+  protected void shutDown() {
+    if (server != null) {
+      try {
+        server.stop();
+      } catch (UnableToCompleteException e) {
+        // Already logged.
+      }
+      server = null;
+    }
+  }
+
+  @Override
+  protected int startUpServer() {
+    PerfLogger.start("GWTShell.startup (Jetty launch)");
+    JettyLauncher launcher = new JettyLauncher();
+    try {
+      TreeLogger serverLogger = getTopLogger().branch(TreeLogger.INFO,
+          "Starting HTTP on port " + getPort(), null);
+      ModuleDef[] moduleArray = modules.toArray(new ModuleDef[modules.size()]);
+      for (ModuleDef moduleDef : moduleArray) {
+        String[] servletPaths = moduleDef.getServletPaths();
+        if (servletPaths.length > 0) {
+          serverLogger.log(TreeLogger.WARN,
+              "Ignoring legacy <servlet> tag(s) in module '"
+                  + moduleDef.getName()
+                  + "'; add servlet tags to your web.xml instead");
+        }
+      }
+      servletFilter = new GWTShellServletFilter(serverLogger, options,
+          moduleArray);
+      server = launcher.start(serverLogger, getPort(), options.getOutDir(),
+          servletFilter);
+    } catch (UnableToCompleteException e) {
+      PerfLogger.end();
+      return -1;
+    }
+    assert (server != null);
+
+    PerfLogger.end();
+    return server.getPort();
+  }
+
+}
diff --git a/dev/core/src/com/google/gwt/dev/GWTShell.java b/dev/core/src/com/google/gwt/dev/GWTShell.java
index 7a56d3d..24ad277 100644
--- a/dev/core/src/com/google/gwt/dev/GWTShell.java
+++ b/dev/core/src/com/google/gwt/dev/GWTShell.java
@@ -19,7 +19,7 @@
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.TreeLogger.Type;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
-import com.google.gwt.dev.Precompile.CompilerOptionsImpl;
+import com.google.gwt.dev.GWTCompiler.GWTCompilerOptionsImpl;
 import com.google.gwt.dev.cfg.ModuleDef;
 import com.google.gwt.dev.cfg.ModuleDefLoader;
 import com.google.gwt.dev.shell.BrowserWidget;
@@ -30,10 +30,11 @@
 import com.google.gwt.dev.shell.PlatformSpecific;
 import com.google.gwt.dev.shell.ShellMainWindow;
 import com.google.gwt.dev.shell.ShellModuleSpaceHost;
+import com.google.gwt.dev.shell.WorkDirs;
 import com.google.gwt.dev.shell.tomcat.EmbeddedTomcatServer;
-import com.google.gwt.dev.util.PerfLogger;
 import com.google.gwt.dev.util.arg.ArgHandlerDisableAggressiveOptimization;
 import com.google.gwt.dev.util.arg.ArgHandlerEnableAssertions;
+import com.google.gwt.dev.util.arg.ArgHandlerExtraDir;
 import com.google.gwt.dev.util.arg.ArgHandlerGenDir;
 import com.google.gwt.dev.util.arg.ArgHandlerLogLevel;
 import com.google.gwt.dev.util.arg.ArgHandlerOutDir;
@@ -160,9 +161,10 @@
   }
 
   /**
-   * Handles the list of startup urls that can be passed on the command line.
+   * Handles the list of startup urls that can be passed at the end of the
+   * command line.
    */
-  protected class ArgHandlerStartupURLs extends ArgHandlerExtra {
+  protected class ArgHandlerStartupURLsExtra extends ArgHandlerExtra {
 
     @Override
     public boolean addExtraArg(String arg) {
@@ -212,6 +214,20 @@
     }
   }
 
+  /**
+   * Concrete class to implement all compiler options.
+   */
+  static class ShellOptionsImpl extends GWTCompilerOptionsImpl implements
+      ShellOptions, WorkDirs {
+    public File getCompilerOutputDir(ModuleDef moduleDef) {
+      return new File(getWorkDir(), moduleDef.getDeployTo());
+    }
+
+    public File getShellWorkDir(ModuleDef moduleDef) {
+      return new File(new File(getWorkDir(), moduleDef.getName()), "shell");
+    }
+  }
+
   private class BrowserWidgetHostImpl implements BrowserWidgetHost {
     public BrowserWidgetHostImpl() {
     }
@@ -244,14 +260,9 @@
         ModuleDef moduleDef = loadModule(moduleName, logger);
         assert (moduleDef != null);
 
-        // Create a sandbox for the module.
-        //
-        File shellDir = new File(options.getOutDir(), GWT_SHELL_PATH
-            + File.separator + moduleName);
-
         TypeOracle typeOracle = moduleDef.getTypeOracle(logger);
         ShellModuleSpaceHost host = doCreateShellModuleSpaceHost(logger,
-            typeOracle, moduleDef, options.getGenDir(), shellDir);
+            typeOracle, moduleDef);
         return host;
       } finally {
         Cursor normalCursor = display.getSystemCursor(SWT.CURSOR_ARROW);
@@ -291,9 +302,6 @@
     }
   }
 
-  public static final String GWT_SHELL_PATH = ".gwt-tmp" + File.separator
-      + "shell";
-
   private static Image[] icons;
 
   static {
@@ -314,7 +322,7 @@
   }
 
   public static String computeHostRegex(String url) {
-    // the enture URL up to the first slash not prefixed by a slash or colon.
+    // the entire URL up to the first slash not prefixed by a slash or colon.
     String raw = url.split("(?<![:/])/")[0];
     // escape the dots and put a begin line specifier on the result
     return "^" + raw.replaceAll("[.]", "[.]");
@@ -365,6 +373,8 @@
    */
   protected final Display display = Display.getDefault();
 
+  protected final ShellOptionsImpl options = new ShellOptionsImpl();
+
   /**
    * Cheat on the first load's refresh by assuming the module loaded by
    * {@link com.google.gwt.dev.shell.GWTShellServlet} is still fresh. This
@@ -381,8 +391,6 @@
 
   private ShellMainWindow mainWnd;
 
-  private final CompilerOptionsImpl options = new CompilerOptionsImpl();
-
   private int port;
 
   private boolean runTomcat = true;
@@ -408,17 +416,16 @@
     registerHandler(new ArgHandlerLogLevel(options));
 
     registerHandler(new ArgHandlerGenDir(options));
+    registerHandler(new ArgHandlerExtraDir(options));
 
     if (!noURLs) {
-      registerHandler(new ArgHandlerStartupURLs());
+      registerHandler(new ArgHandlerStartupURLsExtra());
     }
 
     registerHandler(new ArgHandlerOutDir(options));
 
     registerHandler(new ArgHandlerScriptStyle(options));
-
     registerHandler(new ArgHandlerEnableAssertions(options));
-
     registerHandler(new ArgHandlerDisableAggressiveOptimization(options));
   }
 
@@ -433,7 +440,7 @@
   }
 
   public CompilerOptions getCompilerOptions() {
-    return new CompilerOptionsImpl(options);
+    return new GWTCompilerOptionsImpl(options);
   }
 
   public int getPort() {
@@ -578,7 +585,7 @@
    */
   protected void compile(TreeLogger logger, ModuleDef moduleDef)
       throws UnableToCompleteException {
-    CompilerOptions newOptions = new CompilerOptionsImpl(options);
+    CompilerOptions newOptions = new GWTCompilerOptionsImpl(options);
     newOptions.setModuleName(moduleDef.getName());
     new GWTCompiler(newOptions).run(logger);
   }
@@ -595,10 +602,9 @@
    * @return ShellModuleSpaceHost instance
    */
   protected ShellModuleSpaceHost doCreateShellModuleSpaceHost(
-      TreeLogger logger, TypeOracle typeOracle, ModuleDef moduleDef,
-      File genDir, File shellDir) {
-    return new ShellModuleSpaceHost(logger, typeOracle, moduleDef, genDir,
-        shellDir);
+      TreeLogger logger, TypeOracle typeOracle, ModuleDef moduleDef) {
+    return new ShellModuleSpaceHost(logger, typeOracle, moduleDef,
+        options.getGenDir(), options.getShellWorkDir(moduleDef), null);
   }
 
   /**
@@ -712,29 +718,30 @@
     initializeLogger();
 
     if (runTomcat) {
-      // Start the HTTP server.
-      // Use a new thread so that logging that occurs during startup is
-      // displayed immediately.
-      //
-      final int serverPort = getPort();
-
-      PerfLogger.start("GWTShell.startup (Tomcat launch)");
-      String whyFailed = EmbeddedTomcatServer.start(getTopLogger(), serverPort,
-          options.getOutDir());
-      PerfLogger.end();
-
-      if (whyFailed != null) {
-        System.err.println(whyFailed);
+      int resultPort = startUpServer();
+      if (resultPort < 0) {
         return false;
       }
-
-      // Record what port Tomcat is actually running on.
-      port = EmbeddedTomcatServer.getPort();
+      port = resultPort;
     }
 
     return true;
   }
 
+  protected int startUpServer() {
+    // TODO(bruce): make tomcat work in terms of the modular launcher
+    String whyFailed = EmbeddedTomcatServer.start(getTopLogger(), getPort(),
+        options);
+
+    // TODO(bruce): test that we can remove this old approach in favor of
+    // a better, logger-based error reporting
+    if (whyFailed != null) {
+      System.err.println(whyFailed);
+      return -1;
+    }
+    return EmbeddedTomcatServer.getPort();
+  }
+
   private Shell createTrackedBrowserShell() {
     final Shell shell = new Shell(display);
     FillLayout fillLayout = new FillLayout();
diff --git a/dev/core/src/com/google/gwt/dev/Link.java b/dev/core/src/com/google/gwt/dev/Link.java
index 2bc55cd..d458108 100644
--- a/dev/core/src/com/google/gwt/dev/Link.java
+++ b/dev/core/src/com/google/gwt/dev/Link.java
@@ -27,11 +27,8 @@
 import com.google.gwt.dev.cfg.ModuleDefLoader;
 import com.google.gwt.dev.cfg.StaticPropertyOracle;
 import com.google.gwt.dev.util.Util;
-import com.google.gwt.dev.util.arg.ArgHandlerLogLevel;
-import com.google.gwt.dev.util.arg.ArgHandlerModuleName;
-import com.google.gwt.dev.util.arg.ArgHandlerOutDir;
-import com.google.gwt.dev.util.arg.ArgHandlerTreeLoggerFlag;
-import com.google.gwt.util.tools.ToolBase;
+import com.google.gwt.dev.util.arg.ArgHandlerExtraDir;
+import com.google.gwt.dev.util.arg.OptionExtraDir;
 
 import java.io.File;
 import java.util.HashMap;
@@ -42,21 +39,16 @@
  * to compile, and a ready-to-compile AST.
  */
 public class Link {
+  /**
+   * Options for Link.
+   */
+  public interface LinkOptions extends CompileTaskOptions, OptionExtraDir {
+  }
 
-  static class ArgProcessor extends ToolBase {
-    public ArgProcessor(CompileTaskOptions options) {
-      registerHandler(new ArgHandlerLogLevel(options));
-      registerHandler(new ArgHandlerTreeLoggerFlag(options));
-      registerHandler(new ArgHandlerOutDir(options));
-      registerHandler(new ArgHandlerModuleName(options));
-    }
-
-    /*
-     * Overridden to make public.
-     */
-    @Override
-    public boolean processArgs(String[] args) {
-      return super.processArgs(args);
+  static class ArgProcessor extends CompileArgProcessor {
+    public ArgProcessor(LinkOptions options) {
+      super(options);
+      registerHandler(new ArgHandlerExtraDir(options));
     }
 
     @Override
@@ -65,6 +57,35 @@
     }
   }
 
+  /**
+   * Concrete class to implement link options.
+   */
+  static class LinkOptionsImpl extends CompileTaskOptionsImpl implements
+      LinkOptions {
+
+    private File extraDir;
+
+    public LinkOptionsImpl() {
+    }
+
+    public LinkOptionsImpl(LinkOptions other) {
+      copyFrom(other);
+    }
+
+    public void copyFrom(LinkOptions other) {
+      super.copyFrom(other);
+      setExtraDir(other.getExtraDir());
+    }
+
+    public File getExtraDir() {
+      return extraDir;
+    }
+
+    public void setExtraDir(File extraDir) {
+      this.extraDir = extraDir;
+    }
+  }
+
   public static ArtifactSet link(TreeLogger logger, ModuleDef module,
       Precompilation precompilation, File[] jsFiles)
       throws UnableToCompleteException {
@@ -80,7 +101,7 @@
      * shutdown AWT related threads, since the contract for their termination is
      * still implementation-dependent.
      */
-    final CompileTaskOptions options = new CompileTaskOptionsImpl();
+    final LinkOptions options = new LinkOptionsImpl();
     if (new ArgProcessor(options).processArgs(args)) {
       CompileTask task = new CompileTask() {
         public boolean run(TreeLogger logger) throws UnableToCompleteException {
@@ -110,7 +131,7 @@
     }
 
     linkerContext.addOrReplaceArtifacts(precompilation.getGeneratedArtifacts());
-    return linkerContext.invokeLinkerStack(logger);
+    return linkerContext.invokeLink(logger);
   }
 
   private static void finishPermuation(TreeLogger logger, Permutation perm,
@@ -143,17 +164,17 @@
   /**
    * This is the output directory for private files.
    */
-  private File moduleAuxDir;
+  private File moduleExtraDir;
 
   /**
    * This is the output directory for public files.
    */
   private File moduleOutDir;
 
-  private final CompileTaskOptionsImpl options;
+  private final LinkOptionsImpl options;
 
-  public Link(CompileTaskOptions options) {
-    this.options = new CompileTaskOptionsImpl(options);
+  public Link(LinkOptions options) {
+    this.options = new LinkOptionsImpl(options);
   }
 
   public boolean run(TreeLogger logger) throws UnableToCompleteException {
@@ -197,7 +218,7 @@
         jsFiles);
     if (artifacts != null) {
       linkerContext.produceOutputDirectory(branch, artifacts, moduleOutDir,
-          moduleAuxDir);
+          moduleExtraDir);
       branch.log(TreeLogger.INFO, "Link succeeded");
       return true;
     }
@@ -207,9 +228,11 @@
 
   private void init(TreeLogger logger) throws UnableToCompleteException {
     module = ModuleDefLoader.loadFromClassPath(logger, options.getModuleName());
-    moduleOutDir = new File(options.getOutDir(), module.getName());
+    moduleOutDir = new File(options.getOutDir(), module.getDeployTo());
     Util.recursiveDelete(moduleOutDir, true);
-    moduleAuxDir = new File(options.getOutDir(), module.getName() + "-aux");
-    Util.recursiveDelete(moduleAuxDir, false);
+    if (options.getExtraDir() != null) {
+      moduleExtraDir = new File(options.getExtraDir(), module.getDeployTo());
+      Util.recursiveDelete(moduleExtraDir, false);
+    }
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/Precompile.java b/dev/core/src/com/google/gwt/dev/Precompile.java
index 8465ac5..0dd403a 100644
--- a/dev/core/src/com/google/gwt/dev/Precompile.java
+++ b/dev/core/src/com/google/gwt/dev/Precompile.java
@@ -30,11 +30,11 @@
 import com.google.gwt.dev.jdt.RebindOracle;
 import com.google.gwt.dev.jdt.RebindPermutationOracle;
 import com.google.gwt.dev.jdt.WebModeCompilerFrontEnd;
-import com.google.gwt.dev.jjs.UnifiedAst;
 import com.google.gwt.dev.jjs.JJSOptions;
 import com.google.gwt.dev.jjs.JJSOptionsImpl;
 import com.google.gwt.dev.jjs.JavaToJavaScriptCompiler;
 import com.google.gwt.dev.jjs.JsOutputOption;
+import com.google.gwt.dev.jjs.UnifiedAst;
 import com.google.gwt.dev.shell.StandardRebindOracle;
 import com.google.gwt.dev.util.Util;
 import com.google.gwt.dev.util.arg.ArgHandlerDisableAggressiveOptimization;
@@ -42,6 +42,8 @@
 import com.google.gwt.dev.util.arg.ArgHandlerGenDir;
 import com.google.gwt.dev.util.arg.ArgHandlerScriptStyle;
 import com.google.gwt.dev.util.arg.ArgHandlerValidateOnlyFlag;
+import com.google.gwt.dev.util.arg.OptionGenDir;
+import com.google.gwt.dev.util.arg.OptionValidateOnly;
 
 import java.io.File;
 import java.util.HashSet;
@@ -56,8 +58,15 @@
  */
 public class Precompile {
 
-  static class ArgProcessor extends Link.ArgProcessor {
-    public ArgProcessor(CompilerOptions options) {
+  /**
+   * The set of options for the precompiler.
+   */
+  public interface PrecompileOptions extends JJSOptions, CompileTaskOptions,
+      OptionGenDir, OptionValidateOnly {
+  }
+
+  static class ArgProcessor extends CompileArgProcessor {
+    public ArgProcessor(PrecompileOptions options) {
       super(options);
       registerHandler(new ArgHandlerGenDir(options));
       registerHandler(new ArgHandlerScriptStyle(options));
@@ -71,32 +80,27 @@
       return Precompile.class.getName();
     }
   }
-  /**
-   * Concrete class to implement all compiler options.
-   */
-  static class CompilerOptionsImpl extends CompileTaskOptionsImpl implements
-      CompilerOptions {
 
+  static class PrecompileOptionsImpl extends CompileTaskOptionsImpl implements
+      PrecompileOptions {
     private File genDir;
     private final JJSOptionsImpl jjsOptions = new JJSOptionsImpl();
     private boolean validateOnly;
 
-    public CompilerOptionsImpl() {
+    public PrecompileOptionsImpl() {
     }
 
-    public CompilerOptionsImpl(CompilerOptions other) {
+    public PrecompileOptionsImpl(PrecompileOptions other) {
       copyFrom(other);
     }
 
-    public void copyFrom(CompilerOptions other) {
+    public void copyFrom(PrecompileOptions other) {
       super.copyFrom(other);
 
+      jjsOptions.copyFrom(other);
+
       setGenDir(other.getGenDir());
       setValidateOnly(other.isValidateOnly());
-
-      setAggressivelyOptimize(other.isAggressivelyOptimize());
-      setEnableAssertions(other.isEnableAssertions());
-      setOutput(other.getOutput());
     }
 
     public File getGenDir() {
@@ -217,7 +221,7 @@
      * shutdown AWT related threads, since the contract for their termination is
      * still implementation-dependent.
      */
-    final CompilerOptions options = new CompilerOptionsImpl();
+    final PrecompileOptions options = new PrecompileOptionsImpl();
     if (new ArgProcessor(options).processArgs(args)) {
       CompileTask task = new CompileTask() {
         public boolean run(TreeLogger logger) throws UnableToCompleteException {
@@ -328,14 +332,12 @@
     }
   }
 
-  private File generatorResourcesDir;
-
   private ModuleDef module;
 
-  private final CompilerOptionsImpl options;
+  private final PrecompileOptionsImpl options;
 
-  public Precompile(CompilerOptions options) {
-    this.options = new CompilerOptionsImpl(options);
+  public Precompile(PrecompileOptions options) {
+    this.options = new PrecompileOptionsImpl(options);
   }
 
   public boolean run(TreeLogger logger) throws UnableToCompleteException {
@@ -344,7 +346,7 @@
       TreeLogger branch = logger.branch(TreeLogger.INFO,
           "Validating compilation " + module.getName());
       if (validate(branch, options, module, options.getGenDir(),
-          generatorResourcesDir)) {
+          options.getCompilerWorkDir())) {
         branch.log(TreeLogger.INFO, "Validation succeeded");
         return true;
       } else {
@@ -356,7 +358,7 @@
       TreeLogger branch = logger.branch(TreeLogger.INFO, "Precompiling module "
           + module.getName());
       Precompilation precompilation = precompile(branch, options, module,
-          options.getGenDir(), generatorResourcesDir);
+          options.getGenDir(), options.getCompilerWorkDir());
       if (precompilation != null) {
         Util.writeObjectAsFile(branch, new File(options.getCompilerWorkDir(),
             PRECOMPILATION_FILENAME), precompilation);
@@ -382,10 +384,6 @@
     this.module = ModuleDefLoader.loadFromClassPath(logger,
         options.getModuleName());
 
-    // Place generated resources inside the work dir.
-    generatorResourcesDir = new File(compilerWorkDir, "generated");
-    generatorResourcesDir.mkdirs();
-
     // TODO: All JDT checks now before even building TypeOracle?
     module.getCompilationState().compile(logger);
   }
diff --git a/dev/core/src/com/google/gwt/dev/ShellOptions.java b/dev/core/src/com/google/gwt/dev/ShellOptions.java
new file mode 100644
index 0000000..71ea4e8
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/ShellOptions.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev;
+
+import com.google.gwt.dev.jjs.JJSOptions;
+import com.google.gwt.dev.util.arg.OptionExtraDir;
+import com.google.gwt.dev.util.arg.OptionGenDir;
+import com.google.gwt.dev.util.arg.OptionLogLevel;
+import com.google.gwt.dev.util.arg.OptionOutDir;
+
+/**
+ * The complete set of options for the GWT compiler.
+ */
+public interface ShellOptions extends JJSOptions, OptionLogLevel, OptionOutDir,
+    OptionGenDir, OptionExtraDir {
+}
diff --git a/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java b/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java
index 3d6efb6..b4e4b1e 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java
@@ -87,6 +87,8 @@
 
   private CompilationState compilationState;
 
+  private String deployTo;
+
   private final List<String> entryPointTypeNames = new ArrayList<String>();
 
   private final Set<File> gwtXmlFiles = new HashSet<File>();
@@ -264,6 +266,17 @@
     return compilationState;
   }
 
+  /**
+   * Returns the desired deployment path within the output directory. The
+   * returned value will start and end with a <code>'/'</code> character.
+   */
+  public String getDeployTo() {
+    String result = (deployTo == null) ? ('/' + getName() + '/') : deployTo;
+    assert result.startsWith("/");
+    assert result.endsWith("/");
+    return result;
+  }
+
   public synchronized String[] getEntryPointTypeNames() {
     final int n = entryPointTypeNames.size();
     return entryPointTypeNames.toArray(new String[n]);
@@ -368,6 +381,26 @@
   }
 
   /**
+   * Set the deployment path for this module. Setting this value to
+   * <code>null</code> or the empty string will default to the fully-qualified
+   * module name.
+   */
+  public void setDeployTo(String deployTo) {
+    if (deployTo != null && deployTo.length() == 0) {
+      deployTo = null;
+    } else {
+      assert deployTo.startsWith("/");
+      // Ensure ends with trailing slash.
+      if (!deployTo.endsWith("/")) {
+        deployTo += '/';
+      }
+      assert deployTo.endsWith("/");
+    }
+
+    this.deployTo = deployTo;
+  }
+
+  /**
    * Override the module's apparent name. Setting this value to
    * <code>null<code> will disable the name override.
    */
diff --git a/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java b/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java
index a0c2e94..bd394eb 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java
@@ -921,6 +921,7 @@
   }
 
   protected final String __module_1_rename_to = "";
+  protected final String __module_2_deploy_to = "";
 
   private final PropertyAttrCvt bindingPropAttrCvt = new PropertyAttrCvt(
       BindingProperty.class);
@@ -967,11 +968,21 @@
     registerAttributeConverter(Class.class, classAttrCvt);
   }
 
-  protected Schema __module_begin(NullableName renameTo) {
+  protected Schema __module_begin(NullableName renameTo, String deployTo)
+      throws UnableToCompleteException {
+
+    if (deployTo != null && deployTo.length() > 0) {
+      // Only absolute paths, although it is okay to have multiple slashes.
+      if (!deployTo.startsWith("/")) {
+        logger.log(TreeLogger.ERROR, "deploy-to '" + deployTo
+            + "' must begin with forward slash (e.g. '/foo')");
+        throw new UnableToCompleteException();
+      }
+    }
     return bodySchema;
   }
 
-  protected void __module_end(NullableName renameTo) {
+  protected void __module_end(NullableName renameTo, String deployTo) {
     // Maybe infer source and public.
     //
     if (!foundExplicitSourceOrSuperSource) {
@@ -986,6 +997,7 @@
 
     // We do this in __module_end so this value is never inherited
     moduleDef.setNameOverride(renameTo.token);
+    moduleDef.setDeployTo(deployTo);
   }
 
   /**
diff --git a/dev/core/src/com/google/gwt/dev/shell/GWTShellServlet.java b/dev/core/src/com/google/gwt/dev/shell/GWTShellServlet.java
index 670af09..8330b81 100644
--- a/dev/core/src/com/google/gwt/dev/shell/GWTShellServlet.java
+++ b/dev/core/src/com/google/gwt/dev/shell/GWTShellServlet.java
@@ -19,7 +19,6 @@
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.linker.impl.HostedModeLinker;
 import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
-import com.google.gwt.dev.GWTShell;
 import com.google.gwt.dev.cfg.ModuleDef;
 import com.google.gwt.dev.cfg.ModuleDefLoader;
 import com.google.gwt.dev.jjs.JJSOptionsImpl;
@@ -117,7 +116,7 @@
 
   private int nextRequestId;
 
-  private File outDir;
+  private WorkDirs workDirs;
 
   private final Object requestIdLock = new Object();
 
@@ -427,8 +426,7 @@
 
       if (foundResource == null) {
         // Look for generated files
-        File shellDir = new File(getOutputDir(), GWTShell.GWT_SHELL_PATH
-            + File.separator + moduleName);
+        File shellDir = getShellWorkDirs().getShellWorkDir(moduleDef);
         File requestedFile = new File(shellDir, partialPath);
         if (requestedFile.exists()) {
           try {
@@ -441,11 +439,10 @@
 
       /*
        * If the user is coming from compiled web-mode, check the linker output
-       * directory for the real bootstrap file. We'll default to using the
-       * output directory of the first linker defined in the <set-linker> tab.
+       * directory for the real bootstrap file.
        */
       if (foundResource == null) {
-        File moduleDir = new File(getOutputDir(), moduleName);
+        File moduleDir = getShellWorkDirs().getCompilerOutputDir(moduleDef);
         File requestedFile = new File(moduleDir, partialPath);
         if (requestedFile.exists()) {
           try {
@@ -617,21 +614,21 @@
     }
   }
 
-  private synchronized File getOutputDir() {
-    if (outDir == null) {
-      ServletContext servletContext = getServletContext();
-      final String attr = "com.google.gwt.dev.shell.outdir";
-      outDir = (File) servletContext.getAttribute(attr);
-      assert (outDir != null);
-    }
-    return outDir;
-  }
-
   @SuppressWarnings("unchecked")
   private Map<String, String[]> getParameterMap(HttpServletRequest request) {
     return request.getParameterMap();
   }
 
+  private synchronized WorkDirs getShellWorkDirs() {
+    if (workDirs == null) {
+      ServletContext servletContext = getServletContext();
+      final String attr = "com.google.gwt.dev.shell.workdirs";
+      workDirs = (WorkDirs) servletContext.getAttribute(attr);
+      assert (workDirs != null);
+    }
+    return workDirs;
+  }
+
   private String guessMimeType(String fullPath) {
     int dot = fullPath.lastIndexOf('.');
     if (dot != -1) {
@@ -951,7 +948,7 @@
         // ServeletContext.getResourceAsStream()
         //
         ServletContext context = new HostedModeServletContextProxy(
-            getServletContext(), moduleDef, getOutputDir());
+            getServletContext(), moduleDef, getShellWorkDirs());
         ServletConfig config = new HostedModeServletConfigProxy(
             getServletConfig(), context);
 
diff --git a/dev/core/src/com/google/gwt/dev/shell/GWTShellServletFilter.java b/dev/core/src/com/google/gwt/dev/shell/GWTShellServletFilter.java
new file mode 100644
index 0000000..e8c599e
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/GWTShellServletFilter.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.shell;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.ArtifactSet;
+import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
+import com.google.gwt.dev.ShellOptions;
+import com.google.gwt.dev.cfg.ModuleDef;
+
+import org.apache.commons.collections.map.AbstractReferenceMap;
+import org.apache.commons.collections.map.ReferenceIdentityMap;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Built-in servlet for convenient access to the public path of a specified
+ * module.
+ */
+public class GWTShellServletFilter implements Filter {
+
+  private final Map<String, ModuleDef> autogenScripts = new HashMap<String, ModuleDef>();
+  /**
+   * Maintains a persistent map of linker contexts for each module, for
+   * incremental re-link with new generated artifacts.
+   */
+  @SuppressWarnings("unchecked")
+  private final Map<ModuleDef, StandardLinkerContext> linkerContextsByModule = new ReferenceIdentityMap(
+      AbstractReferenceMap.WEAK, AbstractReferenceMap.HARD, true);
+
+  private TreeLogger logger;
+
+  private final ShellOptions options;
+
+  public GWTShellServletFilter(TreeLogger logger, ShellOptions options,
+      ModuleDef[] moduleDefs) {
+    this.logger = logger;
+    this.options = options;
+    for (ModuleDef moduleDef : moduleDefs) {
+      String scriptName = moduleDef.getDeployTo() + moduleDef.getName()
+          + ".nocache.js";
+      autogenScripts.put(scriptName, moduleDef);
+    }
+  }
+
+  public void destroy() {
+  }
+
+  public void doFilter(ServletRequest req, ServletResponse resp,
+      FilterChain chain) throws IOException, ServletException {
+
+    if (req instanceof HttpServletRequest) {
+      HttpServletRequest request = (HttpServletRequest) req;
+      String pathInfo = request.getRequestURI();
+      logger.log(TreeLogger.TRACE, "Request for: " + pathInfo);
+      ModuleDef moduleDef = autogenScripts.get(pathInfo);
+      if (moduleDef != null) {
+        /*
+         * If the '?compiled' request property is specified, don't
+         * auto-generate.
+         * 
+         * TODO(scottb): does this even do anything anymore?
+         * 
+         * TODO(scottb): how do we avoid clobbering a compiled selection script?
+         */
+        if (req.getParameter("compiled") == null) {
+          try {
+            // Run the linkers for hosted mode.
+            hostedModeLink(logger.branch(TreeLogger.TRACE, "Request for '"
+                + pathInfo + "' maps to script generator for module '"
+                + moduleDef.getName() + "'"), moduleDef);
+          } catch (UnableToCompleteException e) {
+            /*
+             * The error will have already been logged. Continue, since this
+             * could actually be a request for a static file that happens to
+             * have an unfortunately confusing name.
+             */
+          }
+        }
+      }
+    }
+
+    // Do normal handling, knowing that the linkers may have run earlier to
+    // produce files we are just about to serve.
+    chain.doFilter(req, resp);
+  }
+
+  public void init(FilterConfig filterConfig) throws ServletException {
+  }
+
+  /**
+   * Called when new generated artifacts are produced.
+   */
+  void relink(TreeLogger logger, ModuleDef moduleDef, ArtifactSet newArtifacts)
+      throws UnableToCompleteException {
+    StandardLinkerContext context = linkerContextsByModule.get(moduleDef);
+    assert context != null;
+
+    ArtifactSet artifacts = context.invokeRelink(logger, newArtifacts);
+    dumpArtifacts(logger, moduleDef, context, artifacts);
+  }
+
+  private void dumpArtifacts(TreeLogger logger, ModuleDef moduleDef,
+      StandardLinkerContext context, ArtifactSet artifacts)
+      throws UnableToCompleteException {
+    File outputPath = new File(options.getOutDir(), moduleDef.getDeployTo());
+    File extraPath = null;
+    if (options.getExtraDir() != null) {
+      extraPath = new File(options.getExtraDir(), moduleDef.getDeployTo());
+    }
+    context.produceOutputDirectory(logger, artifacts, outputPath, extraPath);
+  }
+
+  private void hostedModeLink(TreeLogger logger, ModuleDef moduleDef)
+      throws UnableToCompleteException {
+    String moduleName = moduleDef.getName();
+    logger.log(TreeLogger.TRACE, "Running linkers for module " + moduleName);
+
+    // TODO: blow away artifacts from a previous link.
+
+    // Perform the initial link.
+    StandardLinkerContext context = new StandardLinkerContext(logger,
+        moduleDef, options);
+    ArtifactSet artifacts = context.invokeLink(logger);
+    dumpArtifacts(logger, moduleDef, context, artifacts);
+
+    // Save off a new active link state (which may overwrite an old one).
+    linkerContextsByModule.put(moduleDef, context);
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/shell/HostedModeServletContextProxy.java b/dev/core/src/com/google/gwt/dev/shell/HostedModeServletContextProxy.java
index 5270bf1..852bc65 100644
--- a/dev/core/src/com/google/gwt/dev/shell/HostedModeServletContextProxy.java
+++ b/dev/core/src/com/google/gwt/dev/shell/HostedModeServletContextProxy.java
@@ -15,7 +15,6 @@
  */
 package com.google.gwt.dev.shell;
 
-import com.google.gwt.dev.GWTShell;
 import com.google.gwt.dev.cfg.ModuleDef;
 import com.google.gwt.dev.resource.Resource;
 
@@ -43,13 +42,13 @@
    * Avoid pinning my moduleDef.
    */
   private final WeakReference<ModuleDef> moduleDefRef;
-  private final File outDir;
+  private final WorkDirs workDirs;
 
   HostedModeServletContextProxy(ServletContext context, ModuleDef moduleDef,
-      File outDir) {
+      WorkDirs workDirs) {
     this.context = context;
     this.moduleDefRef = new WeakReference<ModuleDef>(moduleDef);
-    this.outDir = outDir;
+    this.workDirs = workDirs;
   }
 
   /**
@@ -78,6 +77,10 @@
     return context.getContext(arg0);
   }
 
+  public String getContextPath() {
+    return context.getContextPath();
+  }
+
   /**
    * @param arg0
    * @return
@@ -179,8 +182,7 @@
     }
 
     // Otherwise try the path but rooted in the shell's output directory
-    File shellDir = new File(outDir, GWTShell.GWT_SHELL_PATH + File.separator
-        + moduleDef.getName());
+    File shellDir = workDirs.getShellWorkDir(moduleDef);
     File requestedFile = new File(shellDir, partialPath);
     if (requestedFile.exists()) {
       return requestedFile.toURI().toURL();
@@ -191,7 +193,8 @@
      * directory for the file. We'll default to using the output directory of
      * the first linker defined in the <set-linker> tab.
      */
-    requestedFile = new File(new File(outDir, moduleDef.getName()), partialPath);
+    File linkDir = workDirs.getCompilerOutputDir(moduleDef);
+    requestedFile = new File(linkDir, partialPath);
     if (requestedFile.exists()) {
       try {
         return requestedFile.toURI().toURL();
diff --git a/dev/core/src/com/google/gwt/dev/shell/ServletContainer.java b/dev/core/src/com/google/gwt/dev/shell/ServletContainer.java
new file mode 100644
index 0000000..732998a
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/ServletContainer.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.shell;
+
+import com.google.gwt.core.ext.UnableToCompleteException;
+
+/**
+ * An instance of a servlet container that can be used by the shell. It is
+ * assumed that this servlet container serves a web app from the root directory
+ * specified by a call to
+ * {@link ServletContainerLauncher#setAppRootDir(java.io.File)}.
+ */
+public interface ServletContainer {
+
+  /**
+   * Provides the port on which the server is actually running, which can be
+   * useful when automatic port selection was requested.
+   */
+  int getPort();
+
+  /**
+   * Causes the web app to pick up changes made within the app root dir while
+   * running. This method cannot be called after {@link #stop()} has been
+   * called.
+   * 
+   * TODO(bruce): need to determine whether all the important servlet containers
+   * will let us do this (e.g. ensure they don't lock files we would need to
+   * update)
+   * 
+   * @throws UnableToCompleteException
+   */
+  void refresh() throws UnableToCompleteException;
+
+  /**
+   * Stops the running servlet container. It cannot be restarted after this.
+   * 
+   * @throws UnableToCompleteException
+   */
+  void stop() throws UnableToCompleteException;
+}
diff --git a/dev/core/src/com/google/gwt/dev/shell/ServletContainerLauncher.java b/dev/core/src/com/google/gwt/dev/shell/ServletContainerLauncher.java
new file mode 100644
index 0000000..e3896e2
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/ServletContainerLauncher.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.shell;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+
+import java.io.File;
+
+import javax.servlet.Filter;
+
+/**
+ * Defines the service provider interface for launching servlet containers that
+ * can be used by the shell.
+ */
+public interface ServletContainerLauncher {
+
+  ServletContainer start(TreeLogger topLogger, int port, File appRootDir,
+      Filter shellServletFilter) throws UnableToCompleteException;
+}
diff --git a/dev/core/src/com/google/gwt/dev/shell/ShellModuleSpaceHost.java b/dev/core/src/com/google/gwt/dev/shell/ShellModuleSpaceHost.java
index 39cb165..311c564 100644
--- a/dev/core/src/com/google/gwt/dev/shell/ShellModuleSpaceHost.java
+++ b/dev/core/src/com/google/gwt/dev/shell/ShellModuleSpaceHost.java
@@ -21,7 +21,7 @@
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.dev.cfg.ModuleDef;
 import com.google.gwt.dev.cfg.Rules;
-import com.google.gwt.dev.jdt.RebindOracle;
+import com.google.gwt.dev.shell.StandardRebindOracle.ArtifactAcceptor;
 
 import java.io.File;
 
@@ -41,7 +41,9 @@
 
   private final ModuleDef module;
 
-  private RebindOracle rebindOracle;
+  private StandardRebindOracle rebindOracle;
+
+  private final GWTShellServletFilter servletFilter;
 
   private final File shellDir;
 
@@ -52,15 +54,14 @@
    * @param saveJsni
    */
   public ShellModuleSpaceHost(TreeLogger logger, TypeOracle typeOracle,
-      ModuleDef module, File genDir, File shellDir) {
+      ModuleDef module, File genDir, File shellDir,
+      GWTShellServletFilter servletFilter) {
     this.logger = logger;
     this.typeOracle = typeOracle;
     this.module = module;
     this.genDir = genDir;
-
-    // Combine the user's output dir with the module name to get the
-    // module-specific output dir.
     this.shellDir = shellDir;
+    this.servletFilter = servletFilter;
   }
 
   public CompilingClassLoader getClassLoader() {
@@ -108,10 +109,19 @@
         module.getCompilationState(), readySpace);
   }
 
-  public String rebind(TreeLogger rebindLogger, String sourceTypeName)
+  public String rebind(final TreeLogger rebindLogger, String sourceTypeName)
       throws UnableToCompleteException {
     checkForModuleSpace();
-    return rebindOracle.rebind(rebindLogger, sourceTypeName);
+
+    ArtifactAcceptor artifactAcceptor = (servletFilter == null) ? null
+        : new ArtifactAcceptor() {
+          public void accept(ArtifactSet newlyGeneratedArtifacts)
+              throws UnableToCompleteException {
+            servletFilter.relink(rebindLogger, module, newlyGeneratedArtifacts);
+          }
+        };
+
+    return rebindOracle.rebind(rebindLogger, sourceTypeName, artifactAcceptor);
   }
 
   private void checkForModuleSpace() {
diff --git a/dev/core/src/com/google/gwt/dev/shell/StandardGeneratorContext.java b/dev/core/src/com/google/gwt/dev/shell/StandardGeneratorContext.java
index 332f7de..64687f5 100644
--- a/dev/core/src/com/google/gwt/dev/shell/StandardGeneratorContext.java
+++ b/dev/core/src/com/google/gwt/dev/shell/StandardGeneratorContext.java
@@ -25,7 +25,6 @@
 import com.google.gwt.core.ext.linker.GeneratedResource;
 import com.google.gwt.core.ext.linker.impl.StandardGeneratedResource;
 import com.google.gwt.core.ext.typeinfo.JClassType;
-import com.google.gwt.core.ext.typeinfo.NotFoundException;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.dev.cfg.PublicOracle;
 import com.google.gwt.dev.javac.CompilationState;
@@ -175,7 +174,7 @@
     }
   }
 
-  private final ArtifactSet artifactSet;
+  private final ArtifactSet allGeneratedArtifacts;
 
   private final Set<GeneratedUnitWithFile> committedGeneratedCups = new HashSet<GeneratedUnitWithFile>();
 
@@ -185,9 +184,11 @@
 
   private final File genDir;
 
-  private final Set<String> generatedTypeNames = new HashSet<String>();
+  private final File generatorResourcesDir;
 
-  private final File outDir;
+  private ArtifactSet newlyGeneratedArtifacts = new ArtifactSet();
+
+  private final Set<String> newlyGeneratedTypeNames = new HashSet<String>();
 
   private final Map<OutputStream, PendingResource> pendingResourcesByOutputStream = new IdentityHashMap<OutputStream, PendingResource>();
 
@@ -203,13 +204,13 @@
    */
   public StandardGeneratorContext(CompilationState compilationState,
       PropertyOracle propOracle, PublicOracle publicOracle, File genDir,
-      File outDir, ArtifactSet artifactSet) {
+      File generatorResourcesDir, ArtifactSet allGeneratedArtifacts) {
     this.compilationState = compilationState;
     this.propOracle = propOracle;
     this.publicOracle = publicOracle;
     this.genDir = genDir;
-    this.outDir = outDir;
-    this.artifactSet = artifactSet;
+    this.generatorResourcesDir = generatorResourcesDir;
+    this.allGeneratedArtifacts = allGeneratedArtifacts;
   }
 
   /**
@@ -233,8 +234,8 @@
    */
   public void commitArtifact(TreeLogger logger, Artifact<?> artifact)
       throws UnableToCompleteException {
-    // The artifactSet will be null in hosted mode, since we never run Linkers
-    artifactSet.replace(artifact);
+    allGeneratedArtifacts.replace(artifact);
+    newlyGeneratedArtifacts.add(artifact);
   }
 
   public GeneratedResource commitResource(TreeLogger logger, OutputStream os)
@@ -279,9 +280,9 @@
    * uncommitted compilation units and to force committed compilation units to
    * be parsed and added to the type oracle.
    * 
-   * @return types generated during this object's lifetime
+   * @return any newly generated artifacts since the last call
    */
-  public final JClassType[] finish(TreeLogger logger)
+  public final ArtifactSet finish(TreeLogger logger)
       throws UnableToCompleteException {
 
     abortUncommittedResources(logger);
@@ -317,21 +318,16 @@
         compilationState.compile(logger);
       }
 
-      // Return the generated types.
+      // Make sure all generated types can be found in TypeOracle.
       TypeOracle typeOracle = getTypeOracle();
-      JClassType[] genTypes = new JClassType[genTypeNames.size()];
-      int next = 0;
-      for (Iterator<String> iter = genTypeNames.iterator(); iter.hasNext();) {
-        String genTypeName = iter.next();
-        try {
-          genTypes[next++] = typeOracle.getType(genTypeName);
-        } catch (NotFoundException e) {
+      for (String genTypeName : genTypeNames) {
+        if (typeOracle.findType(genTypeName) == null) {
           String msg = "Unable to find recently-generated type '" + genTypeName;
           logger.log(TreeLogger.ERROR, msg, null);
           throw new UnableToCompleteException();
         }
       }
-      return genTypes;
+      return newlyGeneratedArtifacts;
     } finally {
 
       // Remind the user if there uncommitted cups.
@@ -346,14 +342,11 @@
 
       uncommittedGeneratedCupsByPrintWriter.clear();
       committedGeneratedCups.clear();
-      generatedTypeNames.clear();
+      newlyGeneratedTypeNames.clear();
+      newlyGeneratedArtifacts = new ArtifactSet();
     }
   }
 
-  public File getOutputDir() {
-    return outDir;
-  }
-
   public final PropertyOracle getPropertyOracle() {
     return propOracle;
   }
@@ -380,7 +373,7 @@
     }
 
     // Has anybody tried to create this type during this iteration?
-    if (generatedTypeNames.contains(typeName)) {
+    if (newlyGeneratedTypeNames.contains(typeName)) {
       final String msg = "A request to create type '"
           + typeName
           + "' was received while the type itself was being created; this might be a generator or configuration bug";
@@ -398,7 +391,7 @@
     }
     GeneratedUnitWithFile gcup = new GeneratedUnitWithFile(qualifiedSourceName);
     uncommittedGeneratedCupsByPrintWriter.put(gcup.pw, gcup);
-    generatedTypeNames.add(typeName);
+    newlyGeneratedTypeNames.add(typeName);
 
     return gcup.pw;
   }
@@ -443,7 +436,7 @@
     }
 
     // See if the file is already committed.
-    SortedSet<GeneratedResource> resources = artifactSet.find(GeneratedResource.class);
+    SortedSet<GeneratedResource> resources = allGeneratedArtifacts.find(GeneratedResource.class);
     for (GeneratedResource resource : resources) {
       if (partialPath.equals(resource.getPartialPath())) {
         return null;
@@ -462,7 +455,8 @@
     }
 
     // Record that this file is pending.
-    PendingResource pendingResource = new PendingResource(outDir, partialPath);
+    PendingResource pendingResource = new PendingResource(
+        generatorResourcesDir, partialPath);
     OutputStream os = pendingResource.getOutputStream();
     pendingResourcesByOutputStream.put(os, pendingResource);
 
diff --git a/dev/core/src/com/google/gwt/dev/shell/StandardRebindOracle.java b/dev/core/src/com/google/gwt/dev/shell/StandardRebindOracle.java
index 3cf0c7f..94e7a88 100644
--- a/dev/core/src/com/google/gwt/dev/shell/StandardRebindOracle.java
+++ b/dev/core/src/com/google/gwt/dev/shell/StandardRebindOracle.java
@@ -41,6 +41,18 @@
 public class StandardRebindOracle implements RebindOracle {
 
   /**
+   * A call-back interface to be notified when new types are generated.
+   * 
+   */
+  public interface ArtifactAcceptor {
+    /**
+     * Called if new artifacts are generated.
+     */
+    void accept(ArtifactSet newlyGeneratedArtifacts)
+        throws UnableToCompleteException;
+  }
+
+  /**
    * Makes the actual deferred binding decision by examining rules.
    */
   private final class Rebinder {
@@ -53,14 +65,17 @@
 
     public Rebinder() {
       genCtx = new StandardGeneratorContext(compilationState, propOracle,
-          publicOracle, genDir, outDir, artifactSet);
+          publicOracle, genDir, generatorResourcesDir, allGeneratedArtifacts);
     }
 
-    public String rebind(TreeLogger logger, String typeName)
-        throws UnableToCompleteException {
+    public String rebind(TreeLogger logger, String typeName,
+        ArtifactAcceptor artifactAcceptor) throws UnableToCompleteException {
 
       String result = tryRebind(logger, typeName);
-      genCtx.finish(logger);
+      ArtifactSet newlyGeneratedArtifacts = genCtx.finish(logger);
+      if (!newlyGeneratedArtifacts.isEmpty() && artifactAcceptor != null) {
+        artifactAcceptor.accept(newlyGeneratedArtifacts);
+      }
       if (result == null) {
         result = typeName;
       }
@@ -125,7 +140,7 @@
     }
   }
 
-  private final ArtifactSet artifactSet;
+  private final ArtifactSet allGeneratedArtifacts;
 
   private final Map<String, String> cache = new HashMap<String, String>();
 
@@ -133,7 +148,7 @@
 
   private final File genDir;
 
-  private final File outDir;
+  private final File generatorResourcesDir;
 
   private final PropertyOracle propOracle;
 
@@ -143,25 +158,30 @@
 
   public StandardRebindOracle(CompilationState compilationState,
       PropertyOracle propOracle, PublicOracle publicOracle, Rules rules,
-      File genDir, File moduleOutDir, ArtifactSet artifactSet) {
+      File genDir, File generatorResourcesDir, ArtifactSet allGeneratedArtifacts) {
     this.compilationState = compilationState;
     this.propOracle = propOracle;
     this.publicOracle = publicOracle;
     this.rules = rules;
     this.genDir = genDir;
-    this.outDir = moduleOutDir;
-    this.artifactSet = artifactSet;
+    this.generatorResourcesDir = generatorResourcesDir;
+    this.allGeneratedArtifacts = allGeneratedArtifacts;
   }
 
   public String rebind(TreeLogger logger, String typeName)
       throws UnableToCompleteException {
+    return rebind(logger, typeName, null);
+  }
+
+  public String rebind(TreeLogger logger, String typeName,
+      ArtifactAcceptor artifactAcceptor) throws UnableToCompleteException {
 
     String result = cache.get(typeName);
     if (result == null) {
       logger = Messages.TRACE_TOPLEVEL_REBIND.branch(logger, typeName, null);
 
       Rebinder rebinder = new Rebinder();
-      result = rebinder.rebind(logger, typeName);
+      result = rebinder.rebind(logger, typeName, artifactAcceptor);
       cache.put(typeName, result);
 
       Messages.TRACE_TOPLEVEL_REBIND_RESULT.log(logger, result, null);
diff --git a/dev/core/src/com/google/gwt/dev/shell/WorkDirs.java b/dev/core/src/com/google/gwt/dev/shell/WorkDirs.java
new file mode 100644
index 0000000..fd62396
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/WorkDirs.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.shell;
+
+import com.google.gwt.dev.cfg.ModuleDef;
+
+import java.io.File;
+
+/**
+ * Provides information about work directories.
+ */
+public interface WorkDirs {
+  /**
+   * Gets the compiler output directory for a particular module.
+   */
+  File getCompilerOutputDir(ModuleDef moduleDef);
+
+  /**
+   * Gets the shell work directory for a particular module.
+   */
+  File getShellWorkDir(ModuleDef moduleDef);
+}
diff --git a/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java b/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
new file mode 100644
index 0000000..9a6b12b
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.shell.jetty;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.TreeLogger.Type;
+import com.google.gwt.dev.shell.ServletContainer;
+import com.google.gwt.dev.shell.ServletContainerLauncher;
+
+import org.mortbay.jetty.Handler;
+import org.mortbay.jetty.Server;
+import org.mortbay.jetty.nio.SelectChannelConnector;
+import org.mortbay.jetty.servlet.FilterHolder;
+import org.mortbay.jetty.webapp.WebAppContext;
+import org.mortbay.log.Log;
+import org.mortbay.log.Logger;
+
+import java.io.File;
+
+import javax.servlet.Filter;
+
+/**
+ * A launcher for an embedded Jetty server.
+ */
+public class JettyLauncher implements ServletContainerLauncher {
+
+  /**
+   * An adapter for the Jetty logging system to GWT's TreeLogger. This
+   * implementation class is only public to allow {@link Log} to instantiate it.
+   * 
+   * The weird static data / default construction setup is a game we play with
+   * {@link Log}'s static initializer to prevent the initial log message from
+   * going to stderr.
+   */
+  public static final class JettyTreeLogger implements Logger {
+    private static Type nextBranchLevel;
+    private static TreeLogger nextLogger;
+
+    /**
+     * Returns true if the default constructor can be called.
+     */
+    public static boolean isDefaultConstructionReady() {
+      return nextLogger != null;
+    }
+
+    /**
+     * Call to set initial state for default construction; must be called again
+     * each time before a default instantiation occurs.
+     */
+    public static void setDefaultConstruction(TreeLogger logger,
+        Type branchLevel) {
+      if (logger == null || branchLevel == null) {
+        throw new NullPointerException();
+      }
+      nextLogger = logger;
+      nextBranchLevel = branchLevel;
+    }
+
+    private final Type branchLevel;
+    private final TreeLogger logger;
+
+    public JettyTreeLogger() {
+      this(nextLogger, nextBranchLevel);
+      nextLogger = null;
+      nextBranchLevel = null;
+    }
+
+    public JettyTreeLogger(TreeLogger logger, Type branchLevel) {
+      if (logger == null || branchLevel == null) {
+        throw new NullPointerException();
+      }
+      this.branchLevel = branchLevel;
+      this.logger = logger;
+    }
+
+    public void debug(String msg, Object arg0, Object arg1) {
+      logger.log(TreeLogger.DEBUG, format(msg, arg0, arg1));
+    }
+
+    public void debug(String msg, Throwable th) {
+      logger.log(TreeLogger.DEBUG, msg, th);
+    }
+
+    public Logger getLogger(String name) {
+      return new JettyTreeLogger(logger.branch(branchLevel, name), branchLevel);
+    }
+
+    public void info(String msg, Object arg0, Object arg1) {
+      logger.log(TreeLogger.INFO, format(msg, arg0, arg1));
+    }
+
+    public boolean isDebugEnabled() {
+      return logger.isLoggable(TreeLogger.DEBUG);
+    }
+
+    public void setDebugEnabled(boolean enabled) {
+      // ignored
+    }
+
+    public void warn(String msg, Object arg0, Object arg1) {
+      logger.log(TreeLogger.WARN, format(msg, arg0, arg1));
+    }
+
+    public void warn(String msg, Throwable th) {
+      logger.log(TreeLogger.WARN, msg, th);
+    }
+
+    /**
+     * Copied from org.mortbay.log.StdErrLog.
+     */
+    private String format(String msg, Object arg0, Object arg1) {
+      int i0 = msg.indexOf("{}");
+      int i1 = i0 < 0 ? -1 : msg.indexOf("{}", i0 + 2);
+
+      if (arg1 != null && i1 >= 0) {
+        msg = msg.substring(0, i1) + arg1 + msg.substring(i1 + 2);
+      }
+      if (arg0 != null && i0 >= 0) {
+        msg = msg.substring(0, i0) + arg0 + msg.substring(i0 + 2);
+      }
+      return msg;
+    }
+  }
+
+  private static class JettyServletContainer implements ServletContainer {
+
+    private final int actualPort;
+    private final File appRootDir;
+    private final TreeLogger logger;
+    private final WebAppContext wac;
+
+    public JettyServletContainer(TreeLogger logger, WebAppContext wac,
+        int actualPort, File appRootDir) {
+      this.logger = logger;
+      this.wac = wac;
+      this.actualPort = actualPort;
+      this.appRootDir = appRootDir;
+    }
+
+    public int getPort() {
+      return actualPort;
+    }
+
+    public void refresh() throws UnableToCompleteException {
+      String msg = "Reloading web app to reflect changes in "
+          + appRootDir.getAbsolutePath();
+      TreeLogger branch = logger.branch(TreeLogger.INFO, msg);
+      try {
+        wac.stop();
+      } catch (Exception e) {
+        branch.log(TreeLogger.ERROR, "Unable to stop embedded Jetty server", e);
+        throw new UnableToCompleteException();
+      }
+
+      try {
+        wac.start();
+      } catch (Exception e) {
+        branch.log(TreeLogger.ERROR, "Unable to stop embedded Jetty server", e);
+        throw new UnableToCompleteException();
+      }
+
+      branch.log(TreeLogger.INFO, "Reload completed successfully");
+    }
+
+    public void stop() throws UnableToCompleteException {
+      TreeLogger branch = logger.branch(TreeLogger.INFO,
+          "Stopping Jetty server");
+      try {
+        wac.stop();
+      } catch (Exception e) {
+        branch.log(TreeLogger.ERROR, "Unable to stop embedded Jetty server", e);
+        throw new UnableToCompleteException();
+      }
+      branch.log(TreeLogger.INFO, "Stopped successfully");
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  public ServletContainer start(TreeLogger logger, int port, File appRootDir,
+      Filter shellServletFilter) throws UnableToCompleteException {
+    checkStartParams(logger, port, appRootDir);
+
+    // The dance we do with Jetty's logging system.
+    System.setProperty("VERBOSE", "true");
+    JettyTreeLogger.setDefaultConstruction(logger, TreeLogger.INFO);
+    System.setProperty("org.mortbay.log.class", JettyTreeLogger.class.getName());
+    // Force initialization.
+    Log.isDebugEnabled();
+    if (JettyTreeLogger.isDefaultConstructionReady()) {
+      // The log system was already initialized and did not use our
+      // newly-constructed logger, set it explicitly now.
+      Log.setLog(new JettyTreeLogger());
+    }
+
+    Server server = new Server();
+    SelectChannelConnector connector = new SelectChannelConnector();
+    connector.setPort(port);
+    connector.setHost("127.0.0.1");
+    server.addConnector(connector);
+
+    // Create a new web app in the war directory.
+    WebAppContext wac = new WebAppContext(appRootDir.getAbsolutePath(), "/");
+
+    // Prevent file locking on windows; pick up file changes.
+    wac.getInitParams().put(
+        "org.mortbay.jetty.servlet.Default.useFileMappedBuffer", "false");
+
+    // Setup the shell servlet filter to generate nocache.js files (and run
+    // the hosted mode linker stack.
+    FilterHolder filterHolder = new FilterHolder();
+    filterHolder.setFilter(shellServletFilter);
+    wac.addFilter(filterHolder, "/*", Handler.ALL);
+
+    server.setHandler(wac);
+    server.setStopAtShutdown(true);
+
+    try {
+      server.start();
+      int actualPort = connector.getPort();
+      return new JettyServletContainer(logger, wac, actualPort, appRootDir);
+    } catch (Exception e) {
+      logger.log(TreeLogger.ERROR, "Unable to start embedded Jetty server", e);
+      throw new UnableToCompleteException();
+    }
+  }
+
+  private void checkStartParams(TreeLogger logger, int port, File appRootDir) {
+    if (logger == null) {
+      throw new NullPointerException("logger cannot be null");
+    }
+
+    if (port < 0 || port > 65535) {
+      throw new IllegalArgumentException(
+          "port must be either 0 (for auto) or less than 65536");
+    }
+
+    if (appRootDir == null) {
+      throw new NullPointerException("app root direcotry cannot be null");
+    }
+  }
+
+}
diff --git a/dev/core/src/com/google/gwt/dev/shell/tomcat/EmbeddedTomcatServer.java b/dev/core/src/com/google/gwt/dev/shell/tomcat/EmbeddedTomcatServer.java
index 0ebbe02..1fc2d9b 100644
--- a/dev/core/src/com/google/gwt/dev/shell/tomcat/EmbeddedTomcatServer.java
+++ b/dev/core/src/com/google/gwt/dev/shell/tomcat/EmbeddedTomcatServer.java
@@ -21,6 +21,7 @@
 import com.google.gwt.dev.resource.impl.PathPrefix;
 import com.google.gwt.dev.resource.impl.PathPrefixSet;
 import com.google.gwt.dev.resource.impl.ResourceOracleImpl;
+import com.google.gwt.dev.shell.WorkDirs;
 import com.google.gwt.util.tools.Utility;
 
 import org.apache.catalina.Connector;
@@ -60,13 +61,13 @@
   }
 
   public static synchronized String start(TreeLogger topLogger, int port,
-      File outDir) {
+      WorkDirs workDirs) {
     if (sTomcat != null) {
       throw new IllegalStateException("Embedded Tomcat is already running");
     }
 
     try {
-      new EmbeddedTomcatServer(topLogger, port, outDir);
+      new EmbeddedTomcatServer(topLogger, port, workDirs);
       return null;
     } catch (LifecycleException e) {
       String msg = e.getMessage();
@@ -145,7 +146,7 @@
   private final TreeLogger startupBranchLogger;
 
   private EmbeddedTomcatServer(final TreeLogger topLogger, int listeningPort,
-      final File outDir) throws LifecycleException {
+      final WorkDirs workDirs) throws LifecycleException {
     if (topLogger == null) {
       throw new NullPointerException("No logger specified");
     }
@@ -222,7 +223,7 @@
         if (StandardHost.PRE_INSTALL_EVENT.equals(event.getType())) {
           StandardContext webapp = (StandardContext) event.getData();
           publishShellLoggerAttribute(logger, topLogger, webapp);
-          publishShellOutDirAttribute(logger, outDir, webapp);
+          publishShellWorkDirsAttribute(logger, workDirs, webapp);
         }
       }
     });
@@ -411,12 +412,12 @@
   }
 
   /**
-   * Publish the shell's output dir as an attribute. This attribute is used to
+   * Publish the shell's work dir as an attribute. This attribute is used to
    * find it out of the thin air within the shell servlet.
    */
-  private void publishShellOutDirAttribute(TreeLogger logger,
-      File outDirToPublish, StandardContext webapp) {
-    final String attr = "com.google.gwt.dev.shell.outdir";
-    publishAttributeToWebApp(logger, webapp, attr, outDirToPublish);
+  private void publishShellWorkDirsAttribute(TreeLogger logger,
+      WorkDirs workDirs, StandardContext webapp) {
+    final String attr = "com.google.gwt.dev.shell.workdirs";
+    publishAttributeToWebApp(logger, webapp, attr, workDirs);
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerExtraDir.java b/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerExtraDir.java
new file mode 100644
index 0000000..42626cf
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerExtraDir.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.util.arg;
+
+import com.google.gwt.util.tools.ArgHandlerDir;
+
+import java.io.File;
+
+/**
+ * Argument handler for processing the extra directory option.
+ */
+public final class ArgHandlerExtraDir extends ArgHandlerDir {
+
+  private final OptionExtraDir option;
+
+  public ArgHandlerExtraDir(OptionExtraDir option) {
+    this.option = option;
+  }
+
+  public String getPurpose() {
+    return "The directory into which extra, non-deployed files will be written";
+  }
+
+  public String getTag() {
+    return "-extra";
+  }
+
+  @Override
+  public void setDir(File dir) {
+    option.setExtraDir(dir);
+  }
+}
\ No newline at end of file
diff --git a/dev/core/src/com/google/gwt/dev/util/arg/OptionExtraDir.java b/dev/core/src/com/google/gwt/dev/util/arg/OptionExtraDir.java
new file mode 100644
index 0000000..586f41f
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/arg/OptionExtraDir.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.util.arg;
+
+import java.io.File;
+
+/**
+ * Option to set the output directory for extra artifacts.
+ */
+public interface OptionExtraDir {
+
+  /**
+   * Returns the extra resource directory.
+   */
+  File getExtraDir();
+
+  /**
+   * Sets the extra resource directory.
+   */
+  void setExtraDir(File dir);
+}
diff --git a/eclipse/dev/linux/.classpath b/eclipse/dev/linux/.classpath
index 3cc771f..4b09998 100644
--- a/eclipse/dev/linux/.classpath
+++ b/eclipse/dev/linux/.classpath
@@ -9,6 +9,7 @@
 	<classpathentry kind="var" path="GWT_TOOLS/lib/apache/ant-1.6.5.jar" sourcepath="/GWT_TOOLS/lib/apache/ant-1.6.5-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/apache/tapestry-util-text-4.0.2.jar" sourcepath="/GWT_TOOLS/lib/apache/tapestry-util-text-4.0.2-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/eclipse/jdt-3.3.1.jar" sourcepath="/GWT_TOOLS/lib/eclipse/jdt-3.3.1-src.zip"/>
+	<classpathentry kind="var" path="GWT_TOOLS/lib/jetty/jetty-6.1.11.jar" sourcepath="/GWT_TOOLS/lib/jetty/jetty-6.1.11-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/junit/junit-3.8.1.jar" sourcepath="/GWT_TOOLS/lib/junit/junit-3.8.1-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/tomcat/ant-launcher-1.6.5.jar"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/tomcat/catalina-1.0.jar"/>
diff --git a/eclipse/dev/mac/.classpath b/eclipse/dev/mac/.classpath
index f9ad860..3832d9a 100644
--- a/eclipse/dev/mac/.classpath
+++ b/eclipse/dev/mac/.classpath
@@ -9,6 +9,7 @@
 	<classpathentry kind="var" path="GWT_TOOLS/lib/apache/ant-1.6.5.jar" sourcepath="/GWT_TOOLS/lib/apache/ant-1.6.5-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/apache/tapestry-util-text-4.0.2.jar" sourcepath="/GWT_TOOLS/lib/apache/tapestry-util-text-4.0.2-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/eclipse/jdt-3.3.1.jar" sourcepath="/GWT_TOOLS/lib/eclipse/jdt-3.3.1-src.zip"/>
+	<classpathentry kind="var" path="GWT_TOOLS/lib/jetty/jetty-6.1.11.jar" sourcepath="/GWT_TOOLS/lib/jetty/jetty-6.1.11-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/junit/junit-3.8.1.jar" sourcepath="/GWT_TOOLS/lib/junit/junit-3.8.1-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/tomcat/ant-launcher-1.6.5.jar"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/tomcat/catalina-1.0.jar"/>
diff --git a/eclipse/dev/windows/.classpath b/eclipse/dev/windows/.classpath
index ff4799f..70fc175 100644
--- a/eclipse/dev/windows/.classpath
+++ b/eclipse/dev/windows/.classpath
@@ -9,6 +9,7 @@
 	<classpathentry kind="var" path="GWT_TOOLS/lib/apache/ant-1.6.5.jar" sourcepath="/GWT_TOOLS/lib/apache/ant-1.6.5-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/apache/tapestry-util-text-4.0.2.jar" sourcepath="/GWT_TOOLS/lib/apache/tapestry-util-text-4.0.2-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/eclipse/jdt-3.3.1.jar" sourcepath="/GWT_TOOLS/lib/eclipse/jdt-3.3.1-src.zip"/>
+	<classpathentry kind="var" path="GWT_TOOLS/lib/jetty/jetty-6.1.11.jar" sourcepath="/GWT_TOOLS/lib/jetty/jetty-6.1.11-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/junit/junit-3.8.1.jar" sourcepath="/GWT_TOOLS/lib/junit/junit-3.8.1-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/tomcat/ant-launcher-1.6.5.jar"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/tomcat/catalina-1.0.jar"/>
diff --git a/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java b/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java
index e5e9747..7e11b43 100644
--- a/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java
+++ b/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java
@@ -119,12 +119,13 @@
 
     if (serializationPolicy == null) {
       // Failed to get the requested serialization policy; use the default
-      getServletContext().log(
+      log(
           "WARNING: Failed to get the SerializationPolicy '"
               + strongName
               + "' for module '"
               + moduleBaseURL
-              + "'; a legacy, 1.3.3 compatible, serialization policy will be used.  You may experience SerializationExceptions as a result.");
+              + "'; a legacy, 1.3.3 compatible, serialization policy will be used.  You may experience SerializationExceptions as a result.",
+          null);
       serializationPolicy = RPC.getDefaultSerializationPolicy();
     }
 
@@ -164,7 +165,7 @@
       return RPC.invokeAndEncodeResponse(this, rpcRequest.getMethod(),
           rpcRequest.getParameters(), rpcRequest.getSerializationPolicy());
     } catch (IncompatibleRemoteServiceException ex) {
-      getServletContext().log(
+      log(
           "An IncompatibleRemoteServiceException was thrown while processing this call.",
           ex);
       return RPC.encodeResponseForFailure(null, ex);
@@ -197,7 +198,7 @@
         modulePath = new URL(moduleBaseURL).getPath();
       } catch (MalformedURLException ex) {
         // log the information, we will default
-        getServletContext().log("Malformed moduleBaseURL: " + moduleBaseURL, ex);
+        log("Malformed moduleBaseURL: " + moduleBaseURL, ex);
       }
     }
 
@@ -214,7 +215,7 @@
           + ", is not in the same web application as this servlet, "
           + contextPath
           + ".  Your module may not be properly configured or your client and server code maybe out of date.";
-      getServletContext().log(message);
+      log(message, null);
     } else {
       // Strip off the context path from the module base URL. It should be a
       // strict prefix.
@@ -232,19 +233,17 @@
             serializationPolicy = SerializationPolicyLoader.loadFromStream(is,
                 null);
           } catch (ParseException e) {
-            getServletContext().log(
-                "ERROR: Failed to parse the policy file '"
-                    + serializationPolicyFilePath + "'", e);
+            log("ERROR: Failed to parse the policy file '"
+                + serializationPolicyFilePath + "'", e);
           } catch (IOException e) {
-            getServletContext().log(
-                "ERROR: Could not read the policy file '"
-                    + serializationPolicyFilePath + "'", e);
+            log("ERROR: Could not read the policy file '"
+                + serializationPolicyFilePath + "'", e);
           }
         } else {
           String message = "ERROR: The serialization policy file '"
               + serializationPolicyFilePath
               + "' was not found; did you forget to include it in this deployment?";
-          getServletContext().log(message);
+          log(message, null);
         }
       } finally {
         if (is != null) {
@@ -324,7 +323,7 @@
    * Override this method in order to control the parsing of the incoming
    * request. For example, you may want to bypass the check of the Content-Type
    * and character encoding headers in the request, as some proxies re-write the
-   * request headers.  Note that bypassing these checks may expose the servlet to
+   * request headers. Note that bypassing these checks may expose the servlet to
    * some cross-site vulnerabilities.
    * 
    * @param request the incoming request