/**
* 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;
}
}