Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
8c610eeceThis file is pinned to the selected release. Download the original to open it in your design tool.
/*
* 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