// slang-gcc-compiler-util.cpp #include "slang-gcc-compiler-util.h" #include "../core/slang-common.h" #include "../../slang-com-helper.h" #include "../core/slang-string-util.h" #include "../core/slang-io.h" #include "../core/slang-shared-library.h" #include "../core/slang-char-util.h" namespace Slang { static Index _findVersionEnd(const UnownedStringSlice& in) { Index numDots = 0; const Index len = in.getLength(); for (Index i = 0; i < len; ++i) { const char c = in[i]; if (CharUtil::isDigit(c)) { continue; } if (c == '.') { if (numDots >= 2) { return i; } numDots++; continue; } return i; } return len; } /* static */SlangResult GCCDownstreamCompilerUtil::parseVersion(const UnownedStringSlice& text, const UnownedStringSlice& prefix, DownstreamCompiler::Desc& outDesc) { List lines; StringUtil::calcLines(text, lines); for (auto line : lines) { Index prefixIndex = line.indexOf(prefix); if (prefixIndex < 0) { continue; } const UnownedStringSlice remainingSlice = UnownedStringSlice(line.begin() + prefixIndex + prefix.getLength(), line.end()).trim(); const Index versionEndIndex = _findVersionEnd(remainingSlice); if (versionEndIndex < 0) { return SLANG_FAIL; } const UnownedStringSlice versionSlice(remainingSlice.begin(), remainingSlice.begin() + versionEndIndex); // Version is in format 0.0.0 List split; StringUtil::split(versionSlice, '.', split); List digits; for (auto v : split) { Int version; SLANG_RETURN_ON_FAIL(StringUtil::parseInt(v, version)); digits.add(version); } if (digits.getCount() < 2) { return SLANG_FAIL; } outDesc.majorVersion = digits[0]; outDesc.minorVersion = digits[1]; return SLANG_OK; } return SLANG_FAIL; } SlangResult GCCDownstreamCompilerUtil::calcVersion(const ExecutableLocation& exe, DownstreamCompiler::Desc& outDesc) { CommandLine cmdLine; cmdLine.setExecutableLocation(exe); cmdLine.addArg("-v"); ExecuteResult exeRes; SLANG_RETURN_ON_FAIL(ProcessUtil::execute(cmdLine, exeRes)); // Note we now have builds that add other words in front of the version // such as "Ubuntu clang version" const UnownedStringSlice prefixes[] = { UnownedStringSlice::fromLiteral("clang version"), UnownedStringSlice::fromLiteral("gcc version"), UnownedStringSlice::fromLiteral("Apple LLVM version"), }; const SlangPassThrough types[] = { SLANG_PASS_THROUGH_CLANG, SLANG_PASS_THROUGH_GCC, SLANG_PASS_THROUGH_CLANG, }; SLANG_COMPILE_TIME_ASSERT(SLANG_COUNT_OF(prefixes) == SLANG_COUNT_OF(types)); for (Index i = 0; i < SLANG_COUNT_OF(prefixes); ++i) { // Set the type outDesc.type = types[i]; // Extract the version if (SLANG_SUCCEEDED(parseVersion(exeRes.standardError.getUnownedSlice(), prefixes[i], outDesc))) { return SLANG_OK; } } return SLANG_FAIL; } static SlangResult _parseSeverity(const UnownedStringSlice& in, DownstreamDiagnostic::Severity& outSeverity) { typedef DownstreamDiagnostic::Severity Severity; if (in == "error" || in == "fatal error") { outSeverity = Severity::Error; } else if (in == "warning") { outSeverity = Severity::Warning; } else if (in == "info" || in == "note") { outSeverity = Severity::Info; } else { return SLANG_FAIL; } return SLANG_OK; } namespace { // anonymous enum class LineParseResult { Single, ///< It's a single line Start, ///< Line was the start of a message Continuation, ///< Not totally clear, add to previous line if nothing else hit Ignore, ///< Ignore the line }; } // anonymous static SlangResult _parseGCCFamilyLine(const UnownedStringSlice& line, LineParseResult& outLineParseResult, DownstreamDiagnostic& outDiagnostic) { typedef DownstreamDiagnostic Diagnostic; typedef Diagnostic::Severity Severity; // Set to default case outLineParseResult = LineParseResult::Ignore; /* example error output from different scenarios */ /* tests/cpp-compiler/c-compile-error.c: In function 'int main(int, char**)': tests/cpp-compiler/c-compile-error.c:8:13: error: 'b' was not declared in this scope int a = b + c; ^ tests/cpp-compiler/c-compile-error.c:8:17: error: 'c' was not declared in this scope int a = b + c; ^ */ /* /tmp/ccS0JCWe.o:c-compile-link-error.c:(.rdata$.refptr.thing[.refptr.thing]+0x0): undefined reference to `thing' collect2: error: ld returned 1 exit status*/ /* clang: warning: treating 'c' input as 'c++' when in C++ mode, this behavior is deprecated [-Wdeprecated] Undefined symbols for architecture x86_64: "_thing", referenced from: _main in c-compile-link-error-a83ace.o ld: symbol(s) not found for architecture x86_64 clang: error: linker command failed with exit code 1 (use -v to see invocation) */ /* /tmp/c-compile-link-error-ccf151.o: In function `main': c-compile-link-error.c:(.text+0x19): undefined reference to `thing' clang: error: linker command failed with exit code 1 (use -v to see invocation) */ /* /tmp/c-compile-link-error-301c8c.o: In function `main': /home/travis/build/shader-slang/slang/tests/cpp-compiler/c-compile-link-error.c:10: undefined reference to `thing' clang-7: error: linker command failed with exit code 1 (use -v to see invocation)*/ /* /path/slang-cpp-prelude.h:4:10: fatal error: ../slang.h: No such file or directory #include "../slang.h" ^~~~~~~~~~~~ compilation terminated.*/ /* g++: error: unrecognized command line option ‘-std=c++14’ */ outDiagnostic.stage = Diagnostic::Stage::Compile; List split; StringUtil::split(line, ':', split); // On windows we can have paths that are a: etc... if we detect this we can combine 0 - 1 to be 1. if (split.getCount() > 1 && split[0].getLength() == 1) { const char c = split[0][0]; if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { // We'll assume it's a path UnownedStringSlice path(split[0].begin(), split[1].end()); split.removeAt(0); split[0] = path; } } if (split.getCount() == 2) { const auto split0 = split[0].trim(); if (split0 == UnownedStringSlice::fromLiteral("ld")) { // We'll ignore for now outDiagnostic.stage = Diagnostic::Stage::Link; outDiagnostic.severity = Severity::Info; outDiagnostic.text = split[1].trim(); outLineParseResult = LineParseResult::Start; return SLANG_OK; } if (SLANG_SUCCEEDED(_parseSeverity(split0, outDiagnostic.severity))) { // Command line errors can be just contain 'error:' etc. Can be seen on apple/clang outDiagnostic.stage = Diagnostic::Stage::Compile; outDiagnostic.text = split[1].trim(); outLineParseResult = LineParseResult::Single; return SLANG_OK; } outLineParseResult = LineParseResult::Ignore; return SLANG_OK; } else if (split.getCount() == 3) { const auto split0 = split[0].trim(); const auto split1 = split[1].trim(); const auto text = split[2].trim(); // Check for special handling for clang (Can be Clang or clang apparently) if (split0.startsWith(UnownedStringSlice::fromLiteral("clang")) || split0.startsWith(UnownedStringSlice::fromLiteral("Clang")) || split0 == UnownedStringSlice::fromLiteral("g++") || split0 == UnownedStringSlice::fromLiteral("gcc")) { // Extract the type SLANG_RETURN_ON_FAIL(_parseSeverity(split[1].trim(), outDiagnostic.severity)); if (text.startsWith("linker command failed")) { outDiagnostic.stage = Diagnostic::Stage::Link; } outDiagnostic.text = text; outLineParseResult = LineParseResult::Start; return SLANG_OK; } else if (split1.startsWith("(.text")) { // This is a little weak... but looks like it's a link error outDiagnostic.filePath = split[0]; outDiagnostic.severity = Severity::Error; outDiagnostic.stage = Diagnostic::Stage::Link; outDiagnostic.text = text; outLineParseResult = LineParseResult::Single; return SLANG_OK; } else if (text.startsWith("ld returned")) { outDiagnostic.stage = DownstreamDiagnostic::Stage::Link; SLANG_RETURN_ON_FAIL(_parseSeverity(split[1].trim(), outDiagnostic.severity)); outDiagnostic.text = line; outLineParseResult = LineParseResult::Single; return SLANG_OK; } else if (text == "") { // This is probably a prelude line, we'll just ignore it outLineParseResult = LineParseResult::Ignore; return SLANG_OK; } } else if (split.getCount() == 4) { // Probably a link error, give the source line String ext = Path::getPathExt(split[0]); // Maybe a bit fragile -> but probably okay for now if (ext != "o" && ext != "obj") { outLineParseResult = LineParseResult::Ignore; return SLANG_OK; } else { outDiagnostic.filePath = split[1]; outDiagnostic.fileLine = 0; outDiagnostic.severity = Diagnostic::Severity::Error; outDiagnostic.stage = Diagnostic::Stage::Link; outDiagnostic.text = split[3]; outLineParseResult = LineParseResult::Start; return SLANG_OK; } } else if (split.getCount() >= 5) { // Probably a regular error line SLANG_RETURN_ON_FAIL(_parseSeverity(split[3].trim(), outDiagnostic.severity)); outDiagnostic.filePath = split[0]; SLANG_RETURN_ON_FAIL(StringUtil::parseInt(split[1], outDiagnostic.fileLine)); // Everything from 4 to the end is the error outDiagnostic.text = UnownedStringSlice(split[4].begin(), split.getLast().end()); outLineParseResult = LineParseResult::Start; return SLANG_OK; } // Assume it's a continuation outLineParseResult = LineParseResult::Continuation; return SLANG_OK; } /* static */SlangResult GCCDownstreamCompilerUtil::parseOutput(const ExecuteResult& exeRes, DownstreamDiagnostics& outOutput) { LineParseResult prevLineResult = LineParseResult::Ignore; outOutput.reset(); outOutput.rawDiagnostics = exeRes.standardError; for (auto line : LineParser(exeRes.standardError.getUnownedSlice())) { Diagnostic diagnostic; diagnostic.reset(); LineParseResult lineRes; SLANG_RETURN_ON_FAIL(_parseGCCFamilyLine(line, lineRes, diagnostic)); switch (lineRes) { case LineParseResult::Start: { // It's start of a new message outOutput.diagnostics.add(diagnostic); prevLineResult = LineParseResult::Start; break; } case LineParseResult::Single: { // It's a single message, without anything following outOutput.diagnostics.add(diagnostic); prevLineResult = LineParseResult::Ignore; break; } case LineParseResult::Continuation: { if (prevLineResult == LineParseResult::Start || prevLineResult == LineParseResult::Continuation) { if (outOutput.diagnostics.getCount() > 0) { // We are now in a continuation, add to the last auto& text = outOutput.diagnostics.getLast().text; text.append("\n"); text.append(line); } prevLineResult = LineParseResult::Continuation; } break; } case LineParseResult::Ignore: { prevLineResult = lineRes; break; } default: return SLANG_FAIL; } } if (outOutput.has(Diagnostic::Severity::Error) || exeRes.resultCode != 0) { outOutput.result = SLANG_FAIL; } return SLANG_OK; } /* static */ SlangResult GCCDownstreamCompilerUtil::calcModuleFilePath(const CompileOptions& options, StringBuilder& outPath) { SLANG_ASSERT(options.modulePath.getLength()); outPath.Clear(); switch (options.targetType) { case SLANG_SHARED_LIBRARY: { outPath << SharedLibrary::calcPlatformPath(options.modulePath.getUnownedSlice()); return SLANG_OK; } case SLANG_EXECUTABLE: { outPath << options.modulePath; outPath << Process::getExecutableSuffix(); return SLANG_OK; } case SLANG_OBJECT_CODE: { #if __CYGWIN__ outPath << options.modulePath << ".obj"; #else // Will be .o for typical gcc targets outPath << options.modulePath << ".o"; #endif return SLANG_OK; } } return SLANG_FAIL; } /* static */SlangResult GCCDownstreamCompilerUtil::calcCompileProducts(const CompileOptions& options, ProductFlags flags, List& outPaths) { SLANG_ASSERT(options.modulePath.getLength()); outPaths.clear(); if (flags & ProductFlag::Execution) { StringBuilder builder; SLANG_RETURN_ON_FAIL(calcModuleFilePath(options, builder)); outPaths.add(builder); } return SLANG_OK; } /* static */SlangResult GCCDownstreamCompilerUtil::calcArgs(const CompileOptions& options, CommandLine& cmdLine) { SLANG_ASSERT(options.sourceContents.getLength() == 0); SLANG_ASSERT(options.modulePath.getLength()); PlatformKind platformKind = (options.platform == PlatformKind::Unknown) ? PlatformUtil::getPlatformKind() : options.platform; if (options.sourceLanguage == SLANG_SOURCE_LANGUAGE_CPP) { cmdLine.addArg("-fvisibility=hidden"); // Need C++14 for partial specialization cmdLine.addArg("-std=c++14"); } // TODO(JS): Here we always set -m32 on x86. It could be argued it is only necessary when creating a shared library // but if we create an object file, we don't know what to choose because we don't know what final usage is. // It could also be argued that the platformKind could define the actual desired target - but as it stands // we only have a target of 'Linux' (as opposed to Win32/64). Really it implies we need an arch enumeration too. // // For now we just make X86 binaries try and produce x86 compatible binaries as fixes the immediate problems. #if SLANG_PROCESSOR_X86 /* Used to specify the processor more broadly. For a x86 binary we need to make sure we build x86 builds even when on an x64 system. -m32 -m64*/ cmdLine.addArg("-m32"); #endif switch (options.optimizationLevel) { case OptimizationLevel::None: { // No optimization cmdLine.addArg("-O0"); break; } case OptimizationLevel::Default: { cmdLine.addArg("-Os"); break; } case OptimizationLevel::High: { cmdLine.addArg("-O2"); break; } case OptimizationLevel::Maximal: { cmdLine.addArg("-O4"); break; } default: break; } if (options.debugInfoType != DebugInfoType::None) { cmdLine.addArg("-g"); } if (options.flags & CompileOptions::Flag::Verbose) { cmdLine.addArg("-v"); } switch (options.floatingPointMode) { case FloatingPointMode::Default: break; case FloatingPointMode::Precise: { //cmdLine.addArg("-fno-unsafe-math-optimizations"); break; } case FloatingPointMode::Fast: { // We could enable SSE with -mfpmath=sse // But that would only make sense on a x64/x86 type processor and only if that feature is present (it is on all x64) cmdLine.addArg("-ffast-math"); break; } } StringBuilder moduleFilePath; calcModuleFilePath(options, moduleFilePath); cmdLine.addArg("-o"); cmdLine.addArg(moduleFilePath); switch (options.targetType) { case SLANG_SHARED_LIBRARY: { // Shared library cmdLine.addArg("-shared"); if (PlatformUtil::isFamily(PlatformFamily::Unix, platformKind)) { // Position independent cmdLine.addArg("-fPIC"); } break; } case SLANG_EXECUTABLE: { break; } case SLANG_OBJECT_CODE: { // Don't link, just produce object file cmdLine.addArg("-c"); break; } default: break; } // Add defines for (const auto& define : options.defines) { StringBuilder builder; builder << "-D"; builder << define.nameWithSig; if (define.value.getLength()) { builder << "=" << define.value; } cmdLine.addArg(builder); } // Add includes for (const auto& include : options.includePaths) { cmdLine.addArg("-I"); cmdLine.addArg(include); } // Link options if (0) // && options.targetType != TargetType::Object) { //linkOptions << "-Wl,"; //cmdLine.addArg(linkOptions); } if (options.targetType == SLANG_SHARED_LIBRARY) { if (!PlatformUtil::isFamily(PlatformFamily::Apple, platformKind)) { // On MacOS, this linker option is not supported. That's ok though in // so far as on MacOS it does report any unfound symbols without the option. // Linker flag to report any undefined symbols as a link error cmdLine.addArg("-Wl,--no-undefined"); } } // Files to compile for (const auto& sourceFile : options.sourceFiles) { cmdLine.addArg(sourceFile); } for (const auto& libPath : options.libraryPaths) { // Note that any escaping of the path is handled in the ProcessUtil:: cmdLine.addArg("-L"); cmdLine.addArg(libPath); cmdLine.addArg("-F"); cmdLine.addArg(libPath); } if (options.sourceLanguage == SLANG_SOURCE_LANGUAGE_CPP && !PlatformUtil::isFamily(PlatformFamily::Windows, platformKind)) { // Make STD libs available cmdLine.addArg("-lstdc++"); // Make maths lib available cmdLine.addArg("-lm"); } return SLANG_OK; } /* static */SlangResult GCCDownstreamCompilerUtil::createCompiler(const ExecutableLocation& exe, RefPtr& outCompiler) { DownstreamCompiler::Desc desc; SLANG_RETURN_ON_FAIL(GCCDownstreamCompilerUtil::calcVersion(exe, desc)); RefPtr compiler(new GCCDownstreamCompiler(desc)); compiler->m_cmdLine.setExecutableLocation(exe); outCompiler = compiler; return SLANG_OK; } /* static */SlangResult GCCDownstreamCompilerUtil::locateGCCCompilers(const String& path, ISlangSharedLibraryLoader* loader, DownstreamCompilerSet* set) { SLANG_UNUSED(loader); RefPtr compiler; if (SLANG_SUCCEEDED(createCompiler(ExecutableLocation(path, "g++"), compiler))) { // A downstream compiler for Slang must currently support C++14 - such that // the prelude and generated code works. // // The first version of gcc that supports `-std=c++14` is 5.0 // https://gcc.gnu.org/projects/cxx-status.html // // If could be argued to allow C/C++ compilations via older versions through an older version // but that requires some more complex behavior, so we don't allow for now. auto desc = compiler->getDesc(); if (desc.majorVersion < 5) { // If the version isn't 5 or higher, we don't add this version of the compiler. return SLANG_OK; } set->addCompiler(compiler); } return SLANG_OK; } /* static */SlangResult GCCDownstreamCompilerUtil::locateClangCompilers(const String& path, ISlangSharedLibraryLoader* loader, DownstreamCompilerSet* set) { SLANG_UNUSED(loader); RefPtr compiler; if (SLANG_SUCCEEDED(createCompiler(ExecutableLocation(path, "clang"), compiler))) { set->addCompiler(compiler); } return SLANG_OK; } }