/** * Copyright 2012 Tobias Gierke <tobias.gierke@code-sourcery.de> * * 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 de.codesourcery.jasm16.ide; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Pattern; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import de.codesourcery.jasm16.Address; import de.codesourcery.jasm16.WordAddress; import de.codesourcery.jasm16.compiler.CompilationUnit; import de.codesourcery.jasm16.compiler.CompiledCode; import de.codesourcery.jasm16.compiler.Compiler; import de.codesourcery.jasm16.compiler.DebugInfo; import de.codesourcery.jasm16.compiler.Executable; import de.codesourcery.jasm16.compiler.ICompilationContext; import de.codesourcery.jasm16.compiler.ICompilationListener; import de.codesourcery.jasm16.compiler.ICompilationUnit; import de.codesourcery.jasm16.compiler.ICompiler; import de.codesourcery.jasm16.compiler.ICompiler.CompilerOption; import de.codesourcery.jasm16.compiler.IParentSymbolTable; import de.codesourcery.jasm16.compiler.Linker; import de.codesourcery.jasm16.compiler.ParentSymbolTable; import de.codesourcery.jasm16.compiler.dependencyanalysis.DependencyNode; import de.codesourcery.jasm16.compiler.dependencyanalysis.SourceFileDependencyAnalyzer; import de.codesourcery.jasm16.compiler.io.DefaultResourceMatcher; import de.codesourcery.jasm16.compiler.io.FileObjectCodeWriter; import de.codesourcery.jasm16.compiler.io.FileResource; import de.codesourcery.jasm16.compiler.io.FileResourceResolver; import de.codesourcery.jasm16.compiler.io.IObjectCodeWriter; import de.codesourcery.jasm16.compiler.io.IResource; import de.codesourcery.jasm16.compiler.io.IResource.ResourceType; import de.codesourcery.jasm16.compiler.io.IResourceMatcher; import de.codesourcery.jasm16.compiler.io.IResourceResolver; import de.codesourcery.jasm16.compiler.io.NullObjectCodeWriterFactory; import de.codesourcery.jasm16.compiler.io.SimpleFileObjectCodeWriterFactory; import de.codesourcery.jasm16.exceptions.AmbigousCompilationOrderException; import de.codesourcery.jasm16.exceptions.ResourceNotFoundException; import de.codesourcery.jasm16.exceptions.UnknownCompilationOrderException; import de.codesourcery.jasm16.utils.DebugCompilationListener; import de.codesourcery.jasm16.utils.IOrdered; import de.codesourcery.jasm16.utils.Misc; /** * * * @author tobias.gierke@code-sourcery.de */ public class ProjectBuilder implements IProjectBuilder , IResourceListener, IOrdered { private static final Logger LOG = Logger.getLogger(ProjectBuilder.class); // @GuardedBy( compUnits ) private final List<ICompilationUnit> compUnits = new ArrayList<ICompilationUnit>(); private final IResourceMatcher resourceMatcher = DefaultResourceMatcher.INSTANCE; private final IWorkspace workspace; private final IAssemblyProject project; private final IParentSymbolTable globalSymbolTable; private final AtomicBoolean disposed = new AtomicBoolean(false); private final SourceFileDependencyAnalyzer analyzer = new SourceFileDependencyAnalyzer(); private volatile Executable executable; public ProjectBuilder(IWorkspace workspace,IAssemblyProject project) { if (project == null) { throw new IllegalArgumentException("project must not be null"); } if ( workspace == null ) { throw new IllegalArgumentException("workspace must not be null"); } this.workspace = workspace; this.project = project; this.globalSymbolTable = new ParentSymbolTable( "project: "+project.getName() ); } @Override public void dispose() { this.disposed.set(true); } private void assertNotDisposed() { if ( disposed.get() ) { throw new IllegalStateException("Builder "+this+" is already disposed"); } } @Override public ICompilationUnit parse(IResource source, IResourceResolver resolver , ICompilationListener listener) throws IOException { assertNotDisposed(); final ICompiler compiler = createCompiler(); // do not process .includesource directives here as this would recompile // dependent sources as well (and these are probably already compiled) compiler.setCompilerOption(CompilerOption.NO_SOURCE_INCLUDE_PROCESSING , true ); compiler.setResourceResolver( resolver ); compiler.setObjectCodeWriterFactory(new NullObjectCodeWriterFactory()); final List<ICompilationUnit> toCompile = new ArrayList<>(); final ICompilationUnit newCompilationUnit = CompilationUnit.createInstance( source.getIdentifier() , source ); toCompile.add( newCompilationUnit ); globalSymbolTable.clear( newCompilationUnit ); final List<ICompilationUnit> dependencies = new ArrayList<>( getCompilationUnits() ); for ( int i =0 ; i < dependencies.size() ; i++ ) { final ICompilationUnit unit = dependencies.get(i); if ( unit.getResource().getIdentifier().equals( source.getIdentifier() ) ) { dependencies.remove( i ); break; } } for ( Iterator<ICompilationUnit> it = dependencies.iterator() ; it.hasNext() ; ) { final ICompilationUnit dependency = it.next(); if ( dependency.getAST() == null ) { it.remove(); toCompile.add( dependency ); } } compiler.compile( toCompile , dependencies , globalSymbolTable , listener , DefaultResourceMatcher.INSTANCE ); workspace.compilationFinished( project , newCompilationUnit ); return newCompilationUnit; } private ProjectConfiguration getConfiguration() { return project.getConfiguration(); } protected ICompiler createCompiler() { final ICompiler compiler = new Compiler(); // set compiler options final BuildOptions buildOptions = getConfiguration().getBuildOptions(); compiler.setCompilerOption( CompilerOption.DEBUG_MODE , true ); compiler.setCompilerOption( CompilerOption.RELAXED_PARSING , true ); compiler.setCompilerOption( CompilerOption.LOCAL_LABELS_SUPPORTED,true ); compiler.setCompilerOption( CompilerOption.DISABLE_INLINING, ! buildOptions.isInlineShortLiterals() ); compiler.setCompilerOption( CompilerOption.GENERATE_DEBUG_INFO ,true ); compiler.setCompilerOption( CompilerOption.GENERATE_RELOCATION_INFORMATION , buildOptions.isGenerateSelfRelocatingCode() ); final FileResourceResolver delegate = new FileResourceResolver() { @Override protected ResourceType determineResourceType(File file) { return project.getConfiguration().isSourceFile( file ) ? ResourceType.SOURCE_CODE : ResourceType.UNKNOWN; } }; compiler.setResourceResolver( new IResourceResolver() { @Override public IResource resolveRelative(String identifier, IResource parent) throws ResourceNotFoundException { return delegate.resolveRelative(identifier, parent); } @Override public IResource resolve(String identifier) throws ResourceNotFoundException { return delegate.resolve( identifier ); } }); return compiler; } protected File getOutputFileForSource(IResource resource) { final String objectCodeFile = getNameWithoutSuffix( resource )+".dcpu16"; final File outputDir = getConfiguration().getOutputFolder(); return new File( outputDir , objectCodeFile ); } protected String getNameWithoutSuffix(IResource resource) { String name; if ( resource instanceof FileResource) { FileResource file = (FileResource) resource; name = file.getFile().getName(); } else { name = resource.getIdentifier(); } // get base name final String[] components = name.split("["+Pattern.quote("\\/")+"]"); if ( components.length == 1 ) { name = components[0]; } else { name = components[ components.length -1 ]; } if ( ! name.contains("." ) ) { return name; } final String[] dots = name.split("\\."); return StringUtils.join( ArrayUtils.subarray( dots , 0 , dots.length-1) ); } protected void setObjectCodeOutputFactory(final ICompiler compiler,final List<CompiledCode> objectFiles) { compiler.setObjectCodeWriterFactory( new SimpleFileObjectCodeWriterFactory() { private FileObjectCodeWriter lastWriter; protected IObjectCodeWriter createObjectCodeWriter(ICompilationContext context) { final ICompilationUnit currentUnit = context.getCurrentCompilationUnit(); final File outputFile = getOutputFileForSource( currentUnit.getResource() ); final IResource resource = new FileResource( outputFile , ResourceType.OBJECT_FILE ); WordAddress currentOffset = lastWriter == null ? WordAddress.ZERO : lastWriter.getCurrentWriteOffset().toWordAddress(); // System.out.println(">>>>>>>>> createObjectCodeWriter(): Compiling "+currentUnit+" to object file "+outputFile.getAbsolutePath()+" , offset = "+currentOffset); lastWriter = new FileObjectCodeWriter( outputFile , currentOffset , false ) { protected void closeHook() throws IOException { Address start = getFirstWriteOffset(); Address end = getCurrentWriteOffset(); final int len; if ( start != null && end != null ) { len = end.toByteAddress().getValue() - start.toByteAddress().getValue(); } else { len = 0; } // System.out.println("closeHook(): [ "+start+" - "+end+" ] Closing object file "+outputFile.getAbsolutePath()+", bytes_written: "+len ); if ( len > 0 ) { objectFiles.add( new CompiledCode( currentUnit , resource ) ); workspace.resourceCreated( project , resource ); } } protected void deleteOutputHook() throws IOException { workspace.resourceCreated( project , resource ); }; }; return lastWriter; } } ); } @Override public synchronized boolean build() throws IOException { assertNotDisposed(); // return build( new CompilationListener() ); long time = -System.currentTimeMillis(); try { return build( new DebugCompilationListener(true) ); } finally { time += System.currentTimeMillis(); System.out.println("Building project "+this.project.getName()+" took "+time+" ms"); } } /** * Check whether a given compilation unit is part of the generated executable (compilation root). * * @param unit * @return * @see ProjectConfiguration#getCompilationRoot() */ public boolean isPartOfExecutable(ICompilationUnit unit) { final List<ICompilationUnit> units = getCompilationUnitsForExecutable(); for ( ICompilationUnit that : units ) { if ( that == unit || that.getResource().getIdentifier().equals( unit.getResource().getIdentifier() ) ) { return true; } } return false; } private List<DependencyNode> calculateRootSet() { // compile stuff final List<ICompilationUnit> compilationUnits = getCompilationUnits(); final IResourceMatcher relaxedResolver = new IResourceMatcher() { @Override public boolean isSame(IResource resource1,IResource resource2) { return resource1.getIdentifier().equals( resource2.getIdentifier() ); } }; return analyzer.calculateRootSet( compilationUnits , project , relaxedResolver ); } private List<ICompilationUnit> getCompilationUnitsForExecutable() { final List<DependencyNode> rootSet = calculateRootSet(); DependencyNode nodeToCompile = null; if ( rootSet.size() > 1 ) { for ( DependencyNode n : rootSet ) { final List<ICompilationUnit> units = analyzer.linearize( n ); if ( containsCompilationRoot( units ) ) { nodeToCompile = n; break; } } if ( nodeToCompile == null ) { throw new AmbigousCompilationOrderException( "Project configuration requires a compilation root", rootSet ); } } else if ( rootSet.size() == 1 ) { nodeToCompile = rootSet.get(0); } else { return Collections.emptyList(); } return analyzer.linearize( nodeToCompile ); } @Override public synchronized boolean build(ICompilationListener listener) throws IOException, UnknownCompilationOrderException { assertNotDisposed(); final ICompiler compiler = createCompiler(); /* Set output code writer. * * The following array list will be populated by the ObjectCodeOutputFactory * with all generated object files. */ final List<CompiledCode> objectFiles = new ArrayList<>(); setObjectCodeOutputFactory( compiler , objectFiles ); workspace.buildStarted( project ); boolean buildSuccessful = false; try { // clean output directory clean(); // compile stuff List<ICompilationUnit> units = getCompilationUnits(); if ( units.isEmpty() ) { return true; // => return 'success' immediately } if ( units.size() > 1 ) { File root = getConfiguration().getCompilationRoot() ; if ( root == null ) { throw new IllegalArgumentException("Please set the compilation root on project "+project.getName()); } for (Iterator<ICompilationUnit> it = units.iterator(); it.hasNext();) { final ICompilationUnit unit = it.next(); if ( ! unit.getResource().getIdentifier().equals( root.getAbsolutePath() ) ) { it.remove(); } } if ( units.isEmpty() ) { throw new RuntimeException("Internal error, Failed to find resource for compilation root "+root.getAbsolutePath()+" in project "+project.getName()); } } buildSuccessful = build( units , listener ); } catch (ResourceNotFoundException e) { LOG.error("build(): Caught ",e); } finally { workspace.buildFinished( project , buildSuccessful ); } return buildSuccessful; } private boolean containsCompilationRoot(List<ICompilationUnit> units ) { final File compilationRoot = project.getConfiguration().getCompilationRoot(); if ( compilationRoot == null ) { return false; } for ( ICompilationUnit unit : units ) { if ( unit.getResource().getIdentifier().equals( compilationRoot.getAbsolutePath() ) ) { return true; } } return false; } private boolean build(List<ICompilationUnit> compilationUnits,ICompilationListener listener) throws IOException { final ICompiler compiler = createCompiler(); /* Set output code writer. * * The following array list will be populated by the ObjectCodeOutputFactory * with all generated object files. */ final List<CompiledCode> objectFiles = new ArrayList<>(); setObjectCodeOutputFactory( compiler , objectFiles ); boolean buildSuccessful = false; // compile stuff LOG.info("build(): Starting to build: \n"+StringUtils.join( compilationUnits, "\n" ) ); final IResourceMatcher relaxedResolver = new IResourceMatcher() { @Override public boolean isSame(IResource resource1,IResource resource2) { return resource1.getIdentifier().equals( resource2.getIdentifier() ); } }; globalSymbolTable.clear(); final ArrayList<ICompilationUnit> dependencies = new ArrayList<ICompilationUnit>(); // for ( ICompilationUnit unit : getCompilationUnits() ) // { // boolean alreadyContained = false; // for ( ICompilationUnit unit2 : compilationUnits ) { // if ( unit2.getResource().getIdentifier().equals( unit.getResource().getIdentifier() ) ) { // alreadyContained=true; // break; // } // } // if ( ! alreadyContained ) { // dependencies.add( unit ); // } // } final DebugInfo debugInfo = compiler.compile( compilationUnits , dependencies , globalSymbolTable , listener , relaxedResolver ); // create executable if ( isCompilationSuccessful( compilationUnits ) && ! objectFiles.isEmpty() ) { // System.out.println("\n-------- DEBUG ------------\n"+debugInfo.dumpToString()); final List<CompiledCode> toLink = objectFiles.subList(0, 1 ); LOG.debug("[ "+this+"] Linking "+toLink); executable = link( toLink , debugInfo , compiler.hasCompilerOption( CompilerOption.GENERATE_RELOCATION_INFORMATION ) ); workspace.resourceCreated( project , executable ); buildSuccessful = true; } else { buildSuccessful = false; } return buildSuccessful; } private boolean isCompilationSuccessful( List<ICompilationUnit> compilationUnits) { for ( ICompilationUnit unit : compilationUnits ) { if ( unit.hasErrors() ) { return false; } } return true; } private Executable link(List<CompiledCode> objectFiles, DebugInfo debugInfo, boolean generateRelocatableCode) throws IOException { final File outputFolder = getConfiguration().getOutputFolder(); final File outputFile = new File( outputFolder , getConfiguration().getExecutableName() ); return new Linker().link( objectFiles , debugInfo , outputFile , generateRelocatableCode , true ); } @Override public void clean() throws IOException { assertNotDisposed(); cleanOutputFolder(); } protected void cleanOutputFolder() throws IOException { File folder = getConfiguration().getOutputFolder(); if ( ! folder.exists() ) { if ( ! folder.mkdirs() ) { throw new IOException("Failed to create output folder "+folder.getAbsolutePath()); } return; } for ( File f : folder.listFiles() ) { Misc.deleteRecursively( f ); if ( executable != null && executable.getIdentifier().equals( f.getAbsolutePath() ) ) { executable = null; workspace.resourceDeleted( project , new FileResource( f , ResourceType.EXECUTABLE ) ); } else { workspace.resourceDeleted( project , new FileResource( f , ResourceType.UNKNOWN) ); } } if ( executable != null ) { Executable tmp = executable; executable = null; workspace.resourceDeleted( project , new FileResource( new File(tmp.getIdentifier()) , ResourceType.EXECUTABLE ) ); } } @Override public Executable getExecutable() { assertNotDisposed(); return executable; } public boolean isBuildRequired() { assertNotDisposed(); for ( ICompilationUnit unit : getCompilationUnitsForExecutable()) { if ( unit.getAST() == null || unit.hasErrors() ) { return true; } } if ( getExecutable() == null ) { return true; } return false; } @Override public synchronized ICompilationUnit getCompilationUnit(IResource resource) throws NoSuchElementException { final ICompilationUnit result = findCompilationUnit( resource ); if ( result == null ) { throw new NoSuchElementException("Could not find compilation unit for "+resource); } return result; } private ICompilationUnit findCompilationUnit(IResource source) throws NoSuchElementException { if (source == null) { throw new IllegalArgumentException("source must not be NULL"); } for ( ICompilationUnit unit : getCompilationUnits() ) { if ( resourceMatcher.isSame( unit.getResource() , source ) ) { return unit; } } return null; } @Override public synchronized List<ICompilationUnit> getCompilationUnits() { // TODO: Project building is currently NOT thread-safe , compilation units // passed to callers may be mutated at any time synchronized(compUnits) { return new ArrayList<ICompilationUnit>( compUnits ); } } @Override public void resourceCreated(IAssemblyProject project, IResource resource) { if ( this.project != project) { return; } if ( resource.hasType( ResourceType.SOURCE_CODE ) ) { addCompilationUnit( resource ); } } private void addCompilationUnit(IResource resource) { synchronized (compUnits) { if ( findCompilationUnit( resource ) != null ) { throw new IllegalStateException("Already got a compilation unit for "+resource+" ?"); } compUnits.add( CompilationUnit.createInstance( resource.getIdentifier() , resource ) ); } } private boolean removeCompilationUnit(IResource resource) { synchronized (compUnits) { for (Iterator<ICompilationUnit> it = compUnits.iterator(); it.hasNext();) { final ICompilationUnit existing = it.next(); if ( resourceMatcher.isSame( existing.getResource() , resource ) ) { it.remove(); return true; } } return false; } } @Override public void resourceDeleted(IAssemblyProject project, IResource resource) { if ( this.project != project) { return; } removeCompilationUnit( resource ); maybeRemoveExecutable( resource ); } private void maybeRemoveExecutable(IResource resource) { if ( executable == null ) { return; } if ( executable.refersTo( resource ) ) { IResource tmp = executable; executable = null; workspace.resourceDeleted( project , tmp ); } } @Override public void resourceChanged(IAssemblyProject project, IResource resource) { if ( this.project != project) { return; } final ICompilationUnit found = findCompilationUnit( resource ); if ( found != null ) { removeCompilationUnit( resource ); maybeRemoveExecutable( resource ); } if ( resource.hasType( ResourceType.SOURCE_CODE ) ) { addCompilationUnit( resource ); } } @Override public String toString() { return "ProjectBuilder[ project="+project+" , disposed="+disposed+"]"; } @Override public Priority getPriority() { return Priority.HIGHEST; } }