Loading files
/*
* LibrePCB - Professional EDA for everyone!
* Copyright (C) 2013 LibrePCB Developers, see AUTHORS.md for contributors.
* https://librepcb.org/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 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 General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*******************************************************************************
* Includes
******************************************************************************/
#include "fileformatmigrationv1.h"
#include "../fileio/transactionaldirectory.h"
#include "../fileio/versionfile.h"
#include "../types/length.h"
#include "sexpression.h"
#include <QtCore>
/*******************************************************************************
* Namespace
******************************************************************************/
namespace librepcb {
/*******************************************************************************
* Constructors / Destructor
******************************************************************************/
FileFormatMigrationV1::FileFormatMigrationV1(QObject* parent) noexcept
: FileFormatMigration(Version::fromString("1"), Version::fromString("2"),
parent) {
}
FileFormatMigrationV1::~FileFormatMigrationV1() noexcept {
}
/*******************************************************************************
* General Methods
******************************************************************************/
void FileFormatMigrationV1::upgradeComponentCategory(
TransactionalDirectory& dir) {
// Version File.
upgradeVersionFile(dir, ".librepcb-cmpcat");
}
void FileFormatMigrationV1::upgradePackageCategory(
TransactionalDirectory& dir) {
// Version File.
upgradeVersionFile(dir, ".librepcb-pkgcat");
}
void FileFormatMigrationV1::upgradeSymbol(TransactionalDirectory& dir) {
// Version File.
upgradeVersionFile(dir, ".librepcb-sym");
// Content File.
{
const QString fp = "symbol.lp";
std::unique_ptr<SExpression> root =
SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
root->appendChild("grid_interval", SExpression::createToken("2.54"));
upgradeTexts(*root, true);
dir.write(fp, root->toByteArray());
}
}
void FileFormatMigrationV1::upgradePackage(TransactionalDirectory& dir) {
// Version File.
upgradeVersionFile(dir, ".librepcb-pkg");
// Content File.
{
const QString fp = "package.lp";
std::unique_ptr<SExpression> root =
SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
root->appendChild("grid_interval", SExpression::createToken("2.54"));
root->appendChild("min_copper_clearance", SExpression::createToken("0.2"));
// Footprints.
for (SExpression* fptNode : root->getChildren("footprint")) {
// Add tags depending on name.
const QString name = fptNode->getChild("name/@0").getValue().toLower();
if (name.contains("density level a")) {
fptNode->appendChild("tag", QString("ipc-density-level-a"));
}
if (name.contains("density level b")) {
fptNode->appendChild("tag", QString("ipc-density-level-b"));
}
if (name.contains("density level c")) {
fptNode->appendChild("tag", QString("ipc-density-level-c"));
}
if (name.contains("hand")) {
fptNode->appendChild("tag", QString("hand-soldering"));
}
if (name.contains("reflow")) {
fptNode->appendChild("tag", QString("reflow-soldering"));
}
if (name.contains("wave")) {
fptNode->appendChild("tag", QString("wave-soldering"));
}
if (name.contains("large")) {
fptNode->appendChild("tag", QString("extra-large-pads"));
}
// Pads.
for (SExpression* padNode : fptNode->getChildren("pad")) {
// Revert possibly made manual change as a workaround for bug, see
// https://librepcb.discourse.group/t/migrating-libraries-from-old-version-1-0-pressfit-problem/810
SExpression& padFunction = padNode->getChild("function/@0");
if (padFunction.getValue() == "press_fit") {
padFunction.setValue("pressfit");
}
}
// Stroke texts.
for (SExpression* child : fptNode->getChildren("stroke_text")) {
const QSet<QString> unlockedLayers = {"top_names", "top_values",
"bot_names", "bot_values"};
const QString layer = child->getChild("layer/@0").getValue();
const bool lock = !unlockedLayers.contains(layer);
child->appendChild("lock",
SExpression::createToken(lock ? "true" : "false"));
}
}
dir.write(fp, root->toByteArray());
}
}
void FileFormatMigrationV1::upgradeComponent(TransactionalDirectory& dir) {
// Version File.
upgradeVersionFile(dir, ".librepcb-cmp");
}
void FileFormatMigrationV1::upgradeDevice(TransactionalDirectory& dir) {
// Version File.
upgradeVersionFile(dir, ".librepcb-dev");
// Content File.
{
const QString fp = "device.lp";
std::unique_ptr<SExpression> root =
SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
// Pinout.
for (SExpression* padNode : root->getChildren("pad")) {
padNode->appendChild("optional", SExpression::createToken("false"));
}
dir.write(fp, root->toByteArray());
}
}
void FileFormatMigrationV1::upgradeOrganization(TransactionalDirectory& dir) {
Q_UNUSED(dir);
// Didn't exist yet.
}
void FileFormatMigrationV1::upgradeLibrary(TransactionalDirectory& dir) {
// Version File.
upgradeVersionFile(dir, ".librepcb-lib");
}
void FileFormatMigrationV1::upgradeProject(TransactionalDirectory& dir,
QList<Message>& messages) {
// ATTENTION: Do not actually perform any upgrade in this method! Instead,
// just call virtual protected methods which do the upgrade. This allows
// FileFormatMigrationUnstable to override them with partial upgrades.
ProjectContext context;
// Version File.
upgradeVersionFile(dir, ".librepcb-project");
// Symbols.
foreach (const QString& dirName, dir.getDirs("library/sym")) {
TransactionalDirectory subDir(dir, "library/sym/" % dirName);
if (subDir.fileExists(".librepcb-sym")) {
upgradeSymbol(subDir);
}
}
// Packages.
foreach (const QString& dirName, dir.getDirs("library/pkg")) {
TransactionalDirectory subDir(dir, "library/pkg/" % dirName);
if (subDir.fileExists(".librepcb-pkg")) {
upgradePackage(subDir);
}
}
// Components.
foreach (const QString& dirName, dir.getDirs("library/cmp")) {
TransactionalDirectory subDir(dir, "library/cmp/" % dirName);
if (subDir.fileExists(".librepcb-cmp")) {
upgradeComponent(subDir);
}
}
// Devices.
foreach (const QString& dirName, dir.getDirs("library/dev")) {
TransactionalDirectory subDir(dir, "library/dev/" % dirName);
if (subDir.fileExists(".librepcb-dev")) {
upgradeDevice(subDir);
}
}
// Get schematics list.
// This is important to upgrade only the used schematics. If there are unused
// schematic files left over in the project, they could cause the upgrade to
// fail. It's better to just ignore the unused files (if any).
QStringList schematicFiles; // Relative file paths
{
const QString fp = "schematics/schematics.lp";
const std::unique_ptr<const SExpression> root =
SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
foreach (const SExpression* child, root->getChildren("schematic")) {
schematicFiles.append(child->getChild("@0").getValue());
}
}
// Get boards list.
// This is important to upgrade only the used boards. If there are unused
// board files left over in the project, they could cause the upgrade to
// fail. It's better to just ignore the unused files (if any).
QStringList boardFiles; // Relative file paths
{
const QString fp = "boards/boards.lp";
const std::unique_ptr<const SExpression> root =
SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
foreach (const SExpression* child, root->getChildren("board")) {
boardFiles.append(child->getChild("@0").getValue());
}
}
// Metadata.
{
const QString fp = "project/metadata.lp";
std::unique_ptr<SExpression> root =
SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
upgradeMetadata(*root, messages);
dir.write(fp, root->toByteArray());
}
// Settings.
{
const QString fp = "project/settings.lp";
std::unique_ptr<SExpression> root =
SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
upgradeSettings(*root, messages);
dir.write(fp, root->toByteArray());
}
// Output Jobs.
{
const QString fp = "project/jobs.lp";
std::unique_ptr<SExpression> root =
SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
upgradeOutputJobs(*root, context);
dir.write(fp, root->toByteArray());
}
// Circuit.
{
const QString fp = "circuit/circuit.lp";
std::unique_ptr<SExpression> root =
SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
upgradeCircuit(*root, messages);
dir.write(fp, root->toByteArray());
}
// Schematics.
foreach (const QString& fp, schematicFiles) {
std::unique_ptr<SExpression> root =
SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
upgradeSchematic(*root);
dir.write(fp, root->toByteArray());
}
// Boards.
foreach (const QString& fp, boardFiles) {
++context.boardCount;
std::unique_ptr<SExpression> root =
SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
upgradeBoard(*root);
dir.write(fp, root->toByteArray());
}
// Emit messages at the very end to avoid duplicate messages caused my
// multiple schematics/boards.
if ((context.boardCount > 0) && (!context.hasGerberOutputJob)) {
messages.append(buildMessage(
Message::Severity::Warning,
tr("The dedicated Gerber/Excellon generator dialog has been removed "
"in favor of the more powerful output jobs, and the corresponding "
"output settings will be removed from boards in an upcoming "
"release. It is recommended to add a Gerber/Excellon output job "
"now, as this allows to migrate the old export settings (choose "
"\"Import Old Settings\")."),
1));
}
}
void FileFormatMigrationV1::upgradeWorkspaceData(TransactionalDirectory& dir) {
// Create version file.
dir.write(".librepcb-data", VersionFile(mToVersion).toByteArray());
// Remove legacy files.
const QStringList filesToRemove = {
"cache_v3", //
"cache_v4", //
"cache_v5", //
"cache_v6", //
"cache_v7", //
};
TransactionalDirectory librariesDir(dir, "libraries");
foreach (const QString fileName, librariesDir.getFiles()) {
if (filesToRemove.contains(fileName.split(".").first())) {
qInfo() << "Removing legacy file:"
<< librariesDir.getAbsPath(fileName).toNative();
librariesDir.removeFile(fileName);
}
}
// Upgrade settings.
const QString settingsFp = "settings.lp";
if (dir.fileExists(settingsFp)) {
std::unique_ptr<SExpression> root =
SExpression::parse(dir.read(settingsFp), dir.getAbsPath(settingsFp));
if (SExpression* node = root->tryGetChild("api_endpoints")) {
int index = 0;
foreach (SExpression* child, node->getChildren("url")) {
child->setName("endpoint");
child->appendChild("libraries", SExpression::createToken("true"));
child->appendChild(
"parts", SExpression::createToken((index == 0) ? "true" : "false"));
child->appendChild(
"order", SExpression::createToken((index == 0) ? "true" : "false"));
++index;
}
}
dir.write(settingsFp, root->toByteArray());
}
}
/*******************************************************************************
* Protected Methods
******************************************************************************/
void FileFormatMigrationV1::upgradeMetadata(SExpression& root,
QList<Message>& messages) {
// FileProofName does no longer allow string consisting of only dots
// (e.g. "..") so we rename them.
SExpression& versionNode = root.getChild("version/@0");
if (auto newVersion = upgradeFileProofName(versionNode.getValue())) {
versionNode.setValue(*newVersion);
// Not translated because it's unlikely someone will ever see this message.
messages.append(buildMessage(
Message::Severity::Note,
"Project version has been adjusted due to more restrictive naming "
"requirements. Please review the new version number.",
1));
}
}
void FileFormatMigrationV1::upgradeSettings(SExpression& root,
QList<Message>& messages) {
// The manual BOM export has been removed. If the user has configured custom
// BOM attributes, just remind him to use output jobs now.
QStringList customBomAttributes;
for (const SExpression* node :
root.getChild("custom_bom_attributes").getChildren("attribute")) {
customBomAttributes.append(node->getChild("@0").getValue());
}
if (!customBomAttributes.isEmpty()) {
messages.append(buildMessage(
Message::Severity::Note,
tr("The project has set custom attributes for the BOM export (%1). But "
"in LibrePCB 2.0, the manual BOM export has been removed in favor "
"of the more powerful output jobs feature. Please use output jobs "
"now to generate the BOM. When you add a new BOM output job, those "
"custom attributes will automatically be imported.")
.arg(customBomAttributes.join(", ")),
1));
}
}
void FileFormatMigrationV1::upgradeOutputJobs(SExpression& root,
ProjectContext& context) {
for (SExpression* jobNode : root.getChildren("job")) {
if (jobNode->getChild("type/@0").getValue() == "graphics") {
for (SExpression* contentNode : jobNode->getChildren("content")) {
SExpression& contentTypeNode = contentNode->getChild("type/@0");
if (contentTypeNode.getValue() == "schematic") {
// Add the new layer for image borders.
SExpression& imgBordersLayerNode = contentNode->appendList("layer");
imgBordersLayerNode.appendChild(
SExpression::createToken("schematic_image_borders"));
imgBordersLayerNode.appendChild(
"color", SExpression::createString("#ff808080"));
// Add the new layer for buses.
SExpression& busesLayerNode = contentNode->appendList("layer");
busesLayerNode.appendChild(
SExpression::createToken("schematic_buses"));
busesLayerNode.appendChild("color",
SExpression::createString("#ff008eff"));
// Add the new layer for bus labels.
SExpression& busLabelsLayerNode = contentNode->appendList("layer");
busLabelsLayerNode.appendChild(
SExpression::createToken("schematic_bus_labels"));
busLabelsLayerNode.appendChild(
"color", SExpression::createString("#ff008eff"));
} else if (contentTypeNode.getValue() == "board") {
// We don't need to check the option value since "realistic" was
// the only supported option in v1.
const auto optionNodes = contentNode->getChildren("option");
for (SExpression* optionNode : optionNodes) {
contentNode->removeChild(*optionNode);
}
if (!optionNodes.isEmpty()) {
contentTypeNode.setValue("board_rendering");
for (SExpression* layerNode : contentNode->getChildren("layer")) {
contentNode->removeChild(*layerNode);
}
auto addLayer = [&contentNode](const QString& layer,
const QString& color) {
SExpression& node = contentNode->appendList("layer");
node.appendChild(SExpression::createToken(layer));
node.appendChild("color", color);
};
if (contentNode->getChild("mirror/@0").getValue() == "true") {
addLayer("board_copper_bottom", "#ffbc9c69");
addLayer("board_legend_bottom", "#00000000");
addLayer("board_outlines", "#ff465046");
addLayer("board_stop_mask_bottom", "#00000000");
} else {
addLayer("board_copper_top", "#ffbc9c69");
addLayer("board_legend_top", "#00000000");
addLayer("board_outlines", "#ff465046");
addLayer("board_stop_mask_top", "#00000000");
}
}
}
}
} else if (jobNode->getChild("type/@0").getValue() == "gerber_excellon") {
context.hasGerberOutputJob = true;
} else if (jobNode->getChild("type/@0").getValue() == "gerber_x3") {
jobNode->getChild("top").setName("components_top");
jobNode->getChild("bottom").setName("components_bot");
SExpression& glueTop = jobNode->appendList("glue_top");
glueTop.appendChild("create", SExpression::createToken("false"));
glueTop.appendChild(
"output",
SExpression::createString(
"assembly/{{PROJECT}}_{{VERSION}}_GLUE_{{VARIANT}}_TOP.gbr"));
root.ensureLineBreak();
SExpression& glueBot = jobNode->appendList("glue_bot");
glueBot.appendChild("create", SExpression::createToken("false"));
glueBot.appendChild(
"output",
SExpression::createString(
"assembly/{{PROJECT}}_{{VERSION}}_GLUE_{{VARIANT}}_BOT.gbr"));
root.ensureLineBreak();
}
}
}
void FileFormatMigrationV1::upgradeCircuit(SExpression& root,
QList<Message>& messages) {
// Assembly variants.
int renamedAssemblyVariants = 0;
for (SExpression* variantNode : root.getChildren("variant")) {
// FileProofName does no longer allow string consisting of only dots
// (e.g. "..") so we rename them. We don't do conflict resolution here
// as it is very unlikely to ever happen.
SExpression& nameNode = variantNode->getChild("name/@0");
if (auto newName = upgradeFileProofName(nameNode.getValue())) {
nameNode.setValue(*newName);
++renamedAssemblyVariants;
}
}
if (renamedAssemblyVariants > 0) {
// Not translated because it's unlikely someone will ever see this message.
messages.append(buildMessage(
Message::Severity::Note,
"Assembly variants have been renamed due to more restrictive naming "
"requirements. Please review the new names.",
renamedAssemblyVariants));
}
// Net classes.
for (SExpression* classNode : root.getChildren("netclass")) {
classNode->appendChild("default_trace_width",
SExpression::createToken("inherit"));
classNode->appendChild("default_via_drill_diameter",
SExpression::createToken("inherit"));
classNode->appendChild("min_copper_copper_clearance",
SExpression::createToken("0"));
classNode->appendChild("min_copper_width", SExpression::createToken("0"));
classNode->appendChild("min_via_drill_diameter",
SExpression::createToken("0"));
}
}
void FileFormatMigrationV1::upgradeSchematic(SExpression& root) {
for (SExpression* symbolNode : root.getChildren("symbol")) {
upgradeTexts(*symbolNode, true); // Lock texts depending on layer.
}
upgradeTexts(root, false); // Do not lock any schematic text.
}
void FileFormatMigrationV1::upgradeBoard(SExpression& root) {
// Design rules
{
SExpression& rulesNode = root.getChild("design_rules");
rulesNode.appendChild("default_trace_width",
SExpression::createToken("0.5"));
rulesNode.appendChild("default_via_drill_diameter",
SExpression::createToken("0.3"));
}
// DRC settings.
{
SExpression& drcNode = root.getChild("design_rule_check");
drcNode.ensureLineBreak();
{
SExpression& child = drcNode.appendList("min_pcb_size");
child.appendChild(SExpression::createToken("0.0"));
child.appendChild(SExpression::createToken("0.0"));
}
drcNode.ensureLineBreak();
{
SExpression& child = drcNode.appendList("max_pcb_size");
SExpression& doubleSided = child.appendList("double_sided");
doubleSided.appendChild(SExpression::createToken("0.0"));
doubleSided.appendChild(SExpression::createToken("0.0"));
SExpression& multilayer = child.appendList("multilayer");
multilayer.appendChild(SExpression::createToken("0.0"));
multilayer.appendChild(SExpression::createToken("0.0"));
}
drcNode.ensureLineBreak();
drcNode.appendList("pcb_thickness");
drcNode.ensureLineBreak();
drcNode.appendChild("max_layers", SExpression::createToken("0"));
drcNode.ensureLineBreak();
drcNode.appendList("solder_resist");
drcNode.ensureLineBreak();
drcNode.appendList("silkscreen");
drcNode.ensureLineBreak();
drcNode.appendChild("max_tented_via_drill_diameter",
SExpression::createToken("0.5"));
drcNode.ensureLineBreak();
}
// DRC approvals.
{
SExpression& drcNode = root.getChild("design_rule_check");
const QString approvalsVersion =
drcNode.getChild("approvals_version/@0").getValue();
for (SExpression* approvalNode : drcNode.getChildren("approved")) {
SExpression& approvalTypeNode = approvalNode->getChild("@0");
if ((approvalTypeNode.getValue() == "useless_via") &&
(approvalsVersion != "2")) {
approvalTypeNode.setValue("invalid_via");
} else if (approvalTypeNode.getValue() == "antennae_via") {
approvalTypeNode.setValue("useless_via");
}
}
}
// Preferred footprint tags
{
SExpression& child = root.appendList("preferred_footprint_tags");
child.ensureLineBreak();
child.appendList("tht_top");
child.ensureLineBreak();
child.appendList("tht_bot");
child.ensureLineBreak();
child.appendList("smt_top");
child.ensureLineBreak();
child.appendList("smt_bot");
child.ensureLineBreak();
child.appendList("common");
child.ensureLineBreak();
}
// Devices
for (SExpression* devNode : root.getChildren("device")) {
devNode->appendChild("glue", SExpression::createToken("true"));
}
// Net segments
for (SExpression* devNode : root.getChildren("netsegment")) {
// Vias
for (SExpression* viaNode : devNode->getChildren("via")) {
SExpression& drillNode = viaNode->getChild("drill/@0");
SExpression& sizeNode = viaNode->getChild("size/@0");
const PositiveLength drill = deserialize<PositiveLength>(drillNode);
const PositiveLength size = deserialize<PositiveLength>(sizeNode);
if (size < drill) {
// No longer valid in LibrePCB 2.0!
sizeNode.setValue(drillNode.getValue());
}
}
}
// Planes
for (SExpression* planeNode : root.getChildren("plane")) {
SExpression& clrNode = planeNode->getChild("min_clearance");
clrNode.setName("min_copper_clearance");
planeNode->appendChild(
"min_board_clearance",
std::make_unique<SExpression>(clrNode.getChild("@0")));
planeNode->appendChild(
"min_npth_clearance",
std::make_unique<SExpression>(clrNode.getChild("@0")));
}
}
void FileFormatMigrationV1::upgradeTexts(SExpression& node, bool allowLock) {
for (SExpression* child : node.getChildren("text")) {
const QString layer = child->getChild("layer/@0").getValue();
const bool lock =
allowLock && (layer != "sym_names") && (layer != "sym_values");
child->appendChild("lock",
SExpression::createToken(lock ? "true" : "false"));
}
}
std::optional<QString> FileFormatMigrationV1::upgradeFileProofName(
QString name) {
if (QRegularExpression("\\A\\.+\\z")
.match(name, 0, QRegularExpression::PartialPreferCompleteMatch)
.hasMatch()) {
return name.replace(".", "_");
}
return std::nullopt;
}
/*******************************************************************************
* End of File
******************************************************************************/
} // namespace librepcb
0e185510cThis file is pinned to the selected release. Download the original to open it in your design tool.