Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:CobrA_SK
fcmake
app.cpp
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File app.cpp of Package fcmake
/*************************************************************************** * Copyright (C) 2008 Brno University of Technology, * * Faculty of Information Technology * * Author(s): Marek Vavrusa <xvavru00 AT stud.fit.vutbr.cz> * * Zdenek Vasicek <vasicek AT fit.vutbr.cz> * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include <QDir> #include <QTextStream> #include <QDateTime> #include "app.h" #include "tree.h" #include "category.h" #include <sys/stat.h> #include <utime.h> namespace FCMake { // Static variables const char* App::MCU_DIR = "mcu"; const char* App::FPGA_DIR = "fpga"; class App::Private { public: Private() : target(App::Unknown), parsed(false), category(0), fpgaUseProject(0) {} bool parsed; Category* category; QDir path; QDir pathMcu; QDir pathFpga; QDir treeRoot; QString projectFilePath; QString binFilePath; //fpga bitstream QString hexFilePath; //mcu firmware / mcu library QString readme; App::Target target; QString outputPrefix; QString name; QString revision; QString author; QString email; QString description; QString dcmFrequency; QString fpgaChip; QString toplevelEntity; QString ucfFile; App* fpgaUseProject; bool mathLibrary; QStringList fpgaFiles; QStringList mcuFiles; }; App::App(const QString& path, Category* category) : d ( new Private() ) { if (!path.isEmpty()) load(path); d->category = category; } App::~App() { delete d; } Category* App::category() { return d->category; } const QString& App::name() { return d->name; } const QString& App::revision() { return d->revision; } const QString& App::author() { return d->author; } const QString& App::email() { return d->email; } const QString& App::description() { return d->description; } const QString& App::outputPrefix() { return d->outputPrefix; } const QString& App::binFilePath() { return d->binFilePath; } const QString& App::hexFilePath() { return d->hexFilePath; } bool App::hasReadme() { return d->path.exists("README"); } const QString& App::readme() { return d->readme; } bool App::load(const QString& path) { if (path.isEmpty()) return false; d->parsed = false; d->projectFilePath.clear(); if (QFileInfo(path).isFile()) { //project file given d->path = QDir(QFileInfo(path).absolutePath()); d->projectFilePath = path; } else //project path given { d->path = QDir(path); if (d->path.exists("project.xml")) d->projectFilePath = d->path.absoluteFilePath("project.xml"); if (d->path.exists("library.xml")) d->projectFilePath = d->path.absoluteFilePath("library.xml"); if (d->projectFilePath.isEmpty()) return false; } d->pathMcu = d->path; d->pathMcu.cd(MCU_DIR); d->pathFpga = d->path; d->pathFpga.cd(FPGA_DIR); // qDebug("FCMake::App: intialized on path %s", d->path.absolutePath().toStdString().c_str()); return true; } QString App::path() { return d->path.absolutePath(); } bool App::checkElement(QDomElement& element) { //TODO: CHYBA - element.tagName bude vzdy prazdny, kdyz element NEEXISTUJE!! if (element.isNull()) return false; if (!element.nextSiblingElement(element.tagName()).isNull()) { qDebug("Only one <%s> element allowed", element.tagName().toStdString().c_str()); return false; } return true; } bool App::getElementFiles(const QString& section, QDomElement& element, QStringList* list, QDirList& dirList) { QString filePath; for (QDomElement n = element.firstChildElement(); !n.isNull(); n = n.nextSiblingElement()) { if (n.tagName() == "file") { filePath = Tree::findFile(n.text(), dirList); if (filePath.isEmpty()) { qDebug("File %s not found", n.text().toStdString().c_str()); return false; } if (!list->contains(filePath)) list->append(filePath); } else if (n.tagName() == "files") { foreach (const QString file, n.text().split(" ", QString::SkipEmptyParts)) { filePath = Tree::findFile(file, dirList); if (filePath.isEmpty()) { qDebug("File %s not found", n.text().toStdString().c_str()); return false; } if(!list->contains(filePath)) list->append(filePath); } } else if (n.tagName() == "include") { filePath = Tree::findFile(n.text(), dirList); if(filePath.isEmpty()) { qDebug("File %s not found", n.text().toStdString().c_str()); return false; } if(!parseInclude(section, filePath, list)) return false; } else { qDebug("Unknown element <%s> inside <%s> element", n.tagName().toStdString().c_str(), element.tagName().toStdString().c_str()); return false; } } return true; } bool App::getFpgaFiles(QDomElement& element, QDirList* dirs) { if(dirs == 0) { QDirList tmp; return getFpgaFiles(element, &tmp); } *dirs << d->pathFpga << d->path << d->treeRoot; /* qDebug("get fpga files %s %s %s",d->pathFpga.absolutePath().toStdString().c_str(), d->path.absolutePath().toStdString().c_str(), d->treeRoot.absolutePath().toStdString().c_str()); */ return getElementFiles("fpga", element, &d->fpgaFiles, *dirs); } bool App::getMcuFiles(QDomElement& element, QDirList* dirs) { if (dirs == 0) { QDirList tmp; return getMcuFiles(element, &tmp); } *dirs << d->pathMcu << d->path << d->treeRoot; /* qDebug("get mcu files %s %s %s",d->pathMcu.absolutePath().toStdString().c_str(), d->path.absolutePath().toStdString().c_str(), d->treeRoot.absolutePath().toStdString().c_str()); */ return getElementFiles("mcu", element, &d->mcuFiles, *dirs); } bool App::parseInclude(const QString& section, const QString& path, QStringList* list) { App incApp(path); if (!incApp.parse(true)) { qDebug("Can't parse include file %s", path.toStdString().c_str()); return false; } if (incApp.d->target != Package) qDebug("Warning: include file is not package"); if (section == "mcu") { if (incApp.d->mathLibrary) d->mathLibrary = true; foreach (QString file, incApp.d->mcuFiles) { list->append(file); } } if (section == "fpga") { foreach (QString file, incApp.d->fpgaFiles) { list->append(file); } } return true; } bool App::parse() { return parse(false); } bool App::parse(bool isInclude) { if (d->projectFilePath.isEmpty()) { qDebug("Project file does not exist."); return false; } // Parse readme if(hasReadme()) { QFile rfile(d->path.absoluteFilePath("README")); if (rfile.open(QFile::ReadOnly)) { QTextStream in(&rfile); d->readme = in.readAll(); } } // Find tree root d->treeRoot = Tree::findRoot(d->path); if (!d->treeRoot.exists()) { qDebug("Unable to find root directory"); return false; } // Init file paths d->binFilePath = ""; d->hexFilePath = ""; // Load document QDomDocument doc; QFile projectFile(d->projectFilePath); if (!doc.setContent(&projectFile, true)) { qDebug("Unable to parse XML file"); return false; } QDomElement rootNode, node; // Check header element rootNode = doc.documentElement(); if (rootNode.tagName() == "project") d->target = Project; if (rootNode.tagName() == "library") d->target = Library; if ((isInclude) && (rootNode.tagName() == "package")) d->target = Package; if (d->target == Unknown) { qDebug("Unexpected root element \"%s\"", rootNode.tagName().toStdString().c_str()); return false; } d->outputPrefix = rootNode.attribute("outputprefix", "project"); d->fpgaFiles.clear(); d->mcuFiles.clear(); d->mathLibrary = false; // Find file paths if (d->path.exists("build/"+d->outputPrefix+".bin")) d->binFilePath = d->path.absoluteFilePath("build/"+d->outputPrefix+".bin"); if ((d->target == Project) && (d->path.exists("build/"+d->outputPrefix+".hex"))) d->hexFilePath = d->path.absoluteFilePath("build/"+d->outputPrefix+".hex"); if ((d->target == Library) && (d->path.exists("build/"+d->outputPrefix+".a"))) d->hexFilePath = d->path.absoluteFilePath("build/"+d->outputPrefix+".a"); // Check root elements node = rootNode.firstChildElement("mcu"); if (checkElement(node)) { if (node.attribute("mathlibrary", "no") == "yes") { d->mathLibrary = true; } if(!getMcuFiles(node)) { qDebug("failed to get mcu files"); return false; } } else if (d->target != Package) { qDebug("Element mcu not found"); return false; } node = rootNode.firstChildElement("fpga"); if ((d->target != Library) && (checkElement(node))) { if (d->fpgaUseProject != 0) { delete d->fpgaUseProject; d->fpgaUseProject = 0; } QDomElement incNode = doc.createElement("include"); if ((d->target == Project) && (!node.hasAttribute("use"))) node.insertBefore(incNode, node.firstChildElement()); QString arch = node.attribute("architecture", "bare"); incNode.appendChild(doc.createTextNode("fpga/chips/architecture_" + arch +"/package.xml")); d->dcmFrequency = node.attribute("dcmfrequency","25MHz"); if (!((d->dcmFrequency == "25MHz") || (d->dcmFrequency == "50MHz"))) { qDebug("Unknown DCM frequency"); return false; } d->fpgaChip = node.attribute("fpgachip","xc3s50-4-pq208"); d->toplevelEntity = node.attribute("toplevelentity","fpga"); d->ucfFile = node.attribute("ucffile",""); if ((!d->ucfFile.isEmpty()) && (!d->path.exists(d->ucfFile))) { qDebug("UCF file %s not found", d->ucfFile.toStdString().c_str()); return false; } if (node.hasAttribute("use")) { if (node.hasChildNodes()) { qDebug("Attribute \"use\" forbids to include/use additional files"); return false; } QString useFilePath = Tree::findFile(node.attribute("use",""), QDirList() << d->path << d->treeRoot); d->fpgaUseProject = new App(useFilePath); if (!d->fpgaUseProject->parse()) { qDebug("Can't use the file %s (parse error)", node.attribute("use","").toStdString().c_str()); return false; } d->binFilePath.clear(); if (d->fpgaUseProject->d->path.exists("build/"+d->fpgaUseProject->d->outputPrefix+".bin")) d->binFilePath = d->fpgaUseProject->d->path.absoluteFilePath("build/"+d->fpgaUseProject->d->outputPrefix+".bin"); } if (!getFpgaFiles(node)) //only syntax check { qDebug("failed to get fpga files"); return false; } } else if (d->target == Project) { qDebug("Element fpga not found"); return false; } node = rootNode.firstChildElement("name"); if(checkElement(node)) d->name = node.text(); node = rootNode.firstChildElement("revision"); if(checkElement(node)) d->revision = node.text(); node = rootNode.firstChildElement("author"); if(checkElement(node)) d->author = node.text(); node = rootNode.firstChildElement("authoremail"); if(checkElement(node)) d->email = node.text(); if (d->target == Project) { node = rootNode.firstChildElement("description"); if(checkElement(node)) d->description = node.text(); } d->parsed = true; return true; } //remove generated files and build/*.* except HEX and BIN (cleanAll = false) bool App::cleanDir(QString path, bool isBuildDir, bool cleanAll) { QDir dir(path); QString filePath; bool res = true; qDebug("Clean:%s",path.toStdString().c_str()); foreach (QFileInfo entry, dir.entryInfoList(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Readable)) { filePath = entry.absoluteFilePath(); if (entry.isDir()) { if (cleanDir(filePath, false, cleanAll)) { res = dir.rmdir(filePath); if (!res) qDebug("Can't delete directory: %s",filePath.toStdString().c_str()); } else { res = false; } } if (entry.isFile()) { if ((cleanAll) || ((entry.fileName() == d->outputPrefix + ".bin") && (d->fpgaUseProject != 0)) || ((entry.fileName() != d->outputPrefix + ".hex") && (entry.fileName() != d->outputPrefix + ".bin") && (entry.fileName() != d->outputPrefix + ".a")) ) { if (!dir.remove(filePath)) { qDebug("Can't delete file: %s",filePath.toStdString().c_str()); res = false; } } } } return res; } bool App::parseCheck() { // Parse first if(!d->parsed) { if(!parse()) { qDebug("Unable to parse project file."); return false; } } return true; } bool App::clean(bool cleanAll) { bool res = true; if (!parseCheck()) return false; if ((d->path.exists("Makefile")) && (!d->path.remove("Makefile"))) { qDebug("Unable to delete Makefile file"); res = false; } if(d->path.exists("build")) { QString buildPath = d->path.absoluteFilePath("build"); if (!cleanDir(buildPath, true, cleanAll)) res = false; if ((res) && (cleanAll)) d->path.rmdir("build"); } return res; } bool App::isCompiled() { if (!parseCheck()) return false; if (d->target == Project) { if (d->fpgaUseProject != 0) { if (d->fpgaUseProject->d->binFilePath.isEmpty()) return false; } return (!d->binFilePath.isEmpty()) && (!d->hexFilePath.isEmpty()); } if (d->target == Library) return (!d->hexFilePath.isEmpty()); return false; } bool App::isUpdated() { //qDebug("CHECK %s",d->path.absolutePath().toStdString().c_str()); if ((!parseCheck()) || (!isCompiled()) || (d->projectFilePath.isEmpty())) return false; QDateTime date; QDateTime maxDate; if (d->target == Project) { //maxDate init: BIN depends on the files generated by fcmake (these have the same mtime as XML file) maxDate = QFileInfo(d->projectFilePath).lastModified(); foreach (QString file, d->fpgaFiles) { date = QFileInfo(file).lastModified(); if (date > maxDate) maxDate = date; //qDebug(file.toStdString().c_str()); } if (d->fpgaUseProject != 0) { //as the used project doesn't depend on our sources, //it is sufficient to check isCompiled() only! if (!d->fpgaUseProject->isCompiled()) return false; } //qDebug("CHECK - test final max:%s lastmod:%s", fpgaMaxDate.toString().toStdString().c_str(), // QFileInfo(d->binFilePath).lastModified().toString().toStdString().c_str()); if ((!d->binFilePath.isEmpty()) && (QFileInfo(d->binFilePath).lastModified() < maxDate)) return false; } if (d->hexFilePath.isEmpty()) { qDebug("hex empty"); return false; } maxDate = QDateTime(); //null time foreach (QString file, d->mcuFiles) { date = QFileInfo(file).lastModified(); if (date > maxDate) maxDate = date; //qDebug("%s mtime: %d",file.toStdString().c_str(), date.toTime_t()); } //libfitkit source files dependency if (d->treeRoot.absoluteFilePath(LIBFITKIT_PATH) != d->path.absolutePath()) { App libapp(d->treeRoot.absoluteFilePath(LIBFITKIT_PATH)); libapp.parse(); if (!libapp.isUpdated()) return false; foreach (QString file, libapp.d->mcuFiles) { date = QFileInfo(file).lastModified(); if (date > maxDate) maxDate = date; //qDebug("%s amtime: %d",file.toStdString().c_str(), date.toTime_t()); } } /* qDebug("Final: %s mtime: %d maxmtime:%d",d->hexFilePath.toStdString().c_str(), QFileInfo(d->hexFilePath).lastModified().toTime_t(), maxDate.toTime_t()); */ if (QFileInfo(d->hexFilePath).lastModified() < maxDate) return false; return true; } bool copyMTime(const QString& fromFilePath, const QString& toFilePath) { struct stat sb; struct utimbuf utb; if (stat(fromFilePath.toStdString().c_str(), &sb) < 0) return false; utb.actime = sb.st_atime; //access time utb.modtime = sb.st_mtime; //modification time if (utime(toFilePath.toStdString().c_str(), &utb) < 0) return false; return true; } bool App::createMakefile(const QString& fcmakePath, const QString& fkflashPath) { if (!parseCheck()) return false; //========================================================================== //Output generation - MCU //========================================================================== //Create build directory (if not exists) if(!d->path.exists("build")) { if(!d->path.mkdir("build")) { qDebug("Unable to create <build> directory"); return false; } } if (d->target != Library) { if (!d->path.exists("build/mcu")) { if (!d->path.mkdir("build/mcu")) { qDebug("Unable to create <build/mcu> directory"); return false; } } if(!d->path.exists("build/fpga")) { if(!d->path.mkdir("build/fpga")) { qDebug("Unable to create <build/fpga> directory"); return false; } } } // Create project Makefile if (d->fpgaUseProject != 0) d->fpgaUseProject->createMakefile(fcmakePath, fkflashPath); // Create project Makefile QFile makeFile(d->path.absoluteFilePath("Makefile")); if (!makeFile.open(QIODevice::WriteOnly)) { qDebug("Unable to create Makefile"); return false; } QTextStream out(&makeFile); if (d->target == Library) out << "TARGET = library\n"; out << "BASE = " << d->path.relativeFilePath(d->treeRoot.absolutePath()) << "\n"; if (!fcmakePath.isEmpty()) out << "FCMAKE = " << fcmakePath << "\n"; if (!fkflashPath.isEmpty()) out << "FKFLASH = " << fkflashPath << "\n"; out << "PROJECT = " << d->path.relativeFilePath(d->projectFilePath) << "\n"; out << "OUTPUTPREFIX = " << d->outputPrefix << "\n"; if (d->mathLibrary) out << "LIBRARIES = -lm\n"; //project depends on libfitkit library bool hasDependencies = (d->treeRoot.absoluteFilePath(LIBFITKIT_PATH) != d->path.absolutePath()); if (d->fpgaUseProject != 0) { hasDependencies = true; out << "DEPENDENCIES = " << d->path.relativeFilePath(d->fpgaUseProject->path()) << "\n"; } out << "\n"; out << "all:"; if (hasDependencies) out << " dependencycheck"; if (d->target == Library) out << " build/" << d->outputPrefix << ".a\n"; if (d->target == Project) { out << " build/" << d->outputPrefix << ".hex"; if (d->fpgaUseProject == 0) out << " build/" << d->outputPrefix << ".bin"; out << "\n"; } // out << "\n"; // out << "Makefile: " << d->path.relativeFilePath(d->projectFile) << "\n" << "\t$(FCMAKE) $*\n"; out << "\n#MCU part\n"; out << "#=====================================================================\n"; out << "HEXFILE = " << d->path.relativeFilePath(d->hexFilePath) << "\n\n"; QString ofiles; foreach (QString file, d->mcuFiles) { QFileInfo fi(file); QString ofile; QString cfile = d->path.relativeFilePath(file); ofile = "build/"; if (d->target == Project) ofile += "mcu/"; ofile += fi.completeBaseName() + ".o"; if (!ofiles.isEmpty()) ofiles += " "; ofiles += ofile; out << ofile << ": " << cfile << "\n"; // out << "\t$(CC) $(COPT) -o " << ofile << " -c " << cfile << "\n\n"; out << "\t$(CC) $(COPT) -o $@ -c $<\n\n"; } out << "OBJFILES = " << ofiles << "\n\n"; //========================================================================== //Output generation - FPGA //========================================================================== if (d->target == Project) { out << "#FPGA part\n"; out << "#=====================================================================\n"; if (!d->ucfFile.isEmpty()) out << "FPGAUCF = " << d->ucfFile << "\n"; out << "BINFILE = " << d->path.relativeFilePath(d->binFilePath) << "\n\n"; QFile prjFile(d->path.absoluteFilePath("build/fpga/" + d->outputPrefix + "_config.vhd")); if (!prjFile.open(QIODevice::WriteOnly)) { qDebug("Unable to create config file"); return false; } QTextStream outp(&prjFile); outp << "-- fpga_config.vhd: user constants\nuse work.clkgen_cfg.all;\n\n"; outp << "package fpga_cfg is\n"; outp << " constant DCM_FREQUENCY : dcm_freq := DCM_" << d->dcmFrequency << ";\n"; outp << "end fpga_cfg;\n\n"; outp << "package body fpga_cfg is\nend fpga_cfg;\n"; prjFile.close(); if (!copyMTime(d->projectFilePath, prjFile.fileName())) qDebug("Warning: Can't alter config mtime"); prjFile.setFileName(d->path.absoluteFilePath("build/fpga/" + d->outputPrefix + ".prj")); if (!prjFile.open(QIODevice::WriteOnly)) { qDebug("Unable to create XST project file"); return false; } outp.setDevice(&prjFile); outp << "vhdl work \"" << QDir::toNativeSeparators(d->path.absoluteFilePath("build/fpga/" + d->outputPrefix + "_config.vhd")) << "\"\n"; bool first = true; foreach (QString file, d->fpgaFiles) { outp << "vhdl work \"" << QDir::toNativeSeparators(file) << "\"\n"; out << "HDLFILES "; out << ((first) ? " = " : "+= "); out << d->path.relativeFilePath(file) << "\n"; first = false; } out << "\n"; prjFile.close(); if (!copyMTime(d->projectFilePath, prjFile.fileName())) qDebug("Warning: Can't alter project mtime"); prjFile.setFileName(d->path.absoluteFilePath("build/fpga/" + d->outputPrefix + ".lso")); if (!prjFile.open(QIODevice::WriteOnly)) { qDebug("Unable to create LSO file"); return false; } prjFile.write("work", 4); prjFile.close(); if (!copyMTime(d->projectFilePath, prjFile.fileName())) qDebug("Warning: Can't alter lso mtime"); prjFile.setFileName(d->path.absoluteFilePath("build/fpga/" + d->outputPrefix + ".xst")); if (!prjFile.open(QIODevice::WriteOnly)) { qDebug("Unable to create XST script file"); return false; } outp.setDevice(&prjFile); outp << "set -tmpdir build/fpga -xsthdpdir build/fpga\n"; outp << "run -ifn build/fpga/" + d->outputPrefix + ".prj "; outp << "-ifmt mixed -opt_mode SPEED -opt_level 1 "; outp << "-ofn build/fpga/" + d->outputPrefix + ".ngc "; outp << "-ofmt NGC -lso build/fpga/" + d->outputPrefix + ".lso "; outp << "-p " << d->fpgaChip << " -top " << d->toplevelEntity << " -rtlview yes"; prjFile.close(); if (!copyMTime(d->projectFilePath, prjFile.fileName())) qDebug("Warning: Can't alter xst script mtime"); if (d->pathFpga.exists("sim/sim.fdo")) { QDir simPath = d->pathFpga; simPath.cd("sim"); prjFile.setFileName(d->path.absoluteFilePath("build/fpga/" + d->outputPrefix + "_sim.fdo")); if (!prjFile.open(QIODevice::WriteOnly)) { qDebug("Unable to create FDO script file"); return false; } outp.setDevice(&prjFile); outp << "variable VHDLFILES\n"; outp << "set SIMMODEL \"" << simPath.relativeFilePath(d->path.absoluteFilePath("build/fpga/" + d->outputPrefix + ".timesim.vhd")) << "\"\n\n"; foreach (QString file, d->fpgaFiles) { outp << "lappend VHDLFILES \"" << simPath.relativeFilePath(file) << "\"\n"; if (file.endsWith("clkgen_config.vhd")) //dependency HACK outp << "lappend VHDLFILES \"" << simPath.relativeFilePath(d->path.absoluteFilePath("build/fpga/" + d->outputPrefix + "_config.vhd")) << "\"\n"; } outp << "\nsource \"" << simPath.relativeFilePath(d->treeRoot.absoluteFilePath("base/modelsim.fdo")) << "\"\n"; prjFile.close(); if (!copyMTime(d->projectFilePath, prjFile.fileName())) qDebug("Warning: Can't alter FDO script mtime"); } QString ofile = "build/fpga/" + d->outputPrefix; if (d->fpgaUseProject == 0) { out << "build/" << d->outputPrefix << ".bin: "; out << ofile << ".par.ncd " << ofile << ".pcf\n"; out << "\t$(BITGEN) $(BITGENFLAGS) $< build/" << d->outputPrefix << ".bit "; out << ofile << ".pcf && $(RM) xilinx_device_details.xml\n\n"; } } out << "include $(BASE)/base/Makefile.inc\n"; makeFile.close(); if (!copyMTime(d->projectFilePath, makeFile.fileName())) qDebug("Warning: Can't alter Makefile mtime"); return true; } }
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor