mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-12 23:44:25 +02:00
Write the same PDF for the same project when SOURCE_DATE_EPOCH is set
Two exports of one project differed in the creation and modification dates (the time of the export, in local time) and in the document id (random). With SOURCE_DATE_EPOCH set (reproducible-builds.org), --export-pdf now uses the time it names, in UTC, and a document id derived from the project: version 5 of the project uuid with the SHA-256 of the project file, so it is the same for the same file and changes when the file does. Qt has no setter for the dates, so PdfLinks::setDocumentDate() rewrites them in the document information and the XMP metadata after the file is written, fixing the XMP /Length and shifting the xref table. Without the variable nothing changes. Only --export-pdf reads it; the print window's PDF export does not, as a person exporting by hand wants the real date. qet_export (misc/qet-mcp) gains "reproducible" and "source_date_epoch". It reports whether the build honoured the variable, since an older one ignores it. tst_pdfreproducible exports examples/741.qet four times and requires identical bytes and the epoch's date; it fails with either this change or the hash seed fix reverted. Projects with cross-reference links also need their links in a fixed order, which is a separate fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -407,6 +407,19 @@ target_link_libraries(tst_pdfpagesize PRIVATE Qt::Test Qt::Gui)
|
||||
target_compile_definitions(tst_pdfpagesize PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# With SOURCE_DATE_EPOCH set, --export-pdf writes the same bytes for the
|
||||
# same project in every run. Exports examples/741.qet several times, in
|
||||
# separate processes.
|
||||
add_executable(
|
||||
tst_pdfreproducible
|
||||
tst_pdfreproducible.cpp)
|
||||
add_test(NAME tst_pdfreproducible COMMAND tst_pdfreproducible)
|
||||
add_dependencies(tst_pdfreproducible qelectrotech)
|
||||
target_link_libraries(tst_pdfreproducible PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_pdfreproducible PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# One text per potential: the number goes on the same conductor in every
|
||||
# run when two conductors tie for longest. Exports
|
||||
# fixtures/one_text_per_potential_tie.qet through the real binary, several
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
With SOURCE_DATE_EPOCH set, --export-pdf writes the same bytes for the
|
||||
same project in every run: the dates are the ones it names, the document
|
||||
id comes from the project file, and the fonts come in a fixed order.
|
||||
Without it the dates are the time of the export, as before. Exported in
|
||||
separate processes, since what used to differ changed between runs.
|
||||
*/
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
#include <QTemporaryDir>
|
||||
#include <QtTest>
|
||||
|
||||
class tst_pdfreproducible : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
int m_run = 0;
|
||||
|
||||
QByteArray exportPdf(const QString &project, const QByteArray &epoch)
|
||||
{
|
||||
const int run = m_run++;
|
||||
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(run));
|
||||
const QString tmp = m_dir.filePath(QStringLiteral("tmp%1").arg(run));
|
||||
const QString out = m_dir.filePath(QStringLiteral("out%1.pdf").arg(run));
|
||||
QDir().mkpath(home);
|
||||
QDir().mkpath(tmp);
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/.config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/.local/share"));
|
||||
env.insert(QStringLiteral("TMPDIR"), tmp);
|
||||
env.remove(QStringLiteral("QT_HASH_SEED"));
|
||||
env.remove(QStringLiteral("SOURCE_DATE_EPOCH"));
|
||||
if (!epoch.isEmpty())
|
||||
env.insert(QStringLiteral("SOURCE_DATE_EPOCH"), QString::fromLatin1(epoch));
|
||||
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||
{QStringLiteral("--export-pdf"), project, out});
|
||||
if (!proc.waitForFinished(120000) || proc.exitCode() != 0)
|
||||
return {};
|
||||
QFile file(out);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return {};
|
||||
return file.readAll();
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
}
|
||||
|
||||
void sameBytesEveryRun()
|
||||
{
|
||||
// A project with no cross-reference links: their order is fixed
|
||||
// separately.
|
||||
const QString project =
|
||||
QStringLiteral(QET_EXAMPLES_DIR) + QStringLiteral("/741.qet");
|
||||
QVERIFY2(QFile::exists(project), "examples/741.qet not found");
|
||||
|
||||
const QByteArray first = exportPdf(project, "1700000000");
|
||||
QVERIFY(!first.isEmpty());
|
||||
QVERIFY(first.contains("/CreationDate (D:20231114221320Z)"));
|
||||
QVERIFY(first.contains("xmp:CreateDate=\"2023-11-14T22:13:20Z\""));
|
||||
for (int i = 0; i < 3; ++i)
|
||||
QVERIFY2(exportPdf(project, "1700000000") == first,
|
||||
"two exports of the same project differ");
|
||||
}
|
||||
|
||||
void nowWithoutTheVariable()
|
||||
{
|
||||
const QString project =
|
||||
QStringLiteral(QET_EXAMPLES_DIR) + QStringLiteral("/741.qet");
|
||||
const QByteArray pdf = exportPdf(project, QByteArray());
|
||||
QVERIFY(!pdf.isEmpty());
|
||||
const QByteArray year =
|
||||
"/CreationDate (D:" + QByteArray::number(QDate::currentDate().year());
|
||||
QVERIFY(pdf.contains(year));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_pdfreproducible)
|
||||
#include "tst_pdfreproducible.moc"
|
||||
Reference in New Issue
Block a user