Rickard's Projects

This site hosts my projects.

Projects

Recent events

2025-07-06 09:11 Roger pushed to timeline

changeset:   7992:7bdcd3d4dc9a
parent:      7974:d18436c111ab
user:        roger <roger@rolidata.se>
date:        Sun Jul 06 09:06:44 2025 +0200
summary:     Fixed problem with LANG setting for translations

diff -r d18436c111ab -r 7bdcd3d4dc9a source/timeline.py
--- a/source/timeline.py Sat Apr 19 13:41:56 2025 +0200
+++ b/source/timeline.py Sun Jul 06 09:06:44 2025 +0200
@@ -50,7 +50,7 @@
         # The appropriate environment variables are set on other systems
         import wx
         loc = wx.Locale()
-        language = loc.GetLanguageName(loc.GetSystemLanguage())
+        language = loc.GetLanguageCanonicalName(loc.GetSystemLanguage())
         os.environ['LANG'] = language
         if getattr(sys, 'frozen', False):
             os.chdir(os.path.dirname(sys.executable))

changeset:   7993:dd4c4b92daa3
tag:         tip
parent:      7992:7bdcd3d4dc9a
parent:      7991:ca9470a3fae4
user:        roger <roger@rolidata.se>
date:        Sun Jul 06 09:10:31 2025 +0200
summary:     Merged

diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 Dockerfile.zip-to-exe.ci
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Dockerfile.zip-to-exe.ci Sun Jul 06 09:10:31 2025 +0200
@@ -0,0 +1,32 @@
+FROM fedora:41
+
+ENV WINEDEBUG=-all
+ENV WINEPREFIX=/opt/wine
+ENV PYTHON_DIR=C:\\Python
+ENV PYTHON_PIP_EXE=C:\\Python\\Scripts\\pip.exe
+ENV INNO_DIR=C:\\Inno
+
+RUN dnf update -y
+RUN dnf install -y wine
+RUN dnf install -y wget
+RUN dnf install -y xorg-x11-server-Xvfb
+
+ENV PYTHON_VERSION=3.13.5
+ENV WXPYTHON_VERSION=4.2.3
+
+RUN wget https://www.python.org/ftp/python/$PYTHON_VERSION/python-$PYTHON_VERSION-amd64.exe
+RUN wget https://files.innosetup.nl/innosetup-6.4.3.exe
+
+# https://jrsoftware.org/ishelp/index.php?topic=setupcmdline
+
+RUN umask 0 && xvfb-run bash -c '\
+       wine python-$PYTHON_VERSION-amd64.exe /quiet TargetDir=$PYTHON_DIR \
+    && wine $PYTHON_PIP_EXE install --no-warn-script-location pyinstaller \
+    && wine $PYTHON_PIP_EXE install --no-warn-script-location wxpython==$WXPYTHON_VERSION \
+    && wine $PYTHON_PIP_EXE install --no-warn-script-location humblewx \
+    && wine $PYTHON_PIP_EXE install --no-warn-script-location markdown \
+    && wine $PYTHON_PIP_EXE install --no-warn-script-location icalendar \
+    && wine innosetup-6.4.3.exe /SP- /VERYSILENT /SUPPRESSMSGBOXES /DIR=$INNO_DIR \
+    && wineserver -w'
+
+CMD ["xvfb-run", "python", "tools/build-windows-exe-from-source-zip.py", "--output-list", "Dockerfile.zip-to-exe.ci.files"]
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 Dockerfile.zip.ci
--- a/Dockerfile.zip.ci Sun Jul 06 09:06:44 2025 +0200
+++ b/Dockerfile.zip.ci Sun Jul 06 09:10:31 2025 +0200
@@ -7,4 +7,4 @@
 RUN /venv/bin/pip install icalendar
 RUN /venv/bin/pip install Markdown
 
-CMD ["xvfb-run", "/venv/bin/python", "tools/build-source-zip.py", "--output-list", "Dockerfile.linux311.ci.files", "--output-list", "Dockerfile.zip.ci.files"]
+CMD ["xvfb-run", "/venv/bin/python", "tools/build-source-zip.py", "--output-list", "Dockerfile.zip.ci.files"]
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 documentation/changelog.rst
--- a/documentation/changelog.rst Sun Jul 06 09:06:44 2025 +0200
+++ b/documentation/changelog.rst Sun Jul 06 09:10:31 2025 +0200
@@ -3,11 +3,11 @@
 
 Version 2.11.0
 --------------
-    **Planned to be Released on 9 August 2025.**
-
-    *Don't want to wait for the final release? Try the beta version!*
-
-    * `Download source <https://projects.rickardlindberg.me/artifacts/timeline/>`_.
+**Planned to be Released on 9 August 2025.**
+
+*Don't want to wait for the final release? Try the beta version!*
+
+* Beta versions: |betas|_
 
 New features, enhancements:
 
@@ -34,6 +34,10 @@
 * ``Corrupted icon file causes exception``
   Use a default icon when icon file is corrupted and disable error dialog.
 
+Windows installer:
+
+* The Windows installer is now built on Linux/Wine and only for 64 bit platforms.
+
 
 Version 2.10.0
 --------------
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 documentation/conf.py
--- a/documentation/conf.py Sun Jul 06 09:06:44 2025 +0200
+++ b/documentation/conf.py Sun Jul 06 09:10:31 2025 +0200
@@ -69,13 +69,24 @@
                 break
     return latest_version
 
+def get_current_version():
+    with open("changelog.rst") as f:
+        while True:
+            line = f.readline()
+            if line.startswith("Version "):
+                return line[8:].strip()
+    return "null"
+
 rst_epilog = """
 .. |latest_zip| replace:: timeline-%(version)s.zip
 .. _latest_zip: https://projects.rickardlindberg.me/artifacts/timeline/%(version)s/timeline-%(version)s.zip
 .. |latest_exe| replace:: timeline-%(version)s-Win32Setup.exe
 .. _latest_exe: https://projects.rickardlindberg.me/artifacts/timeline/%(version)s/timeline-%(version)s-Win32Setup.exe
+.. |betas| replace:: all beta versions
+.. _betas: https://projects.rickardlindberg.me/artifacts/timeline/%(current_version)s
 """ % {
     "version": get_latest_version(),
+    "current_version": get_current_version(),
     "exe_version": get_latest_version().replace(".", ""),
 }
 
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 documentation/installing.rst
--- a/documentation/installing.rst Sun Jul 06 09:06:44 2025 +0200
+++ b/documentation/installing.rst Sun Jul 06 09:10:31 2025 +0200
@@ -23,7 +23,7 @@
 Download one of the following installers:
 
 * Latest release: |latest_exe|_
-* Beta version: `latest Windows build <https://projects.rickardlindberg.me/artifacts/timeline/>`_
+* Beta versions: |betas|_
 
 The beta version is for users that want to try the latest features and give
 feedback on them before a release.
@@ -56,7 +56,7 @@
 Download one of the following source packages:
 
 * Latest release: |latest_zip|_
-* Beta version: `latest source build <https://projects.rickardlindberg.me/artifacts/timeline/>`_
+* Beta versions: |betas|_
 
 The beta version is for users that want to try the latest features and give
 feedback on them before a release.
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 source/timelinelib/meta/version.py
--- a/source/timelinelib/meta/version.py Sun Jul 06 09:06:44 2025 +0200
+++ b/source/timelinelib/meta/version.py Sun Jul 06 09:10:31 2025 +0200
@@ -30,7 +30,7 @@
     if TYPE:
         parts.append(TYPE)
     if REVISION_HASH or REVISION_DATE:
-        revision_parts = [item for item in (REVISION_HASH, REVISION_DATE) if item]
+        revision_parts = [item for item in (REVISION_DATE, REVISION_HASH) if item]
         parts.append("(%s)" % " ".join(revision_parts))
     return " ".join(parts)
 
@@ -38,7 +38,7 @@
 def get_filename_version():
     parts = ["timeline", get_version_number_string()]
     if not is_final():
-        parts.extend([TYPE, REVISION_HASH, REVISION_DATE])
+        parts.extend([TYPE, REVISION_DATE, REVISION_HASH])
     return "-".join(parts)
 
 
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 test/unit/meta/version.py
--- a/test/unit/meta/version.py Sun Jul 06 09:06:44 2025 +0200
+++ b/test/unit/meta/version.py Sun Jul 06 09:10:31 2025 +0200
@@ -43,13 +43,13 @@
     def test_full(self):
         self.assertEqual(
             version.get_full_version(),
-            "1.2.3 beta (abc123 2015-11-11)"
+            "1.2.3 beta (2015-11-11 abc123)"
         )
 
     def test_filename(self):
         self.assertEqual(
             version.get_filename_version(),
-            "timeline-1.2.3-beta-abc123-2015-11-11"
+            "timeline-1.2.3-beta-2015-11-11-abc123"
         )
 
     def test_version_numer_string(self):
@@ -77,13 +77,13 @@
     def test_full(self):
         self.assertEqual(
             version.get_full_version(),
-            "1.2.3 development (abc123 2015-11-11)"
+            "1.2.3 development (2015-11-11 abc123)"
         )
 
     def test_filename(self):
         self.assertEqual(
             version.get_filename_version(),
-            "timeline-1.2.3-development-abc123-2015-11-11"
+            "timeline-1.2.3-development-2015-11-11-abc123"
         )
 
     def test_version_numer_string(self):
@@ -111,7 +111,7 @@
     def test_full(self):
         self.assertEqual(
             version.get_full_version(),
-            "1.2.3 (abc123 2015-11-11)"
+            "1.2.3 (2015-11-11 abc123)"
         )
 
     def test_filename(self):
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 tools/build-source-zip.py
--- a/tools/build-source-zip.py Sun Jul 06 09:06:44 2025 +0200
+++ b/tools/build-source-zip.py Sun Jul 06 09:10:31 2025 +0200
@@ -22,7 +22,6 @@
 import argparse
 import json
 import os
-import shutil
 import tempfile
 
 import timelinetools.packaging.repository
@@ -40,11 +39,8 @@
 
 
 def package_source(arguments):
-    tempdir = tempfile.mkdtemp()
-    try:
+    with tempfile.TemporaryDirectory(dir=".") as tempdir:
         create_source_zip(arguments, tempdir)
-    finally:
-        shutil.rmtree(tempdir)
 
 
 def create_source_zip(arguments, tempdir):
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 tools/build-windows-exe-from-source-zip.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/build-windows-exe-from-source-zip.py Sun Jul 06 09:10:31 2025 +0200
@@ -0,0 +1,122 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
+#
+# This file is part of Timeline.
+#
+# Timeline 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.
+#
+# Timeline 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 Timeline.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from os import environ
+from os.path import basename, join
+import argparse
+import glob
+import json
+import subprocess
+import sys
+import tempfile
+
+import timelinetools.packaging.repository
+
+
+def main():
+    build_winows_exe(parse_arguments())
+
+
+def parse_arguments():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--revision", default="tip")
+    parser.add_argument("--output-list")
+    return parser.parse_args()
+
+
+def build_winows_exe(arguments):
+    with tempfile.TemporaryDirectory(dir=".") as tempdir:
+        build_winows_exe_tmp(arguments, tempdir)
+
+
+def build_winows_exe_tmp(arguments, tempdir):
+    zips = glob.glob("timeline-*.zip")
+    if len(zips) != 1:
+        sys.exit("ERROR: Could not find source zip.")
+    archive = timelinetools.packaging.zipfile.ZipFile(".", zips[0]).extract_to(tempdir)
+    archive.change_constant(
+        "tools/winbuildtools/inno/timeline.iss",
+        "AppVerName",
+        f"Timeline {archive.get_version_number_string()}"
+    )
+    exe_name = f"{archive.get_filename_version()}-Win64Setup"
+    archive.change_constant(
+        "tools/winbuildtools/inno/timeline.iss",
+        "OutputBaseFilename",
+        exe_name
+    )
+    archive.change_constant(
+        "source/timelinelib/config/paths.py",
+        "_ROOT",
+        '"."'
+    )
+    archive.clean_pyc_files()
+    subprocess.check_call([
+        "wine",
+        f"{environ['PYTHON_DIR']}\\Scripts\\pyinstaller.exe",
+        "timeline.spec",
+    ], cwd=archive.get_path("tools/winbuildtools"))
+    subprocess.check_call([
+        "wineserver",
+        "-w",
+    ])
+    subprocess.check_call([
+        "cp",
+        "-r",
+        archive.get_path("translations"),
+        archive.get_path("icons"),
+        archive.get_path("tools/winbuildtools/dist"),
+    ])
+    subprocess.check_call([
+        "wine",
+        f"{environ['INNO_DIR']}\\ISCC.exe",
+        "inno/timeline.iss",
+    ], cwd=archive.get_path("tools/winbuildtools"))
+    subprocess.check_call([
+        "wineserver",
+        "-w",
+    ])
+    subprocess.check_call([
+        "mv",
+        archive.get_path(f"tools/winbuildtools/inno/out/{exe_name}.exe"),
+        f"{exe_name}.exe",
+    ])
+    write_output_list(
+        arguments=arguments,
+        exe_file=join(".", f"{exe_name}.exe"),
+        version=archive.get_version_number_string(),
+    )
+
+
+def write_output_list(arguments, exe_file, version):
+    if arguments.output_list:
+        with open(arguments.output_list, "w") as f:
+            f.write(json.dumps({
+                "artifacts": [
+                    {
+                        "source": exe_file,
+                        "destination": join(version, basename(exe_file)),
+                    }
+                ],
+            }))
+
+
+if __name__ == '__main__':
+    main()
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 tools/build_win32_installer.py
--- a/tools/build_win32_installer.py Sun Jul 06 09:06:44 2025 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,285 +0,0 @@
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
-#
-# This file is part of Timeline.
-#
-# Timeline 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.
-#
-# Timeline 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 Timeline.  If not, see <http://www.gnu.org/licenses/>.
-
-
-"""
-Working directory = BUILD_DIR
-COPYDIR     Copies from TIMELINE_DIR to BUILD_DIR
-"""
-
-import argparse
-import sys
-import os
-import shutil
-import tempfile
-import subprocess
-import timelinetools.packaging.repository
-
-
-TIMELINE_DIR = os.path.abspath("..\\..\\")
-BUILD_DIR = os.path.abspath(".\\target")
-ARCHIVE = "archive"
-ARTIFACT = "artifact"
-
-COPYFILE = 0
-COPYDIR = 1
-PUSHD = 3
-POPD = 4
-RUNCMD = 5
-RUNPYSCRIPT = 6
-ANNOTATE = 8
-RUNPYTEST = 9
-
-ACTION_NAMES = {COPYFILE: "COPYFILE",
-                COPYDIR: "COPYDIR",
-                PUSHD: "PUSHD",
-                POPD: "POPD",
-                RUNCMD: "RUNCMD",
-                RUNPYSCRIPT: "RUNPYSCRIPT",
-                RUNPYTEST: "RUNPYTEST"
-                }
-
-
-known_targets = ("win32Installer")
-
-
-win32InstallerActions = (
-    (ANNOTATE, "Run Tests", ""),
-    (RUNPYTEST, ["tools", "execute-specs.py"], ""),
-
-    (ANNOTATE, "Modify source files", ""),
-    (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_timeline_py.py"], ""),
-    (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_paths_py.py"], ""),
-    (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_version_py.py"], ""),
-    (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_factory_py.py"], ""),
-    (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_timeline_iss_win32.py"], ""),
-
-    (ANNOTATE, "Create a target directory for the build", ""),
-    (COPYDIR, ["source", "timelinelib"], ["builddir", "timelinelib"]),
-    (COPYDIR, ["tools", "winbuildtools", "inno"], ["builddir", "inno"]),
-    (COPYFILE, ["source", "timeline.py"], ["builddir", "timeline.py"]),
-    (COPYFILE, ["tools", "winbuildtools", "setup.py"], ["builddir", "setup.py"]),
-    (COPYFILE, ["COPYING"], ["builddir", "COPYING"]),
-    (COPYFILE, ["tools", "winbuildtools", "inno", "WINSTALL"], ["builddir", "WINSTALL"]),
-
-    (ANNOTATE, "Create distribution directory", ""),
-    (COPYDIR, ["icons"], ["builddir", "icons"]),
-    (RUNPYSCRIPT, ["builddir", "setup.py"], "py2exe"),
-    (COPYDIR, ["translations"], ["builddir", "dist", "translations"]),
-    (COPYDIR, ["icons"], ["builddir", "dist", "icons"]),
-    (COPYDIR, ["tools"], ["builddir", "dist", "tools"]),
-
-    (ANNOTATE, "Create installer executable", ""),
-    (RUNCMD, "python", ["builddir", "dist", "tools", "generate-mo-files.py"]),
-
-    (ANNOTATE, "Create Setup executable", ""),
-    (RUNCMD, "iscc.exe", ["builddir", "inno", "timelineWin32.iss"]),
-
-    (ANNOTATE, "Deliver executable artifact", ""),
-    (COPYFILE, [ARTIFACT], [ARTIFACT]),
-
-    (ANNOTATE, "Done", ""),
-)
-
-
-actions = {"win32Installer": win32InstallerActions}
-
-
-class Target():
-
-    def __init__(self, target):
-        print("-------------------------------------------------------")
-        print("  %s" % ("Building target %s" % target))
-        print("-------------------------------------------------------")
-        self.target = target
-        self.actions = actions[target]
-        self.ACTION_METHODS = {COPYFILE: self.copyfile,
-                               COPYDIR: self.copydir,
-                               PUSHD: self.pushd,
-                               POPD: self.popd,
-                               RUNCMD: self.runcmd,
-                               RUNPYSCRIPT: self.runpyscript,
-                               RUNPYTEST: self.runpytest,
-                               ANNOTATE: self.annotate}
-
-    def build(self, arguments, artifact_dir):
-        temp_dir = tempfile.mkdtemp()
-        try:
-            self.assert_that_target_is_known()
-            self.setup_and_create_directories(arguments, artifact_dir, temp_dir)
-            self.execute_actions()
-        finally:
-            #shutil.rmtree(temp_dir)
-            pass
-
-    def assert_that_target_is_known(self):
-        if self.target not in known_targets:
-            print("The target %s is unknown" % self.target)
-            print("BUILD FAILED")
-            sys.exit(1)
-
-    def setup_and_create_directories(self, arguments, artifact_dir, temp_dir):
-        self.artifact_dir = artifact_dir
-        self.project_dir = self.create_project_directory(arguments, temp_dir)
-        print("Artifact dir: %s" % self.artifact_dir)
-        print("Project dir:  %s" % self.project_dir)
-        print("Working dir:  %s" % os.getcwd())
-
-    def create_project_directory(self, arguments, temp_dir):
-        print("Create project directory")
-        repository = timelinetools.packaging.repository.Repository()
-        self.archive = repository.archive(arguments.revision, temp_dir, ARCHIVE)
-        return os.path.join(temp_dir, ARCHIVE)
-
-    def execute_actions(self):
-        count = 0
-        total = len([actions for action in self.actions if action[0] is not ANNOTATE])
-        try:
-            for action, src, dst in self.actions:
-                if action is not ANNOTATE:
-                    count += 1
-                    print("Action %2d(%2d): %s" % (count, total, ACTION_NAMES[action]))
-                self.ACTION_METHODS[action](src, dst)
-            print("BUILD DONE")
-        except Exception as ex:
-            print(str(ex))
-            print("BUILD FAILED")
-            sys.exit(1)
-
-    def annotate(self, src, dst):
-        self.print_header(src)
-
-    def copyfile(self, src, dst):
-        if src[0] == ARTIFACT:
-            f = os.path.join(self.project_dir, self.get_artifact_src_name())
-            t = os.path.join(self.artifact_dir, self.get_artifact_target_name())
-        else:
-            f = os.path.join(self.project_dir, *src)
-            t = os.path.join(self.project_dir, *dst)
-        self.print_src_dst(f, t)
-        shutil.copyfile(f, t)
-
-    def copydir(self, src, dst):
-        f = os.path.join(self.project_dir, *src)
-        t = os.path.join(self.project_dir, *dst)
-        self.print_src_dst(f, t)
-        shutil.copytree(f, t)
-
-    def runpyscript(self, src, arg):
-        try:
-            script_path = os.path.join(self.project_dir, *src)
-            self.print_src_dst(script_path, arg)
-            if src[-1] == "setup.py":
-                self.pushd(os.path.join(self.project_dir, "builddir"), None)
-                success, msg = self.run_pyscript(script_path, [arg])
-                self.popd(None, None)
-            else:
-                success, msg = self.run_pyscript(script_path, [self.project_dir, arg])
-            if not success:
-                raise Exception(msg)
-        except Exception as ex:
-            pass
-
-    def runpytest(self, src, dst):
-        script_path = os.path.join(self.project_dir, *src)
-        self.pushd(os.path.dirname(script_path), None)
-        self.print_src_dst(src, os.path.abspath(dst))
-        success, msg = self.run_pyscript(script_path, [], display_stderr=True)
-        if not success:
-            print('Msg:', msg)
-            if msg:
-                raise Exception(msg)
-        self.popd(None, None)
-
-    def runcmd(self, src, dst):
-        t = os.path.join(self.project_dir, *dst)
-        self.pushd(os.path.dirname(t), None)
-        self.print_src_dst(src, t)
-        success, msg = self.run_command([src, t])
-        self.popd(None, None)
-        if not success:
-            raise Exception(msg)
-
-    def pushd(self, src, dst):
-        self.print_src_dst(os.getcwd(), os.path.abspath(src))
-        self.cwd = os.getcwd()
-        os.chdir(src)
-
-    def popd(self, src, dst):
-        self.print_src_dst(None, self.cwd)
-        print("    dst: %s" % self.cwd)
-        os.chdir(self.cwd)
-
-    def run_pyscript(self, script, args=[], display_stderr=False):
-        return self.run_command(["python", script] + args, display_stderr)
-
-    def run_command(self, cmd, display_stderr=False):
-        if display_stderr:
-            rc = subprocess.call(cmd)
-            return rc == 0, ""
-        else:
-            p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-            out = p.communicate()
-            print(out)
-            if p.returncode == 0:
-                return True, out[0]
-            else:
-                return False, out[1]
-
-    def print_header(self, message):
-        print("-------------------------------------------------------")
-        print("  %s" % message)
-        print("-------------------------------------------------------")
-
-    def print_src_dst(self, src, dst):
-        if src is not None:
-            print("    src: %s" % src)
-        if dst is not None:
-            print("    dst: %s" % dst)
-
-    def get_artifact_src_name(self):
-        versionfile = os.path.join(self.project_dir, "source", "timelinelib", "meta", "version.py")
-        f = open(versionfile, "r")
-        text = f.read()
-        lines = text.split("\n")
-        for line in lines:
-            if line[0:7] == "VERSION":
-                break
-        f.close()
-        # VERSION = (0, 14, 0)
-        line = line.split("(", 1)[1]
-        line = line.split(")", 1)[0]
-        major, minor, bug = line. split(", ")
-        return "SetupTimeline%s%s%sPy2ExeWin32.exe" % (major, minor, bug)
-
-    def get_artifact_target_name(self):
-        return "%s-Win32Setup.exe" % self.archive.get_filename_version()
-
-
-def main():
-    artifactdir = os.path.join(sys.path[0], "..")
-    Target("win32Installer").build(parse_arguments(), artifactdir)
-
-
-def parse_arguments():
-    parser = argparse.ArgumentParser()
-    parser.add_argument("--revision", default="tip")
-    return parser.parse_args()
-
-
-if __name__ == "__main__":
-    main()
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 tools/build_win32_installer_py27.py
--- a/tools/build_win32_installer_py27.py Sun Jul 06 09:06:44 2025 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,286 +0,0 @@
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
-#
-# This file is part of Timeline.
-#
-# Timeline 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.
-#
-# Timeline 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 Timeline.  If not, see <http://www.gnu.org/licenses/>.
-
-
-"""
-Working directory = BUILD_DIR
-COPYDIR     Copies from TIMELINE_DIR to BUILD_DIR
-"""
-
-import argparse
-import sys
-import os
-import shutil
-import tempfile
-import subprocess
-import timelinetools.packaging.repository
-
-
-TIMELINE_DIR = os.path.abspath("..\\..\\")
-BUILD_DIR = os.path.abspath(".\\target")
-ARCHIVE = "archive"
-ARTIFACT = "artifact"
-
-COPYFILE = 0
-COPYDIR = 1
-PUSHD = 3
-POPD = 4
-RUNCMD = 5
-RUNPYSCRIPT = 6
-ANNOTATE = 8
-RUNPYTEST = 9
-
-ACTION_NAMES = {COPYFILE: "COPYFILE",
-                COPYDIR: "COPYDIR",
-                PUSHD: "PUSHD",
-                POPD: "POPD",
-                RUNCMD: "RUNCMD",
-                RUNPYSCRIPT: "RUNPYSCRIPT",
-                RUNPYTEST: "RUNPYTEST"
-                }
-
-
-known_targets = ("win32Installer")
-
-
-win32InstallerActions = (
-                 (ANNOTATE, "Run Tests", ""),
-                 (RUNPYTEST, ["tools", "execute-specs.py"], ""),
-
-                 (ANNOTATE, "Modify source files", ""),
-                 (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_timeline_py.py"], ""),
-                 (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_paths_py.py"], ""),
-                 (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_version_py.py"], ""),
-                 (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_factory_py.py"], ""),
-                 (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_timeline_iss_win32_py27.py"], ""),
-
-                 (ANNOTATE, "Create a target directory for the build", ""),
-                 (COPYDIR, ["source", "timelinelib"], ["builddir", "timelinelib"]),
-                 (COPYDIR, ["dependencies", "timelinelib", "icalendar-3.2\icalendar"], ["builddir", "icalendar"]),
-                 (COPYDIR, ["dependencies", "timelinelib", "pytz-2012j\pytz"], ["builddir", "pytz"]),
-                 (COPYDIR, ["dependencies", "timelinelib", "markdown-2.0.3", "markdown"], ["builddir", "markdown"]),
-                 (COPYDIR, ["tools", "winbuildtools", "inno"], ["builddir", "inno"]),
-                 (COPYFILE, ["source", "timeline.py"], ["builddir", "timeline.py"]),
-                 (COPYFILE, ["tools", "winbuildtools", "setup.py"], ["builddir", "setup.py"]),
-                 (COPYFILE, ["COPYING"], ["builddir", "COPYING"]),
-                 (COPYFILE, ["tools", "winbuildtools", "inno", "WINSTALL"], ["builddir", "WINSTALL"]),
-
-                 (ANNOTATE, "Create distribution directory", ""),
-                 (COPYDIR, ["icons"], ["builddir", "icons"]),
-                 (RUNPYSCRIPT, ["builddir", "setup.py"], "py2exe"),
-                 (COPYDIR, ["translations"], ["builddir", "dist", "translations"]),
-                 (COPYDIR, ["icons"], ["builddir", "dist", "icons"]),
-                 (COPYDIR, ["tools"], ["builddir", "dist", "tools"]),
-
-                 (ANNOTATE, "Create installer executable", ""),
-                 (RUNCMD, "python", ["builddir", "dist", "tools", "generate-mo-files.py"]),
-
-                 (ANNOTATE, "Create Setup executable", ""),
-                 (RUNCMD, "iscc.exe", ["builddir", "inno", "timelineWin32Py27.iss"]),
-
-                 (ANNOTATE, "Deliver executable artifact", ""),
-                 (COPYFILE, [ARTIFACT], [ARTIFACT]),
-
-                 (ANNOTATE, "Done", ""),
-                 )
-
-
-actions = {"win32Installer": win32InstallerActions}
-
-
-class Target():
-
-    def __init__(self, target):
-        print("-------------------------------------------------------")
-        print("  %s" % ("Building target %s" % target))
-        print("-------------------------------------------------------")
-        self.target = target
-        self.actions = actions[target]
-        self.ACTION_METHODS = {COPYFILE: self.copyfile,
-                               COPYDIR: self.copydir,
-                               PUSHD: self.pushd,
-                               POPD: self.popd,
-                               RUNCMD: self.runcmd,
-                               RUNPYSCRIPT: self.runpyscript,
-                               RUNPYTEST: self.runpytest,
-                               ANNOTATE: self.annotate}
-
-    def build(self, arguments, artifact_dir):
-        temp_dir = tempfile.mkdtemp()
-        try:
-            self.assert_that_target_is_known()
-            self.setup_and_create_directories(arguments, artifact_dir, temp_dir)
-            self.execute_actions()
-        finally:
-            # shutil.rmtree(temp_dir)
-            pass
-
-    def assert_that_target_is_known(self):
-        if self.target not in known_targets:
-            print("The target %s is unknown" % self.target)
-            print("BUILD FAILED")
-            sys.exit(1)
-
-    def setup_and_create_directories(self, arguments, artifact_dir, temp_dir):
-        self.artifact_dir = artifact_dir
-        self.project_dir = self.create_project_directory(arguments, temp_dir)
-        print("Artifact dir: %s" % self.artifact_dir)
-        print("Project dir:  %s" % self.project_dir)
-        print("Working dir:  %s" % os.getcwd())
-
-    def create_project_directory(self, arguments, temp_dir):
-        print("Create project directory")
-        repository = timelinetools.packaging.repository.Repository()
-        self.archive = repository.archive(arguments.revision, temp_dir, ARCHIVE)
-        return os.path.join(temp_dir, ARCHIVE)
-
-    def execute_actions(self):
-        count = 0
-        total = len([actions for action in self.actions if action[0] is not ANNOTATE])
-        try:
-            for action, src, dst in self.actions:
-                if action is not ANNOTATE:
-                    count += 1
-                    print("Action %2d(%2d): %s" % (count, total, ACTION_NAMES[action]))
-                self.ACTION_METHODS[action](src, dst)
-            print("BUILD DONE")
-        except Exception as ex:
-            print(str(ex))
-            print("BUILD FAILED")
-            sys.exit(1)
-
-    def annotate(self, src, dst):
-        self.print_header(src)
-
-    def copyfile(self, src, dst):
-        if src[0] == ARTIFACT:
-            f = os.path.join(self.project_dir, self.get_artifact_src_name())
-            t = os.path.join(self.artifact_dir, self.get_artifact_target_name())
-        else:
-            f = os.path.join(self.project_dir, *src)
-            t = os.path.join(self.project_dir, *dst)
-        self.print_src_dst(f, t)
-        shutil.copyfile(f, t)
-
-    def copydir(self, src, dst):
-        f = os.path.join(self.project_dir, *src)
-        t = os.path.join(self.project_dir, *dst)
-        self.print_src_dst(f, t)
-        shutil.copytree(f, t)
-
-    def runpyscript(self, src, arg):
-        try:
-            script_path = os.path.join(self.project_dir, *src)
-            self.print_src_dst(script_path, arg)
-            if src[-1] == "setup.py":
-                self.pushd(os.path.join(self.project_dir, "builddir"), None)
-                success, msg = self.run_pyscript(script_path, [arg])
-                self.popd(None, None)
-            else:
-                success, msg = self.run_pyscript(script_path, [self.project_dir, arg])
-            if not success:
-                raise Exception(msg)
-        except Exception:
-            pass
-
-    def runpytest(self, src, dst):
-        script_path = os.path.join(self.project_dir, *src)
-        self.pushd(os.path.dirname(script_path), None)
-        self.print_src_dst(src, os.path.abspath(dst))
-        success, msg = self.run_pyscript(script_path, [], display_stderr=True)
-        if not success:
-            raise Exception(msg)
-        self.popd(None, None)
-
-    def runcmd(self, src, dst):
-        t = os.path.join(self.project_dir, *dst)
-        self.pushd(os.path.dirname(t), None)
-        self.print_src_dst(src, t)
-        success, msg = self.run_command([src, t])
-        self.popd(None, None)
-        if not success:
-            raise Exception(msg)
-
-    def pushd(self, src, dst):
-        self.print_src_dst(os.getcwd(), os.path.abspath(src))
-        self.cwd = os.getcwd()
-        os.chdir(src)
-
-    def popd(self, src, dst):
-        self.print_src_dst(None, self.cwd)
-        print("    dst: %s" % self.cwd)
-        os.chdir(self.cwd)
-
-    def run_pyscript(self, script, args=[], display_stderr=False):
-        return self.run_command(["python", script] + args, display_stderr)
-
-    def run_command(self, cmd, display_stderr=False):
-        print('cmd:', cmd)
-        if display_stderr:
-            rc = subprocess.call(cmd)
-            return rc == 0, ""
-        else:
-            p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-            out = p.communicate()
-            print(out)
-            if p.returncode == 0:
-                return True, out[0]
-            else:
-                return False, out[1]
-
-    def print_header(self, message):
-        print("-------------------------------------------------------")
-        print("  %s" % message)
-        print("-------------------------------------------------------")
-
-    def print_src_dst(self, src, dst):
-        if src is not None:
-            print("    src: %s" % src)
-        if dst is not None:
-            print("    dst: %s" % dst)
-
-    def get_artifact_src_name(self):
-        versionfile = os.path.join(self.project_dir, "source", "timelinelib", "meta", "version.py")
-        f = open(versionfile, "r")
-        text = f.read()
-        lines = text.split("\n")
-        for line in lines:
-            if line[0:7] == "VERSION":
-                break
-        f.close()
-        # VERSION = (0, 14, 0)
-        line = line.split("(", 1)[1]
-        line = line.split(")", 1)[0]
-        major, minor, bug = line. split(", ")
-        return "SetupTimeline%s%s%sPy2ExeWin32.exe" % (major, minor, bug)
-
-    def get_artifact_target_name(self):
-        return "%s-Win32Setup.exe" % self.archive.get_filename_version()
-
-def main():
-    artifactdir = os.path.join(sys.path[0], "..")
-    Target("win32Installer").build(parse_arguments(), artifactdir)
-
-
-def parse_arguments():
-    parser = argparse.ArgumentParser()
-    parser.add_argument("--revision", default="tip")
-    return parser.parse_args()
-
-
-if __name__ == "__main__":
-    main()
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 tools/timelinetools/packaging/archive.py
--- a/tools/timelinetools/packaging/archive.py Sun Jul 06 09:06:44 2025 +0200
+++ b/tools/timelinetools/packaging/archive.py Sun Jul 06 09:10:31 2025 +0200
@@ -58,7 +58,7 @@
         self._run_tool("execute-specs.py")
 
     def create_zip_archive(self):
-        self._clean_pyc_files()
+        self.clean_pyc_files()
         zip_name = "%s.zip" % self.get_basename()
         subprocess.check_call([
             "zip",
@@ -69,6 +69,12 @@
         ], cwd=self.get_dirname())
         return timelinetools.packaging.zipfile.ZipFile(self.get_dirname(), zip_name)
 
+    def clean_pyc_files(self):
+        for root, dirs, files in os.walk(self.get_path()):
+            for f in files:
+                if f.endswith(".pyc"):
+                    os.remove(os.path.join(root, f))
+
     def _run_tool(self, tool):
         run_python_script_and_exit_if_fails(
             os.path.join(
@@ -80,13 +86,9 @@
 
     def _change_version_constant(self, constant, value):
         _change_constant(self._get_version_path(), constant, value)
-        self._clean_pyc_files()
 
-    def _clean_pyc_files(self):
-        for root, dirs, files in os.walk(self.get_path()):
-            for f in files:
-                if f.endswith(".pyc"):
-                    os.remove(os.path.join(root, f))
+    def change_constant(self, path, constant, value):
+        _change_constant(self.get_path(path), constant, value)
 
     def _get_readme_path(self):
         return os.path.join(self.get_path(), "README")
@@ -101,8 +103,8 @@
 def _change_constant(path, constant, value):
     _make_one_sub(
         path,
-        r"^%s\s*=\s*.*?$" % constant,
-        "%s = %s" % (constant, value)
+        r"^%s(\s*)=(\s*).*?$" % constant,
+        r"%s\1=\2%s" % (constant, value)
     )
 
 
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 tools/timelinetools/packaging/path.py
--- a/tools/timelinetools/packaging/path.py Sun Jul 06 09:06:44 2025 +0200
+++ b/tools/timelinetools/packaging/path.py Sun Jul 06 09:10:31 2025 +0200
@@ -32,8 +32,8 @@
     def get_basename(self):
         return self._basename
 
-    def get_path(self):
-        return os.path.join(self._dirname, self._basename)
+    def get_path(self, *args):
+        return os.path.join(self._dirname, self._basename, *args)
 
     def rename(self, new_basename):
         os.rename(self.get_path(), os.path.join(self._dirname, new_basename))
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 tools/winbuildtools/build_win_setup_executable.py
--- a/tools/winbuildtools/build_win_setup_executable.py Sun Jul 06 09:06:44 2025 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,374 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
-#
-# This file is part of Timeline.
-#
-# Timeline 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.
-#
-# Timeline 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 Timeline.  If not, see <http://www.gnu.org/licenses/>.
-
-
-"""
-This script is used to create a Timeline installation file for Windows
-
-Usage:
-    Copy this file to a temporary directory and issue the command
-        python build_win_setup_executable.py [version]
-
-    If a version argument is given, it must be the name of a tagged version
-    such as 2.10.0.
-    If not given, the latest code is used.
-
-    The path to the python version used is defined by the pragram constant
-    PYTHON_HOME. If default isn't what you want you have to change it.
-
-Prerequisite:
-    1. Must run on a Windows machine
-    2. Mercurial (hg) must be installed
-    3. Python must be installed
-    4. Inno Setup (iscc.exe) must be installed
-    5. Dependent on files in Timeline repo:
-        tools/execute-specs.py
-        tools/generate-mo-files.py
-        tools/winbuildtools/mod_paths.py
-        tools/winbuildtools/mod_iss_timeline_version.py
-        tools/winbuildtools/timeline.spec
-        tools/winbuildtools/dist/icons/*
-        tools/winbuildtools/dist/translations/*
-
-
-"""
-
-import os
-import subprocess
-from cmdlineparser import CmdLineParser, PositionalArg, NamedArg
-
-
-class BuildError(Exception):
-    pass
-
-
-class Build:
-
-    def __init__(self, python_home, build_version=None, keep_workspace=False):
-        self.PYTHON = os.path.join(python_home, "python.exe")
-        self.PIP = os.path.join(python_home, "pip.exe")
-        self.PYINSTALLER = None
-        self._build_version = build_version
-        self._root_dir = os.path.dirname(os.path.realpath(__file__))
-        self._keep_workspace = keep_workspace
-        self._log_header("Input data")
-        self._log(f"Python home   : {python_home}")
-        self._log(f"Build version : {self._build_version}")
-        self._log(f"Keep workspace: {self._keep_workspace}")
-
-    def _validate_prerequisite(self):
-        def os_is_windows():
-            if os.name == "nt":
-                self._log("OS is windows")
-            else:
-                raise BuildError("This script only runs on Windows!")
-
-        def mercurial_is_installed():
-            if self._os_command("where", "hg").endswith("hg.exe"):
-                self._log("Mercurial is installed")
-            else:
-                raise BuildError("Mercurial is not installed!")
-
-        def python_installed():
-            try:
-                version = self._os_command(self.PYTHON, "-V")
-                self._log(f"Python {version} is installed")
-            except:
-                raise BuildError("Python is not installed!")
-
-        def iscc_is_installed():
-            if self._os_command("where", "iscc").lower().endswith("iscc.exe"):
-                self._log("ISCC is installed")
-            else:
-                raise BuildError("ISCC is not installed!")
-
-        self._log_header("Validating Prerequisite")
-        os_is_windows()
-        mercurial_is_installed()
-        python_installed()
-        iscc_is_installed()
-
-    def _create_workspace(self):
-        self._create_dir("workspace")
-        self._cd("workspace")
-
-    def _os_command(self, *args):
-        return subprocess.check_output(list(args)).decode("latin-1").strip()
-
-    def _create_dir(self, dirname):
-        os.mkdir(dirname)
-
-    def _cd(self, dirname):
-        if dirname == "root":
-            path = self._root_dir
-        else:
-            path = os.path.join(self._root_dir, dirname)
-        os.chdir(path)
-        self._log(f"Directory changed to: {os.getcwd()}")
-
-    def _remove_dir(self, dirname):
-        os.rmdir(dirname)
-
-    def _get_timeline_from_repo(self):
-        """http://hg.code.sf.net/p/thetimelineproj/main"""
-        self._log_header("Clone Timeline repo")
-        os.system("hg clone http://hg.code.sf.net/p/thetimelineproj/main .")
-        self._log_header("Get hg info")
-        self._hg_node = self._get_hg_node()
-        self._build_version_exists()
-        self._hg_rev = self._get_hg_rev()
-        self._log(f"Build version: {self._build_version}")
-        self._log(f"Node         : {self._hg_node}")
-        self._log(f"Revision     : {self._hg_rev}")
-
-    def _build_version_exists(self):
-        if self._build_version is not None:
-            try:
-                self._os_command(
-                    "hg", "log", "--rev", self._build_version, "--template", "exists"
-                )
-                self._log("Build version exists")
-                self._os_command("hg", "update", "--rev", self._build_version)
-                self._log("Project updated to build version")
-            except:
-                raise BuildError(f"unknown revision: {self._build_version}!")
-
-    def _get_hg_node(self):
-        return self._os_command("hg", "log", "--rev", ".", "--template", "{node}")
-
-    def _get_hg_rev(self):
-        return self._os_command("hg", "log", "--rev", ".", "--template", "{rev}")
-
-    def _pip_install(self, package, version=None):
-        self._log(f'Install {package} {version if version is not None else ""}')
-        if version is not None:
-            self._os_command(self.PIP, "install", "-v", f"{package}=={version}")
-        else:
-            self._os_command(self.PIP, "install", package)
-
-    def _create_virtual_environment(self):
-        def create_venv():
-            self._os_command(self.PYTHON, "-m", "venv", "venv")
-            self.PYTHON = os.path.join(
-                self._root_dir, "workspace", "venv", "Scripts", "python.exe"
-            )
-            self.PIP = os.path.join(
-                self._root_dir, "workspace", "venv", "Scripts", "pip.exe"
-            )
-
-        def upgrade_pip():
-            self._log("Upgrade pip")
-            self._os_command(self.PYTHON, "-m", "pip", "install", "--upgrade", "pip")
-
-        def install_packages():
-            packages = (
-                ("humblewx", None),
-                ("icalendar", None),
-                ("Markdown", None),
-                ("pyinstaller", None),
-                ("wxPython", "4.1.1"),
-            )
-            for package, version in packages:
-                self._pip_install(package, version)
-            self.PYINSTALLER = os.path.join(
-                self._root_dir, "workspace", "venv", "Scripts", "pyinstaller.exe"
-            )
-            self._log(self._os_command(self.PIP, "list"))
-
-        self._log_header("Create virtual environment")
-        create_venv()
-        upgrade_pip()
-        install_packages()
-
-    def _run_tests(self):
-        self._log_header("Run Tests")
-        self._os_command(
-            self.PYTHON, "tools/execute-specs.py", "--write-testlist", "testlist.txt"
-        )
-
-    def _generate_mo_file(self):
-        self._log_header("Generate mo files")
-        try:
-            self._cd(os.path.join(self._root_dir, "workspace", "tools"))
-            self._os_command(self.PYTHON, "generate-mo-files.py")
-        finally:
-            self._cd(os.path.join(self._root_dir, "workspace"))
-
-    def _modify_source_files(self):
-        self._log_header("Modify source files")
-        self._cd(os.path.join(self._root_dir, "workspace", "tools", "winbuildtools"))
-        self._os_command(self.PYTHON, "mod_paths.py", ".")
-        self._log("paths.py modified")
-        self._log(f"Revision: {self._build_version or self._hg_node}")
-        self._os_command(
-            self.PYTHON,
-            "mod_iss_timeline_version.py",
-            ".",
-            self._build_version or self._hg_node,
-        )
-        self._log("version.py modified")
-
-    def _create_timeline_executable(self):
-        self._log_header("Create Timeline executable")
-        self._cd(os.path.join(self._root_dir, "workspace", "tools", "winbuildtools"))
-        self._os_command(self.PYINSTALLER, "timeline.spec")
-
-    def _copy_files_to_dist(self):
-        self._log("Copying icons and translations to dist")
-        self._cd(os.path.join(self._root_dir, "workspace", "tools", "winbuildtools"))
-        self._create_dir(".\\dist\\icons")
-        self._create_dir(".\\dist\\icons\\event_icons")
-        self._create_dir(".\\dist\\translations")
-
-        self._os_command("xcopy", "/S", "..\\..\\icons\*.*", ".\\dist\icons\\*.*")
-        self._log(f"Icons copied")
-
-        for lang in (
-            "ca",
-            "de",
-            "el",
-            "es",
-            "eu",
-            "fr",
-            "gl",
-            "he",
-            "it",
-            "ko",
-            "lt",
-            "nl",
-            "pl",
-            "pt",
-            "pt_BR",
-            "ru",
-            "sv",
-            "tr",
-            "vi",
-            "zh_CN",
-        ):
-            self._os_command(
-                "xcopy",
-                "/S",
-                f"..\\..\\translations\\{lang}\\*.*",
-                f".\\dist\\translations\\{lang}\\*.*",
-            )
-            self._log(f"{lang} translations copied")
-
-    def _create_distrubtable(self):
-        self._log_header("Create distributable")
-        self._copy_files_to_dist()
-        self._cd(
-            os.path.join(self._root_dir, "workspace", "tools", "winbuildtools", "inno")
-        )
-        self._os_command("iscc.exe", "timeline2Win32.iss")
-
-    def _move_result(self):
-        self._log_header("Move result")
-        self._cd(
-            os.path.join(
-                self._root_dir, "workspace", "tools", "winbuildtools", "inno", "out"
-            )
-        )
-        self._os_command("xcopy", "/Y", "*.exe", f"{self._root_dir}\\*.*")
-        self._log("Resulting exe-file moved to root dir.")
-
-    def _log_header(self, text):
-        print("-" * 50)
-        print(f" {text}")
-        print("-" * 50)
-
-    def _log(self, text):
-        print(f"  {text}")
-
-    def _abort_message(self, ex):
-        self._log_header("Build aborted:")
-        self._log(ex)
-
-    def _clean_up(self):
-        self._log_header("Clean up")
-        self._cd("root")
-        if os.path.isdir("workspace"):
-            os.system("rmdir /S /Q workspace")
-            self._log("workspace directory removed")
-        else:
-            self._log("No workspace found")
-        if os.path.isdir("__pycache__"):
-            os.system("rmdir /S /Q __pycache__")
-            self._log("__pycache__ directory removed")
-
-    def run(self):
-        try:
-            self._clean_up()
-            self._validate_prerequisite()
-            self._create_workspace()
-            self._get_timeline_from_repo()
-            self._create_virtual_environment()
-            self._run_tests()
-            self._generate_mo_file()
-            self._modify_source_files()
-            self._create_timeline_executable()
-            self._create_distrubtable()
-            self._move_result()
-        except BuildError as ex:
-            self._abort_message(ex)
-        finally:
-            if not self._keep_workspace:
-                self._clean_up()
-
-
-def print_help():
-    print(
-        """
-Usage:
-
-    python build_win_setup_executable.py [options]
-
-Options:
-
-    -h, --help                          Print help
-    -p dirname, --python-home dirname   Path to dir where python.exe can be found. Default = c:\pgm\python396
-    -t tagname, --tag tagname           Tagged version to build. Default = latest code
-    -k, --keep-workspace                Kepp the workspace directory when finished
-
-        """
-    )
-
-
-if __name__ == "__main__":
-    DEFAULT_PYTHON_HOME = "c:\\pgm\\Python396"
-    specs = [
-        NamedArg(name="t", long_name="tag", has_value=True),
-        NamedArg(name="k", long_name="keep-workspace"),
-        NamedArg(name="p", long_name="python-home", has_value=True),
-        NamedArg(name="h", long_name="help"),
-    ]
-    parser = CmdLineParser(specs)
-    if parser.help:
-        print_help()
-    else:
-        build_version = parser.tag_value
-        python_home = (
-            parser.python_home_value
-            if parser.python_home_value
-            else DEFAULT_PYTHON_HOME
-        )
-        Build(
-            python_home=python_home,
-            build_version=build_version,
-            keep_workspace=parser.keep_workspace,
-        ).run()
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 tools/winbuildtools/buildwinsetup.cmd
--- a/tools/winbuildtools/buildwinsetup.cmd Sun Jul 06 09:06:44 2025 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-@echo off
-
-echo --------------------------------------
-echo Revision: %1
-echo --------------------------------------
-
-
-echo --------------------------------------
-echo Remove old build and dist directories
-echo --------------------------------------
-rmdir /S /Q build >> nul
-rmdir /S /Q dist >> nul
-del /S /Q inno\out >> nul
-
-echo --------------------------------------
-echo Create translations
-echo --------------------------------------
-pushd ..
-python generate-mo-files.py
-popd
-
-echo --------------------------------------
-echo Modify paths.py
-echo --------------------------------------
-python mod_paths.py .
-
-echo --------------------------------------
-echo Modify version file and iss file
-echo --------------------------------------
-python mod_iss_timeline_version.py . %1
-
-echo --------------------------------------
-echo Building distribution
-echo --------------------------------------
-pyinstaller timeline.spec
-
-echo --------------------------------------
-echo Copying icons and translations to dist
-echo --------------------------------------
-mkdir .\dist\icons\event_icons
-mkdir .\dist\translations
-xcopy /S ..\..\icons\*.*  .\dist\icons\*.*
-xcopy /S ..\..\translations\ca\*.*  .\dist\translations\ca\*.*
-xcopy /S ..\..\translations\de\*.*  .\dist\translations\de\*.*
-xcopy /S ..\..\translations\el\*.*  .\dist\translations\el\*.*
-xcopy /S ..\..\translations\es\*.*  .\dist\translations\es\*.*
-xcopy /S ..\..\translations\eu\*.*  .\dist\translations\eu\*.*
-xcopy /S ..\..\translations\fr\*.*  .\dist\translations\fr\*.*
-xcopy /S ..\..\translations\gl\*.*  .\dist\translations\gl\*.*
-xcopy /S ..\..\translations\he\*.*  .\dist\translations\he\*.*
-xcopy /S ..\..\translations\it\*.*  .\dist\translations\it\*.*
-xcopy /S ..\..\translations\ko\*.*  .\dist\translations\ko\*.*
-xcopy /S ..\..\translations\lt\*.*  .\dist\translations\lt\*.*
-xcopy /S ..\..\translations\nl\*.*  .\dist\translations\nl\*.*
-xcopy /S ..\..\translations\pl\*.*  .\dist\translations\pl\*.*
-xcopy /S ..\..\translations\pt\*.*  .\dist\translations\pt\*.*
-xcopy /S ..\..\translations\pt_BR\*.*  .\dist\translations\pt_BR\*.*
-xcopy /S ..\..\translations\ru\*.*  .\dist\translations\ru\*.*
-xcopy /S ..\..\translations\sv\*.*  .\dist\translations\sv\*.*
-xcopy /S ..\..\translations\tr\*.*  .\dist\translations\tr\*.*
-xcopy /S ..\..\translations\vi\*.*  .\dist\translations\vi\*.*
-xcopy /S ..\..\translations\zh_CH\*.*  .\dist\translations\zh_CH\*.*
-
-
-echo --------------------------------------
-echo Create distributable
-echo --------------------------------------
-pushd inno
-iscc.exe timeline2Win32.iss
-popd
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 tools/winbuildtools/cmdlineparser.py
--- a/tools/winbuildtools/cmdlineparser.py Sun Jul 06 09:06:44 2025 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,282 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
-#
-# This file is part of Timeline.
-#
-# Timeline 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.
-#
-# Timeline 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 Timeline.  If not, see <http://www.gnu.org/licenses/>.
-
-
-"""
-The CmdLineParser reads the command line input and stores the separate
-parts as properties, according to the given specification.
-
-A command line has two types of arguments
-    - Positional argument
-    - Named argument
-
-Arguments are separated with on or more spaces.
-
-A named argument is always prefixed with a dash (-) which a named argument isn't.
-
-A named argument has two types. One with a single dash prefix and one with a double
-dash prefix. The first one is intended for single character names and the second
-one for long names. Both can have a specified value.
-
-If a name contains a dash character, the attribute name of the parser will be the same
-but with the dash replaced with an underscore character.
-
-The name 'invalid-args' is reserved for the parser and cannot be used as a command line
-argument.
-
-By default, the input argument list is taken from sys.argv[1:], but it can be
-specified as an argument to the CmdLineParser constructor.
-
-Example:
-    cmd line:  pgm.exe -v --log-name mylog filename
-
-    pgm.exe             The name of the executed program
-    -v                  A named argument without a value
-    --log-name mylog    A named argument with the value mylog
-    filename            A positional argument
-
-Usage:
-    # Define valid arguments
-    specs = [
-        PositionalArg(name="filename"),
-        NamedArg(name="v", long_name="verbose"),
-        NamedArg(long_name="log-name", has_value=True),
-    ]
-
-    # Create the parser and process the command line input
-    parser = CmdLineParser(specs)
-
-    # Use input data
-    if parser.verbose:
-        print("Verbose mode")
-    print(f"logfile : {parser.log_name}")
-    print(f"filename: {parser.filename}")
-
-"""
-
-import sys
-
-
-class Arg:
-    def __init__(self, name="", long_name="", has_value=False):
-        self._name = name
-        self._long_name = long_name
-        self._has_value = has_value
-
-    @property
-    def name(self):
-        return self._name
-
-    @property
-    def long_name(self):
-        return self._long_name
-
-    @property
-    def has_value(self):
-        return self._has_value
-
-
-class PositionalArg(Arg):
-    def __init__(self, name):
-        Arg.__init__(self, name)
-
-
-class NamedArg(Arg):
-    def __init__(self, name="", long_name="", has_value=None):
-        Arg.__init__(self, name, long_name, has_value)
-
-
-class CmdLineParser:
-    """
-    No  arguments given on the command line
-
-    >>> sys.argv = ["pgm.exe"]
-    >>> specs = [
-    ... PositionalArg(name="filename"),
-    ... NamedArg(name="v", long_name="verbose"),
-    ... NamedArg(name="l", long_name="log-name", has_value=True),]
-    >>> parser = CmdLineParser(specs)
-    >>> parser.filename
-    False
-    >>> parser.log_name
-    False
-    >>> parser.l
-    False
-    >>> parser.verbose
-    False
-    >>> parser.v
-    False
-    >>> parser.invalid_args
-    []
-
-    Command line arguments given
-
-    >>> sys.argv = ["pgm.exe", "-v", "--log-name", "mylog", "myfile"]
-    >>> parser = CmdLineParser(specs)
-    >>> parser.filename
-    'myfile'
-    >>> parser.v and parser.verbose
-    True
-    >>> parser.log_name_value
-    'mylog'
-    >>> parser.l_value
-    'mylog'
-
-    Invalid Command line arguments given
-
-    >>> sys.argv = ["pgm.exe", "-vv", "--log-name", "mylog", "myfile"]
-    >>> parser = CmdLineParser(specs)
-    >>> len(parser.invalid_args)
-    1
-    >>> parser.invalid_args[0]
-    '-vv'
-
-    Same arg given twice
-
-    >>> sys.argv = ["pgm.exe", "--log-name", "mylog2", "--log-name", "mylog2", "myfile1", "myfile2"]
-    >>> parser = CmdLineParser(specs)
-
-    If named args are duplicated the last one is used
-
-    >>> parser.log_name_value
-    'mylog2'
-
-    Positional arguments are always assigned from left to right.
-
-    >>> parser.filename
-    'myfile1'
-    >>> parser.invalid_args[0]
-    'myfile2'
-
-    The 'name' agument is not mandatory for NamedArg
-
-    >>> sys.argv = ["pgm.exe", "--verbose", "myfile1"]
-    >>> specs = [
-    ... PositionalArg(name="filename"),
-    ... NamedArg(long_name="verbose")]
-    >>> parser = CmdLineParser(specs)
-    >>> parser.verbose
-    True
-    """
-
-    def __init__(self, specs, args=None):
-        self._positional_specs = [s for s in specs if isinstance(s, PositionalArg)]
-        self._named_specs = [s for s in specs if isinstance(s, NamedArg)]
-        self._args = args or sys.argv[1:]
-        self._invalid_args = []
-        self._positional_args = []
-        self._init_args()
-        self._parse()
-
-    @property
-    def invalid_args(self):
-        return self._invalid_args
-
-    def _init_args(self):
-        for spec in self._positional_specs:
-            spec.attr_name = self._attr_name(spec.name)
-            setattr(self, spec.attr_name, False)
-        for spec in self._named_specs:
-            self._create_attribute(spec)
-            self._set_attribute(spec, False)
-            self._set_attribute_value(spec, None)
-
-    def _create_attribute(self, spec):
-        if len(spec.name) > 0:
-            spec.attr_name = self._attr_name(spec.name)
-        if len(spec.long_name) > 0:
-            spec.attr_long_name = self._attr_name(spec.long_name)
-
-    def _set_attribute(self, spec, value):
-        if len(spec.name) > 0:
-            setattr(self, spec.attr_name, value)
-        if len(spec.long_name) > 0:
-            setattr(self, spec.attr_long_name, value)
-
-    def _set_attribute_value(self, spec, value):
-        if len(spec.name) > 0:
-            setattr(self, f"{spec.attr_name}_value", value)
-        if len(spec.long_name) > 0:
-            setattr(self, f"{spec.attr_long_name}_value", value)
-
-    def _attr_name(self, name):
-        return name.replace("-", "_")
-
-    @property
-    def _next_arg(self):
-        return self._args[0]
-
-    @property
-    def _more_args_exists(self):
-        return len(self._args) > 0
-
-    @property
-    def _next_arg_is_named(self):
-        return self._next_arg.startswith("-")
-
-    @property
-    def _next_arg_is_positional(self):
-        return not self._next_arg_is_named
-
-    def _remove_first_arg(self):
-        self._args = self._args[1:]
-
-    def _parse(self):
-        while self._more_args_exists:
-            if self._next_arg_is_named:
-                self._parse_named_arg()
-            else:
-                self._parse_positional_arg()
-
-    def _parse_named_arg(self):
-        spec = self._find_spec()
-        if spec is None:
-            self._invalid_args.append(self._next_arg)
-            self._remove_first_arg()
-        else:
-            self._set_attribute(spec, True)
-            self._remove_first_arg()
-            if spec.has_value and self._more_args_exists:
-                if self._next_arg_is_positional:
-                    self._set_attribute_value(spec, self._next_arg)
-                    self._remove_first_arg()
-
-    def _find_spec(self):
-        for spec in self._named_specs:
-            if (
-                f"-{spec.name}" == self._next_arg
-                or f"--{spec.long_name}" == self._next_arg
-            ):
-                return spec
-
-    def _parse_positional_arg(self):
-        index = len(self._positional_args)
-        try:
-            spec = self._positional_specs[index]
-            setattr(self, spec.attr_name, self._next_arg)
-            self._positional_args.append(self._next_arg)
-        except IndexError:
-            self._invalid_args.append(self._next_arg)
-        self._remove_first_arg()
-
-
-if __name__ == "__main__":
-    import doctest
-
-    doctest.testmod()
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 tools/winbuildtools/inno/timeline.iss
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/winbuildtools/inno/timeline.iss Sun Jul 06 09:10:31 2025 +0200
@@ -0,0 +1,66 @@
+; Script generated by the Inno Setup Script Wizard.
+; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
+
+[Setup]
+AppName=Timeline
+;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+;
+; Before running this script ...
+; The two lines below must be uncommented and text changed to reflect
+; the version number of the executable to be built.
+;
+AppVerName=Timeline 2.1.0
+OutputBaseFilename=timeline-2.1.0-beta-4b501487562b-2019-11-26-Win32Setup
+;
+;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+AppPublisher=Rickard Lindberg <ricli85@gmail.com>
+AppPublisherURL=http://thetimelineproj.sourceforge.net/
+AppSupportURL=http://thetimelineproj.sourceforge.net/
+AppUpdatesURL=http://thetimelineproj.sourceforge.net/
+DefaultDirName={pf}\Timeline
+DefaultGroupName=Timeline
+SourceDir=.\
+LicenseFile=..\..\..\COPYING
+InfoBeforeFile=WINSTALL
+OutputDir=out
+Compression=lzma
+SolidCompression=yes     
+DisableDirPage=no
+
+[Languages]
+Name: "english"; MessagesFile: "compiler:Default.isl"
+
+
+[Tasks]
+Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
+Name: "startmenu";   Description: "Create a start menu"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
+
+[Files]
+Source: "..\dist\timeline.exe"; DestDir: "{app}"; Flags: ignoreversion  recursesubdirs
+Source: "..\dist\icons\*"; DestDir: "{app}\icons"; Flags: ignoreversion  recursesubdirs
+Source: "..\dist\translations\*"; DestDir: "{app}\translations"; Flags: ignoreversion  recursesubdirs
+
+;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+;
+; Before running this script ...
+; You must check to see if there are any more po-files to add
+;
+;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+
+[Icons]
+Name: "{commondesktop}\Timeline"; Filename:"{app}\timeline.exe"; IconFilename: "{app}\icons\Timeline.ico"; Tasks: desktopicon
+Name: "{group}\Timeline";         Filename:"{app}\timeline.exe"; IconFilename: "{app}\icons\Timeline.ico"; WorkingDir: "{app}"; Tasks: startmenu
+
+
+
+
+[Run]
+Filename: "{app}\timeline.exe"; Description: "{cm:LaunchProgram,Timeline}"; Flags: shellexec postinstall skipifsilent;
+
+
+[UninstallDelete]
+Type: files; Name: "{win}\uninstall\MYPROG.INI"
+
+
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 tools/winbuildtools/inno/timeline2Win32.iss
--- a/tools/winbuildtools/inno/timeline2Win32.iss Sun Jul 06 09:06:44 2025 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-; Script generated by the Inno Setup Script Wizard.
-; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
-
-[Setup]
-AppName=Timeline
-;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-;
-; Before running this script ...
-; The two lines below must be uncommented and text changed to reflect
-; the version number of the executable to be built.
-;
-AppVerName=Timeline 2.1.0
-OutputBaseFilename=timeline-2.1.0-beta-4b501487562b-2019-11-26-Win32Setup
-;
-;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-
-AppPublisher=Rickard Lindberg <ricli85@gmail.com>
-AppPublisherURL=http://thetimelineproj.sourceforge.net/
-AppSupportURL=http://thetimelineproj.sourceforge.net/
-AppUpdatesURL=http://thetimelineproj.sourceforge.net/
-DefaultDirName={pf}\Timeline
-DefaultGroupName=Timeline
-SourceDir=.\
-LicenseFile=..\..\..\COPYING
-InfoBeforeFile=WINSTALL
-OutputDir=out
-Compression=lzma
-SolidCompression=yes     
-DisableDirPage=no
-
-[Languages]
-Name: "english"; MessagesFile: "compiler:Default.isl"
-
-
-[Tasks]
-Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
-Name: "startmenu";   Description: "Create a start menu"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
-
-[Files]
-Source: "..\dist\timeline.exe"; DestDir: "{app}"; Flags: ignoreversion  recursesubdirs
-Source: "..\dist\icons\*"; DestDir: "{app}\icons"; Flags: ignoreversion  recursesubdirs
-Source: "..\dist\translations\*"; DestDir: "{app}\translations"; Flags: ignoreversion  recursesubdirs
-
-;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-;
-; Before running this script ...
-; You must check to see if there are any more po-files to add
-;
-;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-
-
-[Icons]
-Name: "{commondesktop}\Timeline"; Filename:"{app}\timeline.exe"; IconFilename: "{app}\icons\Timeline.ico"; Tasks: desktopicon
-Name: "{group}\Timeline";         Filename:"{app}\timeline.exe"; IconFilename: "{app}\icons\Timeline.ico"; WorkingDir: "{app}"; Tasks: startmenu
-
-
-
-
-[Run]
-Filename: "{app}\timeline.exe"; Description: "{cm:LaunchProgram,Timeline}"; Flags: shellexec postinstall skipifsilent;
-
-
-[UninstallDelete]
-Type: files; Name: "{win}\uninstall\MYPROG.INI"
-
-
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 tools/winbuildtools/jenkins-build-local.py
--- a/tools/winbuildtools/jenkins-build-local.py Sun Jul 06 09:06:44 2025 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,306 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
-#
-# This file is part of Timeline.
-#
-# Timeline 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.
-#
-# Timeline 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 Timeline.  If not, see <http://www.gnu.org/licenses/>.
-
-
-"""
-Our Jenkins server is about to be retired.
-
-That means that the Windows installation executable can no longer
-be built from Jenkins (calling a build slave).
-
-This script is intended to be run on the local Windows box that
-has the Jenkins slave installation to build the Windows installation
-executable file.
-
-"""
-
-import sys
-import os
-import subprocess
-
-
-class BuildError(Exception):
-    pass
-
-
-def get_build_version_from_input():
-    """
-    The first argument, if any, is the tagged version t obe built.
-    For beta releases, the hg rev number is used.
-
-    >>> get_build_version_from_input()
-
-    >>> sys.argv.insert(1, '2.11.0')
-    >>> get_build_version_from_input()
-    '2.11.0'
-
-    """
-    try:
-        return sys.argv[1]
-    except IndexError:
-        pass
-
-
-def is_windows():
-    return os.name == 'nt'
-
-
-def hg_command(*args):
-    """
-    >>> hg_command('showconfig', 'paths.default')
-    'http://hg.code.sf.net/p/thetimelineproj/main'
-
-    """
-    try:
-        if is_windows():
-            return subprocess.check_output(['hg'] + list(args)).decode('latin-1').strip()
-        else:
-            return subprocess.check_output(['hg'] + list(args)).decode().strip()
-    except Exception as ex:
-        raise BuildError(str(ex))
-
-
-def os_command(*args):
-    try:
-        if is_windows():
-            return subprocess.check_output(list(args)).decode('latin-1').strip()
-        else:
-            return subprocess.check_output(list(args)).decode().strip()
-    except Exception as ex:
-        raise BuildError(str(ex))
-
-
-def get_hg_default_path():
-    return hg_command('showconfig', 'paths.default')
-
-
-def hg_pull():
-    hg_command('pull', '--rev', 'default')
-
-
-def hg_update():
-    hg_command('update', '--clean', '--rev', 'default')
-
-
-def hg_purge():
-    hg_command('--config', 'extensions.purge=', 'clean', '--all')
-
-
-def get_hg_node():
-    return hg_command('log', '--rev', '.', '--template', '{node}')
-
-
-def get_hg_rev():
-    return hg_command('log', '--rev', '.', '--template', '{rev}')
-
-
-def verify_version(build_version, node):
-    """
-    >>> verify_version('2.10.0', 'a821cfd8cbe1f28ac4d01ffe02746632161db27b')
-    'exists'
-
-    >>> verify_version(None, 'a821cfd8cbe1f28ac4d01ffe02746632161db27b')
-    'exists'
-
-    >>> verify_version(None, 'abcd')
-    Traceback (most recent call last):
-        ...
-    jenkins-build-local.BuildError: unknown revision: abcd!
-    """
-    try:
-        return hg_command('log', '--rev', build_version or node, '--template', 'exists')
-    except:
-        raise BuildError(f'unknown revision: {build_version or node}!')
-
-
-def log_changesets(build_revision, node):
-    return hg_command(
-        'log',
-        '--template',
-        "<changeset node='{node}' author='{author|xmlescape}' rev='{rev}' date='{date}'><msg>{desc|xmlescape}</msg>{file_adds % '<addedFile>{file|xmlescape}</addedFile>'}{file_dels % '<deletedFile>{file|xmlescape}</deletedFile>'}{files % '<file>{file|xmlescape}</file>'}<parents>{parents}</parents></changeset>\n",
-        '--rev',
-        f"ancestors('default') and not ancestors({build_revision or node})",
-        '--encoding',
-        'UTF-8',
-        '--encodingmode',
-        'replace')
-
-
-def upgrade_pip():
-    os_command('python.exe', '-m', 'pip', 'install', '--upgrade', 'pip')
-
-
-def display_python_version():
-    log_subheader('Python Version')
-    log(os_command('python', '-V'))
-
-
-def create_virtual_environment():
-    log_subheader('Crate virtual environment')
-    log(os_command('python', '-m', 'venv', 'venv'))
-
-
-def upgrade_pip():
-    log_subheader('Upgrade pip')
-    os.system('venv\\Scripts\\python -m pip install --upgrade pip')
-
-
-def install_wxpython():
-    log_subheader('Install wwPython')
-    os_command('venv\\Scripts\\pip', 'install', '--force-reinstall', '-v', 'wxPython==4.1.1')
-
-
-def install_humblewx():
-    log_subheader('Install humblewx')
-    os_command('venv\Scripts\\pip', 'install', 'humblewx')
-
-
-def install_icalendar():
-    log_subheader('Install icalendar')
-    os_command('venv\\Scripts\\pip', 'install', 'icalendar')
-
-
-def install_markdown():
-    log_subheader('Install Markdown')
-    os_command('venv\\Scripts\\pip', 'install', 'Markdown')
-
-
-def run_tests():
-    log_subheader('Run Tests')
-    os_command('venv\\Scripts\\python', 'tools/execute-specs.py', '--write-testlist', 'testlist.txt')
-
-
-def build(build_version, node):
-    log_subheader(f'Start Build {build_version or node}')
-
-    os.chdir('tools')
-    os.system('..\\venv\\Scripts\python generate-mo-files.py')
-    log('mo file generated')
-
-    os.chdir('winbuildtools')
-    os.system('..\\..\\venv\\Scripts\python mod_paths.py .')
-    log('paths modified')
-
-    os.system(f'..\\..\\venv\\Scripts\python mod_iss_timeline_version.py . {build_version or node}')
-    log('version file modified')
-
-    log_subheader('Remove old build directory')
-    os.system('rmdir /S /Q build >> nul')
-    log_subheader('Remove old dist directory')
-    os.system('rmdir /S /Q dist >> nul')
-    log_subheader('Remove old out directory')
-    os.system('del /S /Q inno\out >> nul')
-
-    log_subheader('pyinstaller')
-    os.system('pyinstaller timeline.spec')
-
-    log_subheader('Copying icons and translations to dist')
-    os.system('mkdir .\\dist\\icons\\event_icons')
-    os.system('mkdir .\\dist\\translations')
-    os.system('xcopy /S ..\\..\\icons\*.*  .\dist\icons\*.*')
-    os.system('xcopy /S ..\\..\\translations\ca\*.*  .\dist\\translations\ca\*.*')
-    os.system('xcopy /S ..\\..\\translations\de\*.*  .\dist\\translations\de\*.*')
-    os.system('xcopy /S ..\\..\\translations\el\*.*  .\dist\\translations\el\*.*')
-    os.system('xcopy /S ..\\..\\translations\es\*.*  .\dist\\translations\es\*.*')
-    os.system('xcopy /S ..\\..\\translations\eu\*.*  .\dist\\translations\eu\*.*')
-    os.system('xcopy /S ..\\..\\translations\fr\*.*  .\dist\\translations\fr\*.*')
-    os.system('xcopy /S ..\\..\\translations\gl\*.*  .\dist\\translations\gl\*.*')
-    os.system('xcopy /S ..\\..\\translations\he\*.*  .\dist\\translations\he\*.*')
-    os.system('xcopy /S ..\\..\\translations\it\*.*  .\dist\\translations\it\*.*')
-    os.system('xcopy /S ..\\..\\translations\ko\*.*  .\dist\\translations\ko\*.*')
-    os.system('xcopy /S ..\\..\\translations\lt\*.*  .\dist\\translations\lt\*.*')
-    os.system('xcopy /S ..\\..\\translations\nl\*.*  .\dist\\translations\nl\*.*')
-    os.system('xcopy /S ..\\..\\translations\pl\*.*  .\dist\\translations\pl\*.*')
-    os.system('xcopy /S ..\\..\\translations\pt\*.*  .\dist\\translations\pt\*.*')
-    os.system('xcopy /S ..\\..\\translations\pt_BR\*.*  .\dist\\translations\pt_BR\*.*')
-    os.system('xcopy /S ..\\..\\translations\ru\*.*  .\dist\\translations\ru\*.*')
-    os.system('xcopy /S ..\\..\\translations\sv\*.*  .\dist\\translations\sv\*.*')
-    os.system('xcopy /S ..\\..\\translations\tr\*.*  .\dist\\translations\tr\*.*')
-    os.system('xcopy /S ..\\..\\translations\vi\*.*  .\dist\\translations\vi\*.*')
-    os.system('xcopy /S ..\\..\\translations\zh_CH\*.*  .\dist\\translations\zh_CH\*.*')
-
-    log('Create distributable')
-    os.chdir('inno')
-    os.system('iscc.exe timeline2Win32.iss')
-    log_subheader('DONE')
-
-
-def log_header():
-    print('-' * 70)
-    print(' jenkins-build-local.py')
-    print('-' * 70)
-
-
-def log_subheader(text):
-    print(f'----({text})------')
-
-
-def log(text):
-    print(f'  {text}')
-
-
-def main():
-    log_header()
-    hg_pull()
-    hg_update()
-    hg_purge()
-    build_version = get_build_version_from_input()
-    default_path = get_hg_default_path()
-    node = get_hg_node()
-    revision = get_hg_rev()
-    log(f'Build version: {build_version}')
-    log(f'Defult Path  : {default_path}')
-    log(f'Node         : {node}')
-    log(f'Revision     : {revision}')
-
-    log_subheader('Verify version')
-    verify_version(build_version, node)
-    log('Verified that revision exists')
-
-    log_subheader('Log changesets')
-    changesets = log_changesets(build_version, node)
-    print(changesets)
-
-    display_python_version()
-    create_virtual_environment()
-    upgrade_pip()
-
-    install_wxpython()
-    install_humblewx()
-    install_icalendar()
-    install_markdown()
-    run_tests()
-
-    os.system('venv\\Scripts\pip list')
-
-    build(build_version, node)
-
-
-if __name__ == '__main__':
-    if 'doctest' in sys.argv:
-        import doctest
-
-        sys.argv = sys.argv[:-1]
-        doctest.testmod()
-    else:
-        try:
-            main()
-        except BuildError as ex:
-            log('Build aborted')
-            log(ex)
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 tools/winbuildtools/mod_iss_timeline_version.py
--- a/tools/winbuildtools/mod_iss_timeline_version.py Sun Jul 06 09:06:44 2025 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,129 +0,0 @@
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
-#
-# This file is part of Timeline.
-#
-# Timeline 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.
-#
-# Timeline 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 Timeline.  If not, see <http://www.gnu.org/licenses/>.
-
-
-import sys
-import os
-import subprocess
-
-
-USAGE = """
-    Usage:
-        python mod_iss_timeline_version.py   project-tools-dir   revision
-"""
-
-
-def get_hash_and_id(revision):
-    try:
-        return subprocess.check_output([
-            "hg", "id",
-            "-r", revision,
-        ]).decode("utf-8").strip().split(" ", 1)
-    except subprocess.CalledProcessError as e:
-        print("ERROR:", str(e))
-        raise
-
-
-def get_revision_date(revision):
-    try:
-        return subprocess.check_output([
-            "hg", "log",
-            "-r", revision,
-            "--template", "{date|shortdate}",
-        ]).decode("utf-8").strip()
-    except subprocess.CalledProcessError as e:
-        print("ERROR:", str(e))
-        raise
-
-
-def get_version(versionfile):
-    with open(versionfile, "r") as f:
-        text = f.read()
-        lines = text.split("\n")
-        for line in lines:
-            if line[0:7] == "VERSION":
-                break
-    line = line.split("(", 1)[1]
-    line = line.split(")", 1)[0]
-    major, minor, bug = line. split(", ")
-    app_ver_name = "Timeline %s.%s.%s" % (major, minor, bug)
-    revision = sys.argv[2]
-    print("Revision:", revision)
-    hash_value, rev_id = get_hash_and_id(sys.argv[2])
-    revision_date = get_revision_date(sys.argv[2])
-    if rev_id == 'tip':
-        output_base_filename = "timeline-%s.%s.%s-beta-%s-%s-Win32Setup" % (major, minor, bug, hash_value, revision_date)
-        type = 'TYPE_BETA'
-    else:
-        output_base_filename = "timeline-%s.%s.%s-Win32Setup" % (major, minor, bug)
-        type = 'TYPE_FINAL'
-    with open(versionfile, "r") as f:
-        text = f.read()
-    text = text.replace('TYPE = TYPE_DEV', f'TYPE = {type}')
-    text = text.replace('REVISION_HASH = ""', f'REVISION_HASH = "{hash_value}"')
-    text = text.replace('REVISION_DATE = ""', f'REVISION_DATE = "{revision_date}"')
-    with open(versionfile, "w") as f:
-        f.write(text)
-    print("[INFO] Version found: %s" % app_ver_name)
-    print("[INFO] Filename: %s" % output_base_filename)
-    return app_ver_name, output_base_filename
-
-
-def modify_iss_file(target, app_ver_name, output_base_filename):
-    with open(target, "r") as f:
-        text = f.read()
-    with open(target, "w") as f:
-        lines = text.split("\n")
-        for line in lines:
-            if line[0:11] == "AppVerName=":
-                line = "AppVerName=%s" % app_ver_name
-                f.write(line + "\n")
-                print("[INFO] Iss file version line: %s" % line)
-            elif line[0:19] == "OutputBaseFilename=":
-                line = "OutputBaseFilename=%s" % output_base_filename
-                f.write(line + "\n")
-                print("[INFO] Iss base filename line: %s" % line)
-            else:
-                f.write(line + "\n")
-
-
-def main():
-    project_dir = sys.argv[1]
-    target = os.path.join(project_dir, "inno", "timeline2Win32.iss")
-    versionfile_path = os.path.join(project_dir, "..", "..", "source", "timelinelib", "meta", "version.py")
-    print("Script: mod2_timeline_iss_win32.py")
-    print("Target:", target)
-    print("Version:", versionfile_path)
-    if not os.path.exists(target):
-        print("[ERROR] Can't find target file: %s" % target)
-        return
-    if not os.path.exists(versionfile_path):
-        print("[ERROR] Can't find version file: %s" % versionfile_path)
-        return
-    app_ver_name, output_base_filename = get_version(versionfile_path)
-    modify_iss_file(target, app_ver_name, output_base_filename)
-
-
-if __name__ == "__main__":
-    if len(sys.argv) != 3:
-        print(USAGE)
-    else:
-        if not os.path.exists(sys.argv[1]):
-            print(USAGE)
-            print("[ERROR] Can't find project root dir: %s" % sys.argv[1])
-        else:
-            main()
diff -r 7bdcd3d4dc9a -r dd4c4b92daa3 tools/winbuildtools/mod_paths.py
--- a/tools/winbuildtools/mod_paths.py Sun Jul 06 09:06:44 2025 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
-#
-# This file is part of Timeline.
-#
-# Timeline 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.
-#
-# Timeline 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 Timeline.  If not, see <http://www.gnu.org/licenses/>.
-
-import sys
-import os
-
-
-def main():
-    project_dir = sys.argv[1]
-    path = os.path.join(
-        project_dir, "..", "..", "source", "timelinelib", "config", "paths.py"
-    )
-    text = None
-    with open(path) as f:
-        text = f.read()
-    collector = []
-    for line in text.split("\n"):
-        if line.strip().startswith("_ROOT ="):
-            line = "_ROOT = '.'"
-        collector.append(line.strip())
-    text = "\n".join(collector)
-    with open(path, "w") as f:
-        f.write(text)
-    print("paths.py modified")
-    print(text)
-
-
-main()

2025-07-06 09:02 Rickard pushed to timeline

changeset:   7991:ca9470a3fae4
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Jul 06 09:02:16 2025 +0200
summary:     Document change in Windows installer.

diff -r af1960bd1ad4 -r ca9470a3fae4 documentation/changelog.rst
--- a/documentation/changelog.rst Sun Jul 06 08:27:09 2025 +0200
+++ b/documentation/changelog.rst Sun Jul 06 09:02:16 2025 +0200
@@ -34,6 +34,10 @@
 * ``Corrupted icon file causes exception``
   Use a default icon when icon file is corrupted and disable error dialog.
 
+Windows installer:
+
+* The Windows installer is now built on Linux/Wine and only for 64 bit platforms.
+
 
 Version 2.10.0
 --------------

2025-07-06 08:27 Rickard pushed to timeline

changeset:   7990:af1960bd1ad4
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Jul 06 08:27:09 2025 +0200
summary:     Win64Setup is no longer experimental.

diff -r 3794bad33bc3 -r af1960bd1ad4 tools/build-windows-exe-from-source-zip.py
--- a/tools/build-windows-exe-from-source-zip.py Sun Jul 06 08:23:28 2025 +0200
+++ b/tools/build-windows-exe-from-source-zip.py Sun Jul 06 08:27:09 2025 +0200
@@ -56,7 +56,7 @@
         "AppVerName",
         f"Timeline {archive.get_version_number_string()}"
     )
-    exe_name = f"{archive.get_filename_version()}-Win64Setup-Experimental"
+    exe_name = f"{archive.get_filename_version()}-Win64Setup"
     archive.change_constant(
         "tools/winbuildtools/inno/timeline.iss",
         "OutputBaseFilename",

2025-07-06 08:23 Rickard pushed to timeline

changeset:   7989:3794bad33bc3
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Jul 06 08:23:28 2025 +0200
summary:     Rename timeline2Win32.iss to timeline.iss.

diff -r c6f07716d723 -r 3794bad33bc3 tools/build-windows-exe-from-source-zip.py
--- a/tools/build-windows-exe-from-source-zip.py Sun Jul 06 08:20:45 2025 +0200
+++ b/tools/build-windows-exe-from-source-zip.py Sun Jul 06 08:23:28 2025 +0200
@@ -52,13 +52,13 @@
         sys.exit("ERROR: Could not find source zip.")
     archive = timelinetools.packaging.zipfile.ZipFile(".", zips[0]).extract_to(tempdir)
     archive.change_constant(
-        "tools/winbuildtools/inno/timeline2Win32.iss",
+        "tools/winbuildtools/inno/timeline.iss",
         "AppVerName",
         f"Timeline {archive.get_version_number_string()}"
     )
     exe_name = f"{archive.get_filename_version()}-Win64Setup-Experimental"
     archive.change_constant(
-        "tools/winbuildtools/inno/timeline2Win32.iss",
+        "tools/winbuildtools/inno/timeline.iss",
         "OutputBaseFilename",
         exe_name
     )
@@ -87,7 +87,7 @@
     subprocess.check_call([
         "wine",
         f"{environ['INNO_DIR']}\\ISCC.exe",
-        "inno/timeline2Win32.iss",
+        "inno/timeline.iss",
     ], cwd=archive.get_path("tools/winbuildtools"))
     subprocess.check_call([
         "wineserver",
diff -r c6f07716d723 -r 3794bad33bc3 tools/winbuildtools/inno/timeline.iss
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/winbuildtools/inno/timeline.iss Sun Jul 06 08:23:28 2025 +0200
@@ -0,0 +1,66 @@
+; Script generated by the Inno Setup Script Wizard.
+; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
+
+[Setup]
+AppName=Timeline
+;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+;
+; Before running this script ...
+; The two lines below must be uncommented and text changed to reflect
+; the version number of the executable to be built.
+;
+AppVerName=Timeline 2.1.0
+OutputBaseFilename=timeline-2.1.0-beta-4b501487562b-2019-11-26-Win32Setup
+;
+;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+AppPublisher=Rickard Lindberg <ricli85@gmail.com>
+AppPublisherURL=http://thetimelineproj.sourceforge.net/
+AppSupportURL=http://thetimelineproj.sourceforge.net/
+AppUpdatesURL=http://thetimelineproj.sourceforge.net/
+DefaultDirName={pf}\Timeline
+DefaultGroupName=Timeline
+SourceDir=.\
+LicenseFile=..\..\..\COPYING
+InfoBeforeFile=WINSTALL
+OutputDir=out
+Compression=lzma
+SolidCompression=yes     
+DisableDirPage=no
+
+[Languages]
+Name: "english"; MessagesFile: "compiler:Default.isl"
+
+
+[Tasks]
+Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
+Name: "startmenu";   Description: "Create a start menu"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
+
+[Files]
+Source: "..\dist\timeline.exe"; DestDir: "{app}"; Flags: ignoreversion  recursesubdirs
+Source: "..\dist\icons\*"; DestDir: "{app}\icons"; Flags: ignoreversion  recursesubdirs
+Source: "..\dist\translations\*"; DestDir: "{app}\translations"; Flags: ignoreversion  recursesubdirs
+
+;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+;
+; Before running this script ...
+; You must check to see if there are any more po-files to add
+;
+;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+
+[Icons]
+Name: "{commondesktop}\Timeline"; Filename:"{app}\timeline.exe"; IconFilename: "{app}\icons\Timeline.ico"; Tasks: desktopicon
+Name: "{group}\Timeline";         Filename:"{app}\timeline.exe"; IconFilename: "{app}\icons\Timeline.ico"; WorkingDir: "{app}"; Tasks: startmenu
+
+
+
+
+[Run]
+Filename: "{app}\timeline.exe"; Description: "{cm:LaunchProgram,Timeline}"; Flags: shellexec postinstall skipifsilent;
+
+
+[UninstallDelete]
+Type: files; Name: "{win}\uninstall\MYPROG.INI"
+
+
diff -r c6f07716d723 -r 3794bad33bc3 tools/winbuildtools/inno/timeline2Win32.iss
--- a/tools/winbuildtools/inno/timeline2Win32.iss Sun Jul 06 08:20:45 2025 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-; Script generated by the Inno Setup Script Wizard.
-; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
-
-[Setup]
-AppName=Timeline
-;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-;
-; Before running this script ...
-; The two lines below must be uncommented and text changed to reflect
-; the version number of the executable to be built.
-;
-AppVerName=Timeline 2.1.0
-OutputBaseFilename=timeline-2.1.0-beta-4b501487562b-2019-11-26-Win32Setup
-;
-;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-
-AppPublisher=Rickard Lindberg <ricli85@gmail.com>
-AppPublisherURL=http://thetimelineproj.sourceforge.net/
-AppSupportURL=http://thetimelineproj.sourceforge.net/
-AppUpdatesURL=http://thetimelineproj.sourceforge.net/
-DefaultDirName={pf}\Timeline
-DefaultGroupName=Timeline
-SourceDir=.\
-LicenseFile=..\..\..\COPYING
-InfoBeforeFile=WINSTALL
-OutputDir=out
-Compression=lzma
-SolidCompression=yes     
-DisableDirPage=no
-
-[Languages]
-Name: "english"; MessagesFile: "compiler:Default.isl"
-
-
-[Tasks]
-Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
-Name: "startmenu";   Description: "Create a start menu"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
-
-[Files]
-Source: "..\dist\timeline.exe"; DestDir: "{app}"; Flags: ignoreversion  recursesubdirs
-Source: "..\dist\icons\*"; DestDir: "{app}\icons"; Flags: ignoreversion  recursesubdirs
-Source: "..\dist\translations\*"; DestDir: "{app}\translations"; Flags: ignoreversion  recursesubdirs
-
-;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-;
-; Before running this script ...
-; You must check to see if there are any more po-files to add
-;
-;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-
-
-[Icons]
-Name: "{commondesktop}\Timeline"; Filename:"{app}\timeline.exe"; IconFilename: "{app}\icons\Timeline.ico"; Tasks: desktopicon
-Name: "{group}\Timeline";         Filename:"{app}\timeline.exe"; IconFilename: "{app}\icons\Timeline.ico"; WorkingDir: "{app}"; Tasks: startmenu
-
-
-
-
-[Run]
-Filename: "{app}\timeline.exe"; Description: "{cm:LaunchProgram,Timeline}"; Flags: shellexec postinstall skipifsilent;
-
-
-[UninstallDelete]
-Type: files; Name: "{win}\uninstall\MYPROG.INI"
-
-

2025-07-06 08:21 Rickard pushed to timeline

changeset:   7988:c6f07716d723
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Jul 06 08:20:45 2025 +0200
summary:     Remove no longer used winbuild tools.

diff -r 2d4564f72ba5 -r c6f07716d723 tools/build_win32_installer.py
--- a/tools/build_win32_installer.py Sat Jul 05 21:51:47 2025 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,285 +0,0 @@
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
-#
-# This file is part of Timeline.
-#
-# Timeline 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.
-#
-# Timeline 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 Timeline.  If not, see <http://www.gnu.org/licenses/>.
-
-
-"""
-Working directory = BUILD_DIR
-COPYDIR     Copies from TIMELINE_DIR to BUILD_DIR
-"""
-
-import argparse
-import sys
-import os
-import shutil
-import tempfile
-import subprocess
-import timelinetools.packaging.repository
-
-
-TIMELINE_DIR = os.path.abspath("..\\..\\")
-BUILD_DIR = os.path.abspath(".\\target")
-ARCHIVE = "archive"
-ARTIFACT = "artifact"
-
-COPYFILE = 0
-COPYDIR = 1
-PUSHD = 3
-POPD = 4
-RUNCMD = 5
-RUNPYSCRIPT = 6
-ANNOTATE = 8
-RUNPYTEST = 9
-
-ACTION_NAMES = {COPYFILE: "COPYFILE",
-                COPYDIR: "COPYDIR",
-                PUSHD: "PUSHD",
-                POPD: "POPD",
-                RUNCMD: "RUNCMD",
-                RUNPYSCRIPT: "RUNPYSCRIPT",
-                RUNPYTEST: "RUNPYTEST"
-                }
-
-
-known_targets = ("win32Installer")
-
-
-win32InstallerActions = (
-    (ANNOTATE, "Run Tests", ""),
-    (RUNPYTEST, ["tools", "execute-specs.py"], ""),
-
-    (ANNOTATE, "Modify source files", ""),
-    (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_timeline_py.py"], ""),
-    (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_paths_py.py"], ""),
-    (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_version_py.py"], ""),
-    (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_factory_py.py"], ""),
-    (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_timeline_iss_win32.py"], ""),
-
-    (ANNOTATE, "Create a target directory for the build", ""),
-    (COPYDIR, ["source", "timelinelib"], ["builddir", "timelinelib"]),
-    (COPYDIR, ["tools", "winbuildtools", "inno"], ["builddir", "inno"]),
-    (COPYFILE, ["source", "timeline.py"], ["builddir", "timeline.py"]),
-    (COPYFILE, ["tools", "winbuildtools", "setup.py"], ["builddir", "setup.py"]),
-    (COPYFILE, ["COPYING"], ["builddir", "COPYING"]),
-    (COPYFILE, ["tools", "winbuildtools", "inno", "WINSTALL"], ["builddir", "WINSTALL"]),
-
-    (ANNOTATE, "Create distribution directory", ""),
-    (COPYDIR, ["icons"], ["builddir", "icons"]),
-    (RUNPYSCRIPT, ["builddir", "setup.py"], "py2exe"),
-    (COPYDIR, ["translations"], ["builddir", "dist", "translations"]),
-    (COPYDIR, ["icons"], ["builddir", "dist", "icons"]),
-    (COPYDIR, ["tools"], ["builddir", "dist", "tools"]),
-
-    (ANNOTATE, "Create installer executable", ""),
-    (RUNCMD, "python", ["builddir", "dist", "tools", "generate-mo-files.py"]),
-
-    (ANNOTATE, "Create Setup executable", ""),
-    (RUNCMD, "iscc.exe", ["builddir", "inno", "timelineWin32.iss"]),
-
-    (ANNOTATE, "Deliver executable artifact", ""),
-    (COPYFILE, [ARTIFACT], [ARTIFACT]),
-
-    (ANNOTATE, "Done", ""),
-)
-
-
-actions = {"win32Installer": win32InstallerActions}
-
-
-class Target():
-
-    def __init__(self, target):
-        print("-------------------------------------------------------")
-        print("  %s" % ("Building target %s" % target))
-        print("-------------------------------------------------------")
-        self.target = target
-        self.actions = actions[target]
-        self.ACTION_METHODS = {COPYFILE: self.copyfile,
-                               COPYDIR: self.copydir,
-                               PUSHD: self.pushd,
-                               POPD: self.popd,
-                               RUNCMD: self.runcmd,
-                               RUNPYSCRIPT: self.runpyscript,
-                               RUNPYTEST: self.runpytest,
-                               ANNOTATE: self.annotate}
-
-    def build(self, arguments, artifact_dir):
-        temp_dir = tempfile.mkdtemp()
-        try:
-            self.assert_that_target_is_known()
-            self.setup_and_create_directories(arguments, artifact_dir, temp_dir)
-            self.execute_actions()
-        finally:
-            #shutil.rmtree(temp_dir)
-            pass
-
-    def assert_that_target_is_known(self):
-        if self.target not in known_targets:
-            print("The target %s is unknown" % self.target)
-            print("BUILD FAILED")
-            sys.exit(1)
-
-    def setup_and_create_directories(self, arguments, artifact_dir, temp_dir):
-        self.artifact_dir = artifact_dir
-        self.project_dir = self.create_project_directory(arguments, temp_dir)
-        print("Artifact dir: %s" % self.artifact_dir)
-        print("Project dir:  %s" % self.project_dir)
-        print("Working dir:  %s" % os.getcwd())
-
-    def create_project_directory(self, arguments, temp_dir):
-        print("Create project directory")
-        repository = timelinetools.packaging.repository.Repository()
-        self.archive = repository.archive(arguments.revision, temp_dir, ARCHIVE)
-        return os.path.join(temp_dir, ARCHIVE)
-
-    def execute_actions(self):
-        count = 0
-        total = len([actions for action in self.actions if action[0] is not ANNOTATE])
-        try:
-            for action, src, dst in self.actions:
-                if action is not ANNOTATE:
-                    count += 1
-                    print("Action %2d(%2d): %s" % (count, total, ACTION_NAMES[action]))
-                self.ACTION_METHODS[action](src, dst)
-            print("BUILD DONE")
-        except Exception as ex:
-            print(str(ex))
-            print("BUILD FAILED")
-            sys.exit(1)
-
-    def annotate(self, src, dst):
-        self.print_header(src)
-
-    def copyfile(self, src, dst):
-        if src[0] == ARTIFACT:
-            f = os.path.join(self.project_dir, self.get_artifact_src_name())
-            t = os.path.join(self.artifact_dir, self.get_artifact_target_name())
-        else:
-            f = os.path.join(self.project_dir, *src)
-            t = os.path.join(self.project_dir, *dst)
-        self.print_src_dst(f, t)
-        shutil.copyfile(f, t)
-
-    def copydir(self, src, dst):
-        f = os.path.join(self.project_dir, *src)
-        t = os.path.join(self.project_dir, *dst)
-        self.print_src_dst(f, t)
-        shutil.copytree(f, t)
-
-    def runpyscript(self, src, arg):
-        try:
-            script_path = os.path.join(self.project_dir, *src)
-            self.print_src_dst(script_path, arg)
-            if src[-1] == "setup.py":
-                self.pushd(os.path.join(self.project_dir, "builddir"), None)
-                success, msg = self.run_pyscript(script_path, [arg])
-                self.popd(None, None)
-            else:
-                success, msg = self.run_pyscript(script_path, [self.project_dir, arg])
-            if not success:
-                raise Exception(msg)
-        except Exception as ex:
-            pass
-
-    def runpytest(self, src, dst):
-        script_path = os.path.join(self.project_dir, *src)
-        self.pushd(os.path.dirname(script_path), None)
-        self.print_src_dst(src, os.path.abspath(dst))
-        success, msg = self.run_pyscript(script_path, [], display_stderr=True)
-        if not success:
-            print('Msg:', msg)
-            if msg:
-                raise Exception(msg)
-        self.popd(None, None)
-
-    def runcmd(self, src, dst):
-        t = os.path.join(self.project_dir, *dst)
-        self.pushd(os.path.dirname(t), None)
-        self.print_src_dst(src, t)
-        success, msg = self.run_command([src, t])
-        self.popd(None, None)
-        if not success:
-            raise Exception(msg)
-
-    def pushd(self, src, dst):
-        self.print_src_dst(os.getcwd(), os.path.abspath(src))
-        self.cwd = os.getcwd()
-        os.chdir(src)
-
-    def popd(self, src, dst):
-        self.print_src_dst(None, self.cwd)
-        print("    dst: %s" % self.cwd)
-        os.chdir(self.cwd)
-
-    def run_pyscript(self, script, args=[], display_stderr=False):
-        return self.run_command(["python", script] + args, display_stderr)
-
-    def run_command(self, cmd, display_stderr=False):
-        if display_stderr:
-            rc = subprocess.call(cmd)
-            return rc == 0, ""
-        else:
-            p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-            out = p.communicate()
-            print(out)
-            if p.returncode == 0:
-                return True, out[0]
-            else:
-                return False, out[1]
-
-    def print_header(self, message):
-        print("-------------------------------------------------------")
-        print("  %s" % message)
-        print("-------------------------------------------------------")
-
-    def print_src_dst(self, src, dst):
-        if src is not None:
-            print("    src: %s" % src)
-        if dst is not None:
-            print("    dst: %s" % dst)
-
-    def get_artifact_src_name(self):
-        versionfile = os.path.join(self.project_dir, "source", "timelinelib", "meta", "version.py")
-        f = open(versionfile, "r")
-        text = f.read()
-        lines = text.split("\n")
-        for line in lines:
-            if line[0:7] == "VERSION":
-                break
-        f.close()
-        # VERSION = (0, 14, 0)
-        line = line.split("(", 1)[1]
-        line = line.split(")", 1)[0]
-        major, minor, bug = line. split(", ")
-        return "SetupTimeline%s%s%sPy2ExeWin32.exe" % (major, minor, bug)
-
-    def get_artifact_target_name(self):
-        return "%s-Win32Setup.exe" % self.archive.get_filename_version()
-
-
-def main():
-    artifactdir = os.path.join(sys.path[0], "..")
-    Target("win32Installer").build(parse_arguments(), artifactdir)
-
-
-def parse_arguments():
-    parser = argparse.ArgumentParser()
-    parser.add_argument("--revision", default="tip")
-    return parser.parse_args()
-
-
-if __name__ == "__main__":
-    main()
diff -r 2d4564f72ba5 -r c6f07716d723 tools/build_win32_installer_py27.py
--- a/tools/build_win32_installer_py27.py Sat Jul 05 21:51:47 2025 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,286 +0,0 @@
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
-#
-# This file is part of Timeline.
-#
-# Timeline 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.
-#
-# Timeline 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 Timeline.  If not, see <http://www.gnu.org/licenses/>.
-
-
-"""
-Working directory = BUILD_DIR
-COPYDIR     Copies from TIMELINE_DIR to BUILD_DIR
-"""
-
-import argparse
-import sys
-import os
-import shutil
-import tempfile
-import subprocess
-import timelinetools.packaging.repository
-
-
-TIMELINE_DIR = os.path.abspath("..\\..\\")
-BUILD_DIR = os.path.abspath(".\\target")
-ARCHIVE = "archive"
-ARTIFACT = "artifact"
-
-COPYFILE = 0
-COPYDIR = 1
-PUSHD = 3
-POPD = 4
-RUNCMD = 5
-RUNPYSCRIPT = 6
-ANNOTATE = 8
-RUNPYTEST = 9
-
-ACTION_NAMES = {COPYFILE: "COPYFILE",
-                COPYDIR: "COPYDIR",
-                PUSHD: "PUSHD",
-                POPD: "POPD",
-                RUNCMD: "RUNCMD",
-                RUNPYSCRIPT: "RUNPYSCRIPT",
-                RUNPYTEST: "RUNPYTEST"
-                }
-
-
-known_targets = ("win32Installer")
-
-
-win32InstallerActions = (
-                 (ANNOTATE, "Run Tests", ""),
-                 (RUNPYTEST, ["tools", "execute-specs.py"], ""),
-
-                 (ANNOTATE, "Modify source files", ""),
-                 (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_timeline_py.py"], ""),
-                 (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_paths_py.py"], ""),
-                 (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_version_py.py"], ""),
-                 (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_factory_py.py"], ""),
-                 (RUNPYSCRIPT, ["tools", "winbuildtools", "mod_timeline_iss_win32_py27.py"], ""),
-
-                 (ANNOTATE, "Create a target directory for the build", ""),
-                 (COPYDIR, ["source", "timelinelib"], ["builddir", "timelinelib"]),
-                 (COPYDIR, ["dependencies", "timelinelib", "icalendar-3.2\icalendar"], ["builddir", "icalendar"]),
-                 (COPYDIR, ["dependencies", "timelinelib", "pytz-2012j\pytz"], ["builddir", "pytz"]),
-                 (COPYDIR, ["dependencies", "timelinelib", "markdown-2.0.3", "markdown"], ["builddir", "markdown"]),
-                 (COPYDIR, ["tools", "winbuildtools", "inno"], ["builddir", "inno"]),
-                 (COPYFILE, ["source", "timeline.py"], ["builddir", "timeline.py"]),
-                 (COPYFILE, ["tools", "winbuildtools", "setup.py"], ["builddir", "setup.py"]),
-                 (COPYFILE, ["COPYING"], ["builddir", "COPYING"]),
-                 (COPYFILE, ["tools", "winbuildtools", "inno", "WINSTALL"], ["builddir", "WINSTALL"]),
-
-                 (ANNOTATE, "Create distribution directory", ""),
-                 (COPYDIR, ["icons"], ["builddir", "icons"]),
-                 (RUNPYSCRIPT, ["builddir", "setup.py"], "py2exe"),
-                 (COPYDIR, ["translations"], ["builddir", "dist", "translations"]),
-                 (COPYDIR, ["icons"], ["builddir", "dist", "icons"]),
-                 (COPYDIR, ["tools"], ["builddir", "dist", "tools"]),
-
-                 (ANNOTATE, "Create installer executable", ""),
-                 (RUNCMD, "python", ["builddir", "dist", "tools", "generate-mo-files.py"]),
-
-                 (ANNOTATE, "Create Setup executable", ""),
-                 (RUNCMD, "iscc.exe", ["builddir", "inno", "timelineWin32Py27.iss"]),
-
-                 (ANNOTATE, "Deliver executable artifact", ""),
-                 (COPYFILE, [ARTIFACT], [ARTIFACT]),
-
-                 (ANNOTATE, "Done", ""),
-                 )
-
-
-actions = {"win32Installer": win32InstallerActions}
-
-
-class Target():
-
-    def __init__(self, target):
-        print("-------------------------------------------------------")
-        print("  %s" % ("Building target %s" % target))
-        print("-------------------------------------------------------")
-        self.target = target
-        self.actions = actions[target]
-        self.ACTION_METHODS = {COPYFILE: self.copyfile,
-                               COPYDIR: self.copydir,
-                               PUSHD: self.pushd,
-                               POPD: self.popd,
-                               RUNCMD: self.runcmd,
-                               RUNPYSCRIPT: self.runpyscript,
-                               RUNPYTEST: self.runpytest,
-                               ANNOTATE: self.annotate}
-
-    def build(self, arguments, artifact_dir):
-        temp_dir = tempfile.mkdtemp()
-        try:
-            self.assert_that_target_is_known()
-            self.setup_and_create_directories(arguments, artifact_dir, temp_dir)
-            self.execute_actions()
-        finally:
-            # shutil.rmtree(temp_dir)
-            pass
-
-    def assert_that_target_is_known(self):
-        if self.target not in known_targets:
-            print("The target %s is unknown" % self.target)
-            print("BUILD FAILED")
-            sys.exit(1)
-
-    def setup_and_create_directories(self, arguments, artifact_dir, temp_dir):
-        self.artifact_dir = artifact_dir
-        self.project_dir = self.create_project_directory(arguments, temp_dir)
-        print("Artifact dir: %s" % self.artifact_dir)
-        print("Project dir:  %s" % self.project_dir)
-        print("Working dir:  %s" % os.getcwd())
-
-    def create_project_directory(self, arguments, temp_dir):
-        print("Create project directory")
-        repository = timelinetools.packaging.repository.Repository()
-        self.archive = repository.archive(arguments.revision, temp_dir, ARCHIVE)
-        return os.path.join(temp_dir, ARCHIVE)
-
-    def execute_actions(self):
-        count = 0
-        total = len([actions for action in self.actions if action[0] is not ANNOTATE])
-        try:
-            for action, src, dst in self.actions:
-                if action is not ANNOTATE:
-                    count += 1
-                    print("Action %2d(%2d): %s" % (count, total, ACTION_NAMES[action]))
-                self.ACTION_METHODS[action](src, dst)
-            print("BUILD DONE")
-        except Exception as ex:
-            print(str(ex))
-            print("BUILD FAILED")
-            sys.exit(1)
-
-    def annotate(self, src, dst):
-        self.print_header(src)
-
-    def copyfile(self, src, dst):
-        if src[0] == ARTIFACT:
-            f = os.path.join(self.project_dir, self.get_artifact_src_name())
-            t = os.path.join(self.artifact_dir, self.get_artifact_target_name())
-        else:
-            f = os.path.join(self.project_dir, *src)
-            t = os.path.join(self.project_dir, *dst)
-        self.print_src_dst(f, t)
-        shutil.copyfile(f, t)
-
-    def copydir(self, src, dst):
-        f = os.path.join(self.project_dir, *src)
-        t = os.path.join(self.project_dir, *dst)
-        self.print_src_dst(f, t)
-        shutil.copytree(f, t)
-
-    def runpyscript(self, src, arg):
-        try:
-            script_path = os.path.join(self.project_dir, *src)
-            self.print_src_dst(script_path, arg)
-            if src[-1] == "setup.py":
-                self.pushd(os.path.join(self.project_dir, "builddir"), None)
-                success, msg = self.run_pyscript(script_path, [arg])
-                self.popd(None, None)
-            else:
-                success, msg = self.run_pyscript(script_path, [self.project_dir, arg])
-            if not success:
-                raise Exception(msg)
-        except Exception:
-            pass
-
-    def runpytest(self, src, dst):
-        script_path = os.path.join(self.project_dir, *src)
-        self.pushd(os.path.dirname(script_path), None)
-        self.print_src_dst(src, os.path.abspath(dst))
-        success, msg = self.run_pyscript(script_path, [], display_stderr=True)
-        if not success:
-            raise Exception(msg)
-        self.popd(None, None)
-
-    def runcmd(self, src, dst):
-        t = os.path.join(self.project_dir, *dst)
-        self.pushd(os.path.dirname(t), None)
-        self.print_src_dst(src, t)
-        success, msg = self.run_command([src, t])
-        self.popd(None, None)
-        if not success:
-            raise Exception(msg)
-
-    def pushd(self, src, dst):
-        self.print_src_dst(os.getcwd(), os.path.abspath(src))
-        self.cwd = os.getcwd()
-        os.chdir(src)
-
-    def popd(self, src, dst):
-        self.print_src_dst(None, self.cwd)
-        print("    dst: %s" % self.cwd)
-        os.chdir(self.cwd)
-
-    def run_pyscript(self, script, args=[], display_stderr=False):
-        return self.run_command(["python", script] + args, display_stderr)
-
-    def run_command(self, cmd, display_stderr=False):
-        print('cmd:', cmd)
-        if display_stderr:
-            rc = subprocess.call(cmd)
-            return rc == 0, ""
-        else:
-            p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-            out = p.communicate()
-            print(out)
-            if p.returncode == 0:
-                return True, out[0]
-            else:
-                return False, out[1]
-
-    def print_header(self, message):
-        print("-------------------------------------------------------")
-        print("  %s" % message)
-        print("-------------------------------------------------------")
-
-    def print_src_dst(self, src, dst):
-        if src is not None:
-            print("    src: %s" % src)
-        if dst is not None:
-            print("    dst: %s" % dst)
-
-    def get_artifact_src_name(self):
-        versionfile = os.path.join(self.project_dir, "source", "timelinelib", "meta", "version.py")
-        f = open(versionfile, "r")
-        text = f.read()
-        lines = text.split("\n")
-        for line in lines:
-            if line[0:7] == "VERSION":
-                break
-        f.close()
-        # VERSION = (0, 14, 0)
-        line = line.split("(", 1)[1]
-        line = line.split(")", 1)[0]
-        major, minor, bug = line. split(", ")
-        return "SetupTimeline%s%s%sPy2ExeWin32.exe" % (major, minor, bug)
-
-    def get_artifact_target_name(self):
-        return "%s-Win32Setup.exe" % self.archive.get_filename_version()
-
-def main():
-    artifactdir = os.path.join(sys.path[0], "..")
-    Target("win32Installer").build(parse_arguments(), artifactdir)
-
-
-def parse_arguments():
-    parser = argparse.ArgumentParser()
-    parser.add_argument("--revision", default="tip")
-    return parser.parse_args()
-
-
-if __name__ == "__main__":
-    main()
diff -r 2d4564f72ba5 -r c6f07716d723 tools/winbuildtools/build_win_setup_executable.py
--- a/tools/winbuildtools/build_win_setup_executable.py Sat Jul 05 21:51:47 2025 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,374 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
-#
-# This file is part of Timeline.
-#
-# Timeline 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.
-#
-# Timeline 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 Timeline.  If not, see <http://www.gnu.org/licenses/>.
-
-
-"""
-This script is used to create a Timeline installation file for Windows
-
-Usage:
-    Copy this file to a temporary directory and issue the command
-        python build_win_setup_executable.py [version]
-
-    If a version argument is given, it must be the name of a tagged version
-    such as 2.10.0.
-    If not given, the latest code is used.
-
-    The path to the python version used is defined by the pragram constant
-    PYTHON_HOME. If default isn't what you want you have to change it.
-
-Prerequisite:
-    1. Must run on a Windows machine
-    2. Mercurial (hg) must be installed
-    3. Python must be installed
-    4. Inno Setup (iscc.exe) must be installed
-    5. Dependent on files in Timeline repo:
-        tools/execute-specs.py
-        tools/generate-mo-files.py
-        tools/winbuildtools/mod_paths.py
-        tools/winbuildtools/mod_iss_timeline_version.py
-        tools/winbuildtools/timeline.spec
-        tools/winbuildtools/dist/icons/*
-        tools/winbuildtools/dist/translations/*
-
-
-"""
-
-import os
-import subprocess
-from cmdlineparser import CmdLineParser, PositionalArg, NamedArg
-
-
-class BuildError(Exception):
-    pass
-
-
-class Build:
-
-    def __init__(self, python_home, build_version=None, keep_workspace=False):
-        self.PYTHON = os.path.join(python_home, "python.exe")
-        self.PIP = os.path.join(python_home, "pip.exe")
-        self.PYINSTALLER = None
-        self._build_version = build_version
-        self._root_dir = os.path.dirname(os.path.realpath(__file__))
-        self._keep_workspace = keep_workspace
-        self._log_header("Input data")
-        self._log(f"Python home   : {python_home}")
-        self._log(f"Build version : {self._build_version}")
-        self._log(f"Keep workspace: {self._keep_workspace}")
-
-    def _validate_prerequisite(self):
-        def os_is_windows():
-            if os.name == "nt":
-                self._log("OS is windows")
-            else:
-                raise BuildError("This script only runs on Windows!")
-
-        def mercurial_is_installed():
-            if self._os_command("where", "hg").endswith("hg.exe"):
-                self._log("Mercurial is installed")
-            else:
-                raise BuildError("Mercurial is not installed!")
-
-        def python_installed():
-            try:
-                version = self._os_command(self.PYTHON, "-V")
-                self._log(f"Python {version} is installed")
-            except:
-                raise BuildError("Python is not installed!")
-
-        def iscc_is_installed():
-            if self._os_command("where", "iscc").lower().endswith("iscc.exe"):
-                self._log("ISCC is installed")
-            else:
-                raise BuildError("ISCC is not installed!")
-
-        self._log_header("Validating Prerequisite")
-        os_is_windows()
-        mercurial_is_installed()
-        python_installed()
-        iscc_is_installed()
-
-    def _create_workspace(self):
-        self._create_dir("workspace")
-        self._cd("workspace")
-
-    def _os_command(self, *args):
-        return subprocess.check_output(list(args)).decode("latin-1").strip()
-
-    def _create_dir(self, dirname):
-        os.mkdir(dirname)
-
-    def _cd(self, dirname):
-        if dirname == "root":
-            path = self._root_dir
-        else:
-            path = os.path.join(self._root_dir, dirname)
-        os.chdir(path)
-        self._log(f"Directory changed to: {os.getcwd()}")
-
-    def _remove_dir(self, dirname):
-        os.rmdir(dirname)
-
-    def _get_timeline_from_repo(self):
-        """http://hg.code.sf.net/p/thetimelineproj/main"""
-        self._log_header("Clone Timeline repo")
-        os.system("hg clone http://hg.code.sf.net/p/thetimelineproj/main .")
-        self._log_header("Get hg info")
-        self._hg_node = self._get_hg_node()
-        self._build_version_exists()
-        self._hg_rev = self._get_hg_rev()
-        self._log(f"Build version: {self._build_version}")
-        self._log(f"Node         : {self._hg_node}")
-        self._log(f"Revision     : {self._hg_rev}")
-
-    def _build_version_exists(self):
-        if self._build_version is not None:
-            try:
-                self._os_command(
-                    "hg", "log", "--rev", self._build_version, "--template", "exists"
-                )
-                self._log("Build version exists")
-                self._os_command("hg", "update", "--rev", self._build_version)
-                self._log("Project updated to build version")
-            except:
-                raise BuildError(f"unknown revision: {self._build_version}!")
-
-    def _get_hg_node(self):
-        return self._os_command("hg", "log", "--rev", ".", "--template", "{node}")
-
-    def _get_hg_rev(self):
-        return self._os_command("hg", "log", "--rev", ".", "--template", "{rev}")
-
-    def _pip_install(self, package, version=None):
-        self._log(f'Install {package} {version if version is not None else ""}')
-        if version is not None:
-            self._os_command(self.PIP, "install", "-v", f"{package}=={version}")
-        else:
-            self._os_command(self.PIP, "install", package)
-
-    def _create_virtual_environment(self):
-        def create_venv():
-            self._os_command(self.PYTHON, "-m", "venv", "venv")
-            self.PYTHON = os.path.join(
-                self._root_dir, "workspace", "venv", "Scripts", "python.exe"
-            )
-            self.PIP = os.path.join(
-                self._root_dir, "workspace", "venv", "Scripts", "pip.exe"
-            )
-
-        def upgrade_pip():
-            self._log("Upgrade pip")
-            self._os_command(self.PYTHON, "-m", "pip", "install", "--upgrade", "pip")
-
-        def install_packages():
-            packages = (
-                ("humblewx", None),
-                ("icalendar", None),
-                ("Markdown", None),
-                ("pyinstaller", None),
-                ("wxPython", "4.1.1"),
-            )
-            for package, version in packages:
-                self._pip_install(package, version)
-            self.PYINSTALLER = os.path.join(
-                self._root_dir, "workspace", "venv", "Scripts", "pyinstaller.exe"
-            )
-            self._log(self._os_command(self.PIP, "list"))
-
-        self._log_header("Create virtual environment")
-        create_venv()
-        upgrade_pip()
-        install_packages()
-
-    def _run_tests(self):
-        self._log_header("Run Tests")
-        self._os_command(
-            self.PYTHON, "tools/execute-specs.py", "--write-testlist", "testlist.txt"
-        )
-
-    def _generate_mo_file(self):
-        self._log_header("Generate mo files")
-        try:
-            self._cd(os.path.join(self._root_dir, "workspace", "tools"))
-            self._os_command(self.PYTHON, "generate-mo-files.py")
-        finally:
-            self._cd(os.path.join(self._root_dir, "workspace"))
-
-    def _modify_source_files(self):
-        self._log_header("Modify source files")
-        self._cd(os.path.join(self._root_dir, "workspace", "tools", "winbuildtools"))
-        self._os_command(self.PYTHON, "mod_paths.py", ".")
-        self._log("paths.py modified")
-        self._log(f"Revision: {self._build_version or self._hg_node}")
-        self._os_command(
-            self.PYTHON,
-            "mod_iss_timeline_version.py",
-            ".",
-            self._build_version or self._hg_node,
-        )
-        self._log("version.py modified")
-
-    def _create_timeline_executable(self):
-        self._log_header("Create Timeline executable")
-        self._cd(os.path.join(self._root_dir, "workspace", "tools", "winbuildtools"))
-        self._os_command(self.PYINSTALLER, "timeline.spec")
-
-    def _copy_files_to_dist(self):
-        self._log("Copying icons and translations to dist")
-        self._cd(os.path.join(self._root_dir, "workspace", "tools", "winbuildtools"))
-        self._create_dir(".\\dist\\icons")
-        self._create_dir(".\\dist\\icons\\event_icons")
-        self._create_dir(".\\dist\\translations")
-
-        self._os_command("xcopy", "/S", "..\\..\\icons\*.*", ".\\dist\icons\\*.*")
-        self._log(f"Icons copied")
-
-        for lang in (
-            "ca",
-            "de",
-            "el",
-            "es",
-            "eu",
-            "fr",
-            "gl",
-            "he",
-            "it",
-            "ko",
-            "lt",
-            "nl",
-            "pl",
-            "pt",
-            "pt_BR",
-            "ru",
-            "sv",
-            "tr",
-            "vi",
-            "zh_CN",
-        ):
-            self._os_command(
-                "xcopy",
-                "/S",
-                f"..\\..\\translations\\{lang}\\*.*",
-                f".\\dist\\translations\\{lang}\\*.*",
-            )
-            self._log(f"{lang} translations copied")
-
-    def _create_distrubtable(self):
-        self._log_header("Create distributable")
-        self._copy_files_to_dist()
-        self._cd(
-            os.path.join(self._root_dir, "workspace", "tools", "winbuildtools", "inno")
-        )
-        self._os_command("iscc.exe", "timeline2Win32.iss")
-
-    def _move_result(self):
-        self._log_header("Move result")
-        self._cd(
-            os.path.join(
-                self._root_dir, "workspace", "tools", "winbuildtools", "inno", "out"
-            )
-        )
-        self._os_command("xcopy", "/Y", "*.exe", f"{self._root_dir}\\*.*")
-        self._log("Resulting exe-file moved to root dir.")
-
-    def _log_header(self, text):
-        print("-" * 50)
-        print(f" {text}")
-        print("-" * 50)
-
-    def _log(self, text):
-        print(f"  {text}")
-
-    def _abort_message(self, ex):
-        self._log_header("Build aborted:")
-        self._log(ex)
-
-    def _clean_up(self):
-        self._log_header("Clean up")
-        self._cd("root")
-        if os.path.isdir("workspace"):
-            os.system("rmdir /S /Q workspace")
-            self._log("workspace directory removed")
-        else:
-            self._log("No workspace found")
-        if os.path.isdir("__pycache__"):
-            os.system("rmdir /S /Q __pycache__")
-            self._log("__pycache__ directory removed")
-
-    def run(self):
-        try:
-            self._clean_up()
-            self._validate_prerequisite()
-            self._create_workspace()
-            self._get_timeline_from_repo()
-            self._create_virtual_environment()
-            self._run_tests()
-            self._generate_mo_file()
-            self._modify_source_files()
-            self._create_timeline_executable()
-            self._create_distrubtable()
-            self._move_result()
-        except BuildError as ex:
-            self._abort_message(ex)
-        finally:
-            if not self._keep_workspace:
-                self._clean_up()
-
-
-def print_help():
-    print(
-        """
-Usage:
-
-    python build_win_setup_executable.py [options]
-
-Options:
-
-    -h, --help                          Print help
-    -p dirname, --python-home dirname   Path to dir where python.exe can be found. Default = c:\pgm\python396
-    -t tagname, --tag tagname           Tagged version to build. Default = latest code
-    -k, --keep-workspace                Kepp the workspace directory when finished
-
-        """
-    )
-
-
-if __name__ == "__main__":
-    DEFAULT_PYTHON_HOME = "c:\\pgm\\Python396"
-    specs = [
-        NamedArg(name="t", long_name="tag", has_value=True),
-        NamedArg(name="k", long_name="keep-workspace"),
-        NamedArg(name="p", long_name="python-home", has_value=True),
-        NamedArg(name="h", long_name="help"),
-    ]
-    parser = CmdLineParser(specs)
-    if parser.help:
-        print_help()
-    else:
-        build_version = parser.tag_value
-        python_home = (
-            parser.python_home_value
-            if parser.python_home_value
-            else DEFAULT_PYTHON_HOME
-        )
-        Build(
-            python_home=python_home,
-            build_version=build_version,
-            keep_workspace=parser.keep_workspace,
-        ).run()
diff -r 2d4564f72ba5 -r c6f07716d723 tools/winbuildtools/buildwinsetup.cmd
--- a/tools/winbuildtools/buildwinsetup.cmd Sat Jul 05 21:51:47 2025 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-@echo off
-
-echo --------------------------------------
-echo Revision: %1
-echo --------------------------------------
-
-
-echo --------------------------------------
-echo Remove old build and dist directories
-echo --------------------------------------
-rmdir /S /Q build >> nul
-rmdir /S /Q dist >> nul
-del /S /Q inno\out >> nul
-
-echo --------------------------------------
-echo Create translations
-echo --------------------------------------
-pushd ..
-python generate-mo-files.py
-popd
-
-echo --------------------------------------
-echo Modify paths.py
-echo --------------------------------------
-python mod_paths.py .
-
-echo --------------------------------------
-echo Modify version file and iss file
-echo --------------------------------------
-python mod_iss_timeline_version.py . %1
-
-echo --------------------------------------
-echo Building distribution
-echo --------------------------------------
-pyinstaller timeline.spec
-
-echo --------------------------------------
-echo Copying icons and translations to dist
-echo --------------------------------------
-mkdir .\dist\icons\event_icons
-mkdir .\dist\translations
-xcopy /S ..\..\icons\*.*  .\dist\icons\*.*
-xcopy /S ..\..\translations\ca\*.*  .\dist\translations\ca\*.*
-xcopy /S ..\..\translations\de\*.*  .\dist\translations\de\*.*
-xcopy /S ..\..\translations\el\*.*  .\dist\translations\el\*.*
-xcopy /S ..\..\translations\es\*.*  .\dist\translations\es\*.*
-xcopy /S ..\..\translations\eu\*.*  .\dist\translations\eu\*.*
-xcopy /S ..\..\translations\fr\*.*  .\dist\translations\fr\*.*
-xcopy /S ..\..\translations\gl\*.*  .\dist\translations\gl\*.*
-xcopy /S ..\..\translations\he\*.*  .\dist\translations\he\*.*
-xcopy /S ..\..\translations\it\*.*  .\dist\translations\it\*.*
-xcopy /S ..\..\translations\ko\*.*  .\dist\translations\ko\*.*
-xcopy /S ..\..\translations\lt\*.*  .\dist\translations\lt\*.*
-xcopy /S ..\..\translations\nl\*.*  .\dist\translations\nl\*.*
-xcopy /S ..\..\translations\pl\*.*  .\dist\translations\pl\*.*
-xcopy /S ..\..\translations\pt\*.*  .\dist\translations\pt\*.*
-xcopy /S ..\..\translations\pt_BR\*.*  .\dist\translations\pt_BR\*.*
-xcopy /S ..\..\translations\ru\*.*  .\dist\translations\ru\*.*
-xcopy /S ..\..\translations\sv\*.*  .\dist\translations\sv\*.*
-xcopy /S ..\..\translations\tr\*.*  .\dist\translations\tr\*.*
-xcopy /S ..\..\translations\vi\*.*  .\dist\translations\vi\*.*
-xcopy /S ..\..\translations\zh_CH\*.*  .\dist\translations\zh_CH\*.*
-
-
-echo --------------------------------------
-echo Create distributable
-echo --------------------------------------
-pushd inno
-iscc.exe timeline2Win32.iss
-popd
diff -r 2d4564f72ba5 -r c6f07716d723 tools/winbuildtools/cmdlineparser.py
--- a/tools/winbuildtools/cmdlineparser.py Sat Jul 05 21:51:47 2025 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,282 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
-#
-# This file is part of Timeline.
-#
-# Timeline 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.
-#
-# Timeline 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 Timeline.  If not, see <http://www.gnu.org/licenses/>.
-
-
-"""
-The CmdLineParser reads the command line input and stores the separate
-parts as properties, according to the given specification.
-
-A command line has two types of arguments
-    - Positional argument
-    - Named argument
-
-Arguments are separated with on or more spaces.
-
-A named argument is always prefixed with a dash (-) which a named argument isn't.
-
-A named argument has two types. One with a single dash prefix and one with a double
-dash prefix. The first one is intended for single character names and the second
-one for long names. Both can have a specified value.
-
-If a name contains a dash character, the attribute name of the parser will be the same
-but with the dash replaced with an underscore character.
-
-The name 'invalid-args' is reserved for the parser and cannot be used as a command line
-argument.
-
-By default, the input argument list is taken from sys.argv[1:], but it can be
-specified as an argument to the CmdLineParser constructor.
-
-Example:
-    cmd line:  pgm.exe -v --log-name mylog filename
-
-    pgm.exe             The name of the executed program
-    -v                  A named argument without a value
-    --log-name mylog    A named argument with the value mylog
-    filename            A positional argument
-
-Usage:
-    # Define valid arguments
-    specs = [
-        PositionalArg(name="filename"),
-        NamedArg(name="v", long_name="verbose"),
-        NamedArg(long_name="log-name", has_value=True),
-    ]
-
-    # Create the parser and process the command line input
-    parser = CmdLineParser(specs)
-
-    # Use input data
-    if parser.verbose:
-        print("Verbose mode")
-    print(f"logfile : {parser.log_name}")
-    print(f"filename: {parser.filename}")
-
-"""
-
-import sys
-
-
-class Arg:
-    def __init__(self, name="", long_name="", has_value=False):
-        self._name = name
-        self._long_name = long_name
-        self._has_value = has_value
-
-    @property
-    def name(self):
-        return self._name
-
-    @property
-    def long_name(self):
-        return self._long_name
-
-    @property
-    def has_value(self):
-        return self._has_value
-
-
-class PositionalArg(Arg):
-    def __init__(self, name):
-        Arg.__init__(self, name)
-
-
-class NamedArg(Arg):
-    def __init__(self, name="", long_name="", has_value=None):
-        Arg.__init__(self, name, long_name, has_value)
-
-
-class CmdLineParser:
-    """
-    No  arguments given on the command line
-
-    >>> sys.argv = ["pgm.exe"]
-    >>> specs = [
-    ... PositionalArg(name="filename"),
-    ... NamedArg(name="v", long_name="verbose"),
-    ... NamedArg(name="l", long_name="log-name", has_value=True),]
-    >>> parser = CmdLineParser(specs)
-    >>> parser.filename
-    False
-    >>> parser.log_name
-    False
-    >>> parser.l
-    False
-    >>> parser.verbose
-    False
-    >>> parser.v
-    False
-    >>> parser.invalid_args
-    []
-
-    Command line arguments given
-
-    >>> sys.argv = ["pgm.exe", "-v", "--log-name", "mylog", "myfile"]
-    >>> parser = CmdLineParser(specs)
-    >>> parser.filename
-    'myfile'
-    >>> parser.v and parser.verbose
-    True
-    >>> parser.log_name_value
-    'mylog'
-    >>> parser.l_value
-    'mylog'
-
-    Invalid Command line arguments given
-
-    >>> sys.argv = ["pgm.exe", "-vv", "--log-name", "mylog", "myfile"]
-    >>> parser = CmdLineParser(specs)
-    >>> len(parser.invalid_args)
-    1
-    >>> parser.invalid_args[0]
-    '-vv'
-
-    Same arg given twice
-
-    >>> sys.argv = ["pgm.exe", "--log-name", "mylog2", "--log-name", "mylog2", "myfile1", "myfile2"]
-    >>> parser = CmdLineParser(specs)
-
-    If named args are duplicated the last one is used
-
-    >>> parser.log_name_value
-    'mylog2'
-
-    Positional arguments are always assigned from left to right.
-
-    >>> parser.filename
-    'myfile1'
-    >>> parser.invalid_args[0]
-    'myfile2'
-
-    The 'name' agument is not mandatory for NamedArg
-
-    >>> sys.argv = ["pgm.exe", "--verbose", "myfile1"]
-    >>> specs = [
-    ... PositionalArg(name="filename"),
-    ... NamedArg(long_name="verbose")]
-    >>> parser = CmdLineParser(specs)
-    >>> parser.verbose
-    True
-    """
-
-    def __init__(self, specs, args=None):
-        self._positional_specs = [s for s in specs if isinstance(s, PositionalArg)]
-        self._named_specs = [s for s in specs if isinstance(s, NamedArg)]
-        self._args = args or sys.argv[1:]
-        self._invalid_args = []
-        self._positional_args = []
-        self._init_args()
-        self._parse()
-
-    @property
-    def invalid_args(self):
-        return self._invalid_args
-
-    def _init_args(self):
-        for spec in self._positional_specs:
-            spec.attr_name = self._attr_name(spec.name)
-            setattr(self, spec.attr_name, False)
-        for spec in self._named_specs:
-            self._create_attribute(spec)
-            self._set_attribute(spec, False)
-            self._set_attribute_value(spec, None)
-
-    def _create_attribute(self, spec):
-        if len(spec.name) > 0:
-            spec.attr_name = self._attr_name(spec.name)
-        if len(spec.long_name) > 0:
-            spec.attr_long_name = self._attr_name(spec.long_name)
-
-    def _set_attribute(self, spec, value):
-        if len(spec.name) > 0:
-            setattr(self, spec.attr_name, value)
-        if len(spec.long_name) > 0:
-            setattr(self, spec.attr_long_name, value)
-
-    def _set_attribute_value(self, spec, value):
-        if len(spec.name) > 0:
-            setattr(self, f"{spec.attr_name}_value", value)
-        if len(spec.long_name) > 0:
-            setattr(self, f"{spec.attr_long_name}_value", value)
-
-    def _attr_name(self, name):
-        return name.replace("-", "_")
-
-    @property
-    def _next_arg(self):
-        return self._args[0]
-
-    @property
-    def _more_args_exists(self):
-        return len(self._args) > 0
-
-    @property
-    def _next_arg_is_named(self):
-        return self._next_arg.startswith("-")
-
-    @property
-    def _next_arg_is_positional(self):
-        return not self._next_arg_is_named
-
-    def _remove_first_arg(self):
-        self._args = self._args[1:]
-
-    def _parse(self):
-        while self._more_args_exists:
-            if self._next_arg_is_named:
-                self._parse_named_arg()
-            else:
-                self._parse_positional_arg()
-
-    def _parse_named_arg(self):
-        spec = self._find_spec()
-        if spec is None:
-            self._invalid_args.append(self._next_arg)
-            self._remove_first_arg()
-        else:
-            self._set_attribute(spec, True)
-            self._remove_first_arg()
-            if spec.has_value and self._more_args_exists:
-                if self._next_arg_is_positional:
-                    self._set_attribute_value(spec, self._next_arg)
-                    self._remove_first_arg()
-
-    def _find_spec(self):
-        for spec in self._named_specs:
-            if (
-                f"-{spec.name}" == self._next_arg
-                or f"--{spec.long_name}" == self._next_arg
-            ):
-                return spec
-
-    def _parse_positional_arg(self):
-        index = len(self._positional_args)
-        try:
-            spec = self._positional_specs[index]
-            setattr(self, spec.attr_name, self._next_arg)
-            self._positional_args.append(self._next_arg)
-        except IndexError:
-            self._invalid_args.append(self._next_arg)
-        self._remove_first_arg()
-
-
-if __name__ == "__main__":
-    import doctest
-
-    doctest.testmod()
diff -r 2d4564f72ba5 -r c6f07716d723 tools/winbuildtools/jenkins-build-local.py
--- a/tools/winbuildtools/jenkins-build-local.py Sat Jul 05 21:51:47 2025 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,306 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
-#
-# This file is part of Timeline.
-#
-# Timeline 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.
-#
-# Timeline 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 Timeline.  If not, see <http://www.gnu.org/licenses/>.
-
-
-"""
-Our Jenkins server is about to be retired.
-
-That means that the Windows installation executable can no longer
-be built from Jenkins (calling a build slave).
-
-This script is intended to be run on the local Windows box that
-has the Jenkins slave installation to build the Windows installation
-executable file.
-
-"""
-
-import sys
-import os
-import subprocess
-
-
-class BuildError(Exception):
-    pass
-
-
-def get_build_version_from_input():
-    """
-    The first argument, if any, is the tagged version t obe built.
-    For beta releases, the hg rev number is used.
-
-    >>> get_build_version_from_input()
-
-    >>> sys.argv.insert(1, '2.11.0')
-    >>> get_build_version_from_input()
-    '2.11.0'
-
-    """
-    try:
-        return sys.argv[1]
-    except IndexError:
-        pass
-
-
-def is_windows():
-    return os.name == 'nt'
-
-
-def hg_command(*args):
-    """
-    >>> hg_command('showconfig', 'paths.default')
-    'http://hg.code.sf.net/p/thetimelineproj/main'
-
-    """
-    try:
-        if is_windows():
-            return subprocess.check_output(['hg'] + list(args)).decode('latin-1').strip()
-        else:
-            return subprocess.check_output(['hg'] + list(args)).decode().strip()
-    except Exception as ex:
-        raise BuildError(str(ex))
-
-
-def os_command(*args):
-    try:
-        if is_windows():
-            return subprocess.check_output(list(args)).decode('latin-1').strip()
-        else:
-            return subprocess.check_output(list(args)).decode().strip()
-    except Exception as ex:
-        raise BuildError(str(ex))
-
-
-def get_hg_default_path():
-    return hg_command('showconfig', 'paths.default')
-
-
-def hg_pull():
-    hg_command('pull', '--rev', 'default')
-
-
-def hg_update():
-    hg_command('update', '--clean', '--rev', 'default')
-
-
-def hg_purge():
-    hg_command('--config', 'extensions.purge=', 'clean', '--all')
-
-
-def get_hg_node():
-    return hg_command('log', '--rev', '.', '--template', '{node}')
-
-
-def get_hg_rev():
-    return hg_command('log', '--rev', '.', '--template', '{rev}')
-
-
-def verify_version(build_version, node):
-    """
-    >>> verify_version('2.10.0', 'a821cfd8cbe1f28ac4d01ffe02746632161db27b')
-    'exists'
-
-    >>> verify_version(None, 'a821cfd8cbe1f28ac4d01ffe02746632161db27b')
-    'exists'
-
-    >>> verify_version(None, 'abcd')
-    Traceback (most recent call last):
-        ...
-    jenkins-build-local.BuildError: unknown revision: abcd!
-    """
-    try:
-        return hg_command('log', '--rev', build_version or node, '--template', 'exists')
-    except:
-        raise BuildError(f'unknown revision: {build_version or node}!')
-
-
-def log_changesets(build_revision, node):
-    return hg_command(
-        'log',
-        '--template',
-        "<changeset node='{node}' author='{author|xmlescape}' rev='{rev}' date='{date}'><msg>{desc|xmlescape}</msg>{file_adds % '<addedFile>{file|xmlescape}</addedFile>'}{file_dels % '<deletedFile>{file|xmlescape}</deletedFile>'}{files % '<file>{file|xmlescape}</file>'}<parents>{parents}</parents></changeset>\n",
-        '--rev',
-        f"ancestors('default') and not ancestors({build_revision or node})",
-        '--encoding',
-        'UTF-8',
-        '--encodingmode',
-        'replace')
-
-
-def upgrade_pip():
-    os_command('python.exe', '-m', 'pip', 'install', '--upgrade', 'pip')
-
-
-def display_python_version():
-    log_subheader('Python Version')
-    log(os_command('python', '-V'))
-
-
-def create_virtual_environment():
-    log_subheader('Crate virtual environment')
-    log(os_command('python', '-m', 'venv', 'venv'))
-
-
-def upgrade_pip():
-    log_subheader('Upgrade pip')
-    os.system('venv\\Scripts\\python -m pip install --upgrade pip')
-
-
-def install_wxpython():
-    log_subheader('Install wwPython')
-    os_command('venv\\Scripts\\pip', 'install', '--force-reinstall', '-v', 'wxPython==4.1.1')
-
-
-def install_humblewx():
-    log_subheader('Install humblewx')
-    os_command('venv\Scripts\\pip', 'install', 'humblewx')
-
-
-def install_icalendar():
-    log_subheader('Install icalendar')
-    os_command('venv\\Scripts\\pip', 'install', 'icalendar')
-
-
-def install_markdown():
-    log_subheader('Install Markdown')
-    os_command('venv\\Scripts\\pip', 'install', 'Markdown')
-
-
-def run_tests():
-    log_subheader('Run Tests')
-    os_command('venv\\Scripts\\python', 'tools/execute-specs.py', '--write-testlist', 'testlist.txt')
-
-
-def build(build_version, node):
-    log_subheader(f'Start Build {build_version or node}')
-
-    os.chdir('tools')
-    os.system('..\\venv\\Scripts\python generate-mo-files.py')
-    log('mo file generated')
-
-    os.chdir('winbuildtools')
-    os.system('..\\..\\venv\\Scripts\python mod_paths.py .')
-    log('paths modified')
-
-    os.system(f'..\\..\\venv\\Scripts\python mod_iss_timeline_version.py . {build_version or node}')
-    log('version file modified')
-
-    log_subheader('Remove old build directory')
-    os.system('rmdir /S /Q build >> nul')
-    log_subheader('Remove old dist directory')
-    os.system('rmdir /S /Q dist >> nul')
-    log_subheader('Remove old out directory')
-    os.system('del /S /Q inno\out >> nul')
-
-    log_subheader('pyinstaller')
-    os.system('pyinstaller timeline.spec')
-
-    log_subheader('Copying icons and translations to dist')
-    os.system('mkdir .\\dist\\icons\\event_icons')
-    os.system('mkdir .\\dist\\translations')
-    os.system('xcopy /S ..\\..\\icons\*.*  .\dist\icons\*.*')
-    os.system('xcopy /S ..\\..\\translations\ca\*.*  .\dist\\translations\ca\*.*')
-    os.system('xcopy /S ..\\..\\translations\de\*.*  .\dist\\translations\de\*.*')
-    os.system('xcopy /S ..\\..\\translations\el\*.*  .\dist\\translations\el\*.*')
-    os.system('xcopy /S ..\\..\\translations\es\*.*  .\dist\\translations\es\*.*')
-    os.system('xcopy /S ..\\..\\translations\eu\*.*  .\dist\\translations\eu\*.*')
-    os.system('xcopy /S ..\\..\\translations\fr\*.*  .\dist\\translations\fr\*.*')
-    os.system('xcopy /S ..\\..\\translations\gl\*.*  .\dist\\translations\gl\*.*')
-    os.system('xcopy /S ..\\..\\translations\he\*.*  .\dist\\translations\he\*.*')
-    os.system('xcopy /S ..\\..\\translations\it\*.*  .\dist\\translations\it\*.*')
-    os.system('xcopy /S ..\\..\\translations\ko\*.*  .\dist\\translations\ko\*.*')
-    os.system('xcopy /S ..\\..\\translations\lt\*.*  .\dist\\translations\lt\*.*')
-    os.system('xcopy /S ..\\..\\translations\nl\*.*  .\dist\\translations\nl\*.*')
-    os.system('xcopy /S ..\\..\\translations\pl\*.*  .\dist\\translations\pl\*.*')
-    os.system('xcopy /S ..\\..\\translations\pt\*.*  .\dist\\translations\pt\*.*')
-    os.system('xcopy /S ..\\..\\translations\pt_BR\*.*  .\dist\\translations\pt_BR\*.*')
-    os.system('xcopy /S ..\\..\\translations\ru\*.*  .\dist\\translations\ru\*.*')
-    os.system('xcopy /S ..\\..\\translations\sv\*.*  .\dist\\translations\sv\*.*')
-    os.system('xcopy /S ..\\..\\translations\tr\*.*  .\dist\\translations\tr\*.*')
-    os.system('xcopy /S ..\\..\\translations\vi\*.*  .\dist\\translations\vi\*.*')
-    os.system('xcopy /S ..\\..\\translations\zh_CH\*.*  .\dist\\translations\zh_CH\*.*')
-
-    log('Create distributable')
-    os.chdir('inno')
-    os.system('iscc.exe timeline2Win32.iss')
-    log_subheader('DONE')
-
-
-def log_header():
-    print('-' * 70)
-    print(' jenkins-build-local.py')
-    print('-' * 70)
-
-
-def log_subheader(text):
-    print(f'----({text})------')
-
-
-def log(text):
-    print(f'  {text}')
-
-
-def main():
-    log_header()
-    hg_pull()
-    hg_update()
-    hg_purge()
-    build_version = get_build_version_from_input()
-    default_path = get_hg_default_path()
-    node = get_hg_node()
-    revision = get_hg_rev()
-    log(f'Build version: {build_version}')
-    log(f'Defult Path  : {default_path}')
-    log(f'Node         : {node}')
-    log(f'Revision     : {revision}')
-
-    log_subheader('Verify version')
-    verify_version(build_version, node)
-    log('Verified that revision exists')
-
-    log_subheader('Log changesets')
-    changesets = log_changesets(build_version, node)
-    print(changesets)
-
-    display_python_version()
-    create_virtual_environment()
-    upgrade_pip()
-
-    install_wxpython()
-    install_humblewx()
-    install_icalendar()
-    install_markdown()
-    run_tests()
-
-    os.system('venv\\Scripts\pip list')
-
-    build(build_version, node)
-
-
-if __name__ == '__main__':
-    if 'doctest' in sys.argv:
-        import doctest
-
-        sys.argv = sys.argv[:-1]
-        doctest.testmod()
-    else:
-        try:
-            main()
-        except BuildError as ex:
-            log('Build aborted')
-            log(ex)
diff -r 2d4564f72ba5 -r c6f07716d723 tools/winbuildtools/mod_iss_timeline_version.py
--- a/tools/winbuildtools/mod_iss_timeline_version.py Sat Jul 05 21:51:47 2025 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,129 +0,0 @@
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
-#
-# This file is part of Timeline.
-#
-# Timeline 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.
-#
-# Timeline 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 Timeline.  If not, see <http://www.gnu.org/licenses/>.
-
-
-import sys
-import os
-import subprocess
-
-
-USAGE = """
-    Usage:
-        python mod_iss_timeline_version.py   project-tools-dir   revision
-"""
-
-
-def get_hash_and_id(revision):
-    try:
-        return subprocess.check_output([
-            "hg", "id",
-            "-r", revision,
-        ]).decode("utf-8").strip().split(" ", 1)
-    except subprocess.CalledProcessError as e:
-        print("ERROR:", str(e))
-        raise
-
-
-def get_revision_date(revision):
-    try:
-        return subprocess.check_output([
-            "hg", "log",
-            "-r", revision,
-            "--template", "{date|shortdate}",
-        ]).decode("utf-8").strip()
-    except subprocess.CalledProcessError as e:
-        print("ERROR:", str(e))
-        raise
-
-
-def get_version(versionfile):
-    with open(versionfile, "r") as f:
-        text = f.read()
-        lines = text.split("\n")
-        for line in lines:
-            if line[0:7] == "VERSION":
-                break
-    line = line.split("(", 1)[1]
-    line = line.split(")", 1)[0]
-    major, minor, bug = line. split(", ")
-    app_ver_name = "Timeline %s.%s.%s" % (major, minor, bug)
-    revision = sys.argv[2]
-    print("Revision:", revision)
-    hash_value, rev_id = get_hash_and_id(sys.argv[2])
-    revision_date = get_revision_date(sys.argv[2])
-    if rev_id == 'tip':
-        output_base_filename = "timeline-%s.%s.%s-beta-%s-%s-Win32Setup" % (major, minor, bug, hash_value, revision_date)
-        type = 'TYPE_BETA'
-    else:
-        output_base_filename = "timeline-%s.%s.%s-Win32Setup" % (major, minor, bug)
-        type = 'TYPE_FINAL'
-    with open(versionfile, "r") as f:
-        text = f.read()
-    text = text.replace('TYPE = TYPE_DEV', f'TYPE = {type}')
-    text = text.replace('REVISION_HASH = ""', f'REVISION_HASH = "{hash_value}"')
-    text = text.replace('REVISION_DATE = ""', f'REVISION_DATE = "{revision_date}"')
-    with open(versionfile, "w") as f:
-        f.write(text)
-    print("[INFO] Version found: %s" % app_ver_name)
-    print("[INFO] Filename: %s" % output_base_filename)
-    return app_ver_name, output_base_filename
-
-
-def modify_iss_file(target, app_ver_name, output_base_filename):
-    with open(target, "r") as f:
-        text = f.read()
-    with open(target, "w") as f:
-        lines = text.split("\n")
-        for line in lines:
-            if line[0:11] == "AppVerName=":
-                line = "AppVerName=%s" % app_ver_name
-                f.write(line + "\n")
-                print("[INFO] Iss file version line: %s" % line)
-            elif line[0:19] == "OutputBaseFilename=":
-                line = "OutputBaseFilename=%s" % output_base_filename
-                f.write(line + "\n")
-                print("[INFO] Iss base filename line: %s" % line)
-            else:
-                f.write(line + "\n")
-
-
-def main():
-    project_dir = sys.argv[1]
-    target = os.path.join(project_dir, "inno", "timeline2Win32.iss")
-    versionfile_path = os.path.join(project_dir, "..", "..", "source", "timelinelib", "meta", "version.py")
-    print("Script: mod2_timeline_iss_win32.py")
-    print("Target:", target)
-    print("Version:", versionfile_path)
-    if not os.path.exists(target):
-        print("[ERROR] Can't find target file: %s" % target)
-        return
-    if not os.path.exists(versionfile_path):
-        print("[ERROR] Can't find version file: %s" % versionfile_path)
-        return
-    app_ver_name, output_base_filename = get_version(versionfile_path)
-    modify_iss_file(target, app_ver_name, output_base_filename)
-
-
-if __name__ == "__main__":
-    if len(sys.argv) != 3:
-        print(USAGE)
-    else:
-        if not os.path.exists(sys.argv[1]):
-            print(USAGE)
-            print("[ERROR] Can't find project root dir: %s" % sys.argv[1])
-        else:
-            main()
diff -r 2d4564f72ba5 -r c6f07716d723 tools/winbuildtools/mod_paths.py
--- a/tools/winbuildtools/mod_paths.py Sat Jul 05 21:51:47 2025 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
-#
-# This file is part of Timeline.
-#
-# Timeline 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.
-#
-# Timeline 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 Timeline.  If not, see <http://www.gnu.org/licenses/>.
-
-import sys
-import os
-
-
-def main():
-    project_dir = sys.argv[1]
-    path = os.path.join(
-        project_dir, "..", "..", "source", "timelinelib", "config", "paths.py"
-    )
-    text = None
-    with open(path) as f:
-        text = f.read()
-    collector = []
-    for line in text.split("\n"):
-        if line.strip().startswith("_ROOT ="):
-            line = "_ROOT = '.'"
-        collector.append(line.strip())
-    text = "\n".join(collector)
-    with open(path, "w") as f:
-        f.write(text)
-    print("paths.py modified")
-    print(text)
-
-
-main()

2025-07-05 21:51 Rickard pushed to timeline

changeset:   7987:2d4564f72ba5
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sat Jul 05 21:51:47 2025 +0200
summary:     Extract variables for Python version and wxPython version.

diff -r b21b1bd519b5 -r 2d4564f72ba5 Dockerfile.zip-to-exe.ci
--- a/Dockerfile.zip-to-exe.ci Sat Jul 05 18:53:16 2025 +0200
+++ b/Dockerfile.zip-to-exe.ci Sat Jul 05 21:51:47 2025 +0200
@@ -11,15 +11,18 @@
 RUN dnf install -y wget
 RUN dnf install -y xorg-x11-server-Xvfb
 
-RUN wget https://www.python.org/ftp/python/3.13.5/python-3.13.5-amd64.exe
+ENV PYTHON_VERSION=3.13.5
+ENV WXPYTHON_VERSION=4.2.3
+
+RUN wget https://www.python.org/ftp/python/$PYTHON_VERSION/python-$PYTHON_VERSION-amd64.exe
 RUN wget https://files.innosetup.nl/innosetup-6.4.3.exe
 
 # https://jrsoftware.org/ishelp/index.php?topic=setupcmdline
 
 RUN umask 0 && xvfb-run bash -c '\
-       wine python-3.13.5-amd64.exe /quiet TargetDir=$PYTHON_DIR \
+       wine python-$PYTHON_VERSION-amd64.exe /quiet TargetDir=$PYTHON_DIR \
     && wine $PYTHON_PIP_EXE install --no-warn-script-location pyinstaller \
-    && wine $PYTHON_PIP_EXE install --no-warn-script-location wxpython==4.2.3 \
+    && wine $PYTHON_PIP_EXE install --no-warn-script-location wxpython==$WXPYTHON_VERSION \
     && wine $PYTHON_PIP_EXE install --no-warn-script-location humblewx \
     && wine $PYTHON_PIP_EXE install --no-warn-script-location markdown \
     && wine $PYTHON_PIP_EXE install --no-warn-script-location icalendar \

2025-07-05 21:43 Rickard pushed to projects2

commit 70758ad39bbb716e61c84790f9af5463daedcf79
Author: Rickard Lindberg <rickard@rickardlindberg.me>
Date:   Sat Jul 5 21:43:23 2025 +0200

    Use cp instead of mv to get correct selinux context.

diff --git a/projects2.py b/projects2.py
index 403a421..c10bf24 100755
--- a/projects2.py
+++ b/projects2.py
@@ -507,7 +507,7 @@ class Projects2:
           name: '/opt/rlprojects/web/artifacts/timeline/1.0.0'
           user: 'scm'
           group: 'scm'
-        RUN => ['mv', '/opt/rlprojects/tmp/tmp123/file.exe', '/opt/rlprojects/web/artifacts/timeline/1.0.0/file.exe']
+        RUN => ['cp', '/opt/rlprojects/tmp/tmp123/file.exe', '/opt/rlprojects/web/artifacts/timeline/1.0.0/file.exe']
         ADD_FILE_EVENT =>
           path: '/opt/rlprojects/events/-timeline-1/meta.json'
           file: 'timeline/1.0.0/file.exe'
@@ -1286,7 +1286,7 @@ server {{
           name: '/opt/rlprojects/web/artifacts/test-project'
           user: 'scm'
           group: 'scm'
-        RUN => ['mv', '/opt/rlprojects/tmp/tmp123/repo/target/file-xyz.zip', '/opt/rlprojects/web/artifacts/test-project/file-xyz.zip']
+        RUN => ['cp', '/opt/rlprojects/tmp/tmp123/repo/target/file-xyz.zip', '/opt/rlprojects/web/artifacts/test-project/file-xyz.zip']
         ADD_FILE_EVENT =>
           path: '/opt/rlprojects/events/-test-project-1/meta.json'
           file: 'test-project/file-xyz.zip'
@@ -1484,7 +1484,7 @@ server {{
             user=self.config.SSH_USER,
             group=self.config.SSH_USER,
         )
-        self.process.ensure(["mv", source, absolute_destination])
+        self.process.ensure(["cp", source, absolute_destination])
         self.event_service.add_file_to_event(
             path=event_path,
             file=relpath(

2025-07-05 19:54 Rickard pushed to timeline

changeset:   7981:e97ea1bdd985
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Fri Jul 04 17:23:47 2025 +0200
summary:     Only clean pyc files once.

diff -r 4ab1b37abf3c -r e97ea1bdd985 tools/timelinetools/packaging/archive.py
--- a/tools/timelinetools/packaging/archive.py Thu Jul 03 15:13:50 2025 +0200
+++ b/tools/timelinetools/packaging/archive.py Fri Jul 04 17:23:47 2025 +0200
@@ -58,7 +58,7 @@
         self._run_tool("execute-specs.py")
 
     def create_zip_archive(self):
-        self._clean_pyc_files()
+        self.clean_pyc_files()
         zip_name = "%s.zip" % self.get_basename()
         subprocess.check_call([
             "zip",
@@ -69,6 +69,12 @@
         ], cwd=self.get_dirname())
         return timelinetools.packaging.zipfile.ZipFile(self.get_dirname(), zip_name)
 
+    def clean_pyc_files(self):
+        for root, dirs, files in os.walk(self.get_path()):
+            for f in files:
+                if f.endswith(".pyc"):
+                    os.remove(os.path.join(root, f))
+
     def _run_tool(self, tool):
         run_python_script_and_exit_if_fails(
             os.path.join(
@@ -80,13 +86,6 @@
 
     def _change_version_constant(self, constant, value):
         _change_constant(self._get_version_path(), constant, value)
-        self._clean_pyc_files()
-
-    def _clean_pyc_files(self):
-        for root, dirs, files in os.walk(self.get_path()):
-            for f in files:
-                if f.endswith(".pyc"):
-                    os.remove(os.path.join(root, f))
 
     def _get_readme_path(self):
         return os.path.join(self.get_path(), "README")

changeset:   7986:b21b1bd519b5
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sat Jul 05 18:53:16 2025 +0200
summary:     Attempte to modify exe build scripts to work on build server.

diff -r 4c7bbdb609ee -r b21b1bd519b5 Dockerfile.zip-to-exe.ci
--- a/Dockerfile.zip-to-exe.ci Fri Jul 04 19:55:58 2025 +0200
+++ b/Dockerfile.zip-to-exe.ci Sat Jul 05 18:53:16 2025 +0200
@@ -1,34 +1,29 @@
 FROM fedora:41
 
+ENV WINEDEBUG=-all
+ENV WINEPREFIX=/opt/wine
+ENV PYTHON_DIR=C:\\Python
+ENV PYTHON_PIP_EXE=C:\\Python\\Scripts\\pip.exe
+ENV INNO_DIR=C:\\Inno
+
 RUN dnf update -y
 RUN dnf install -y wine
-RUN dnf install -y winetricks
 RUN dnf install -y wget
-
-RUN wget https://www.python.org/ftp/python/3.13.5/python-3.13.5-amd64.exe
-
 RUN dnf install -y xorg-x11-server-Xvfb
 
-ENV WINEPREFIX=/opt/wine
-ENV PYTHON_ROOT=/opt/wine/drive_c/users/root/AppData/Local/Programs/Python/Python313
-
-RUN xvfb-run bash -c "wine python-3.13.5-amd64.exe /quiet && wineserver -w"
-
-RUN xvfb-run bash -c "wine $PYTHON_ROOT/python.exe --version; wineserver -w"
-
-RUN xvfb-run bash -c "wine $PYTHON_ROOT/Scripts/pip.exe install --no-warn-script-location pyinstaller; wineserver -w"
-
-RUN xvfb-run bash -c "wine $PYTHON_ROOT/Scripts/pip.exe install --no-warn-script-location wxpython==4.2.3; wineserver -w"
-
-RUN xvfb-run bash -c "wine $PYTHON_ROOT/Scripts/pip.exe install --no-warn-script-location humblewx; wineserver -w"
-
-RUN xvfb-run bash -c "wine $PYTHON_ROOT/Scripts/pip.exe install --no-warn-script-location markdown; wineserver -w"
-
-RUN xvfb-run bash -c "wine $PYTHON_ROOT/Scripts/pip.exe install --no-warn-script-location icalendar; wineserver -w"
-
+RUN wget https://www.python.org/ftp/python/3.13.5/python-3.13.5-amd64.exe
 RUN wget https://files.innosetup.nl/innosetup-6.4.3.exe
 
 # https://jrsoftware.org/ishelp/index.php?topic=setupcmdline
-RUN xvfb-run bash -c "wine innosetup-6.4.3.exe /SP- /VERYSILENT /SUPPRESSMSGBOXES && wineserver -w"
+
+RUN umask 0 && xvfb-run bash -c '\
+       wine python-3.13.5-amd64.exe /quiet TargetDir=$PYTHON_DIR \
+    && wine $PYTHON_PIP_EXE install --no-warn-script-location pyinstaller \
+    && wine $PYTHON_PIP_EXE install --no-warn-script-location wxpython==4.2.3 \
+    && wine $PYTHON_PIP_EXE install --no-warn-script-location humblewx \
+    && wine $PYTHON_PIP_EXE install --no-warn-script-location markdown \
+    && wine $PYTHON_PIP_EXE install --no-warn-script-location icalendar \
+    && wine innosetup-6.4.3.exe /SP- /VERYSILENT /SUPPRESSMSGBOXES /DIR=$INNO_DIR \
+    && wineserver -w'
 
 CMD ["xvfb-run", "python", "tools/build-windows-exe-from-source-zip.py", "--output-list", "Dockerfile.zip-to-exe.ci.files"]
diff -r 4c7bbdb609ee -r b21b1bd519b5 tools/build-windows-exe-from-source-zip.py
--- a/tools/build-windows-exe-from-source-zip.py Fri Jul 04 19:55:58 2025 +0200
+++ b/tools/build-windows-exe-from-source-zip.py Sat Jul 05 18:53:16 2025 +0200
@@ -18,6 +18,7 @@
 # along with Timeline.  If not, see <http://www.gnu.org/licenses/>.
 
 
+from os import environ
 from os.path import basename, join
 import argparse
 import glob
@@ -69,7 +70,7 @@
     archive.clean_pyc_files()
     subprocess.check_call([
         "wine",
-        "/opt/wineprefix/drive_c/users/root/AppData/Local/Programs/Python/Python313/Scripts/pyinstaller.exe",
+        f"{environ['PYTHON_DIR']}\\Scripts\\pyinstaller.exe",
         "timeline.spec",
     ], cwd=archive.get_path("tools/winbuildtools"))
     subprocess.check_call([
@@ -85,7 +86,7 @@
     ])
     subprocess.check_call([
         "wine",
-        "/root/.wine/drive_c/Program Files (x86)/Inno Setup 6/ISCC.exe",
+        f"{environ['INNO_DIR']}\\ISCC.exe",
         "inno/timeline2Win32.iss",
     ], cwd=archive.get_path("tools/winbuildtools"))
     subprocess.check_call([

2025-07-05 19:53 Rickard pushed to projects2

commit 81a44eb20ad6d76ffabd6ad5b2980e6b46dafa8d
Author: Rickard Lindberg <rickard@rickardlindberg.me>
Date:   Sat Jul 5 19:53:30 2025 +0200

    Allow builds to take 10 minutes.
    
    Building timeline exe takes longer than one minute.

diff --git a/projects2.py b/projects2.py
index 06bd382..403a421 100755
--- a/projects2.py
+++ b/projects2.py
@@ -1276,7 +1276,7 @@ server {{
         LOG => 'Building Dockerfile.ci...'
         RUN => ['podman', 'build', '-f', 'Dockerfile.ci', '-q', '.']
         LOG => 'Running Dockerfile.ci...'
-        RUN => ['timeout', '-k', '5s', '1m', 'podman', 'run', '--init', '--rm', '-v', '/opt/rlprojects/tmp/tmp123/repo:/repo:z', '-w', '/repo', '']
+        RUN => ['timeout', '-k', '5s', '10m', 'podman', 'run', '--init', '--rm', '-v', '/opt/rlprojects/tmp/tmp123/repo:/repo:z', '-w', '/repo', '']
         ENSURE_FOLDER =>
           name: '/opt/rlprojects/web/test-project'
           user: 'scm'
@@ -1391,7 +1391,7 @@ server {{
             ]).stdout.strip()
             self.logger.log(f"Running {basename(dockerfile)}...")
             self.process.ensure([
-                "timeout", "-k", "5s", "1m",
+                "timeout", "-k", "5s", "10m",
                 "podman", "run",
                 "--init",
                 "--rm",

commit 17032719cba5040d8fef8b5013e6e5027c2f3cfb
Author: Rickard Lindberg <rickard@rickardlindberg.me>
Date:   Sat Jul 5 19:52:23 2025 +0200

    Use podman instead of docker.
    
    Mainly because it can run without root, and that using wine containers
    easier.

diff --git a/projects2.py b/projects2.py
index 042167a..06bd382 100755
--- a/projects2.py
+++ b/projects2.py
@@ -886,9 +886,7 @@ class Projects2:
         LOG => 'Setting up tools...'
         RUN => ['dnf', 'install', '-y', 'git', 'mercurial', 'tar', 'rsync']
         LOG => 'Setting up CI...'
-        RUN => ['dnf', 'install', '-y', 'docker-cli']
-        RUN => ['systemctl', 'enable', 'docker']
-        RUN => ['systemctl', 'start', 'docker']
+        RUN => ['dnf', 'install', '-y', 'podman']
         LOG => 'Setting up timezone...'
         RUN => ['timedatectl', 'set-timezone', 'Europe/Stockholm']
         LOG => 'Configuring project git-project...'
@@ -1035,9 +1033,7 @@ server {{
         self.logger.log(f"Setting up tools...")
         self.process.ensure(["dnf", "install", "-y", "git", "mercurial", "tar", "rsync"])
         self.logger.log(f"Setting up CI...")
-        self.process.ensure(["dnf", "install", "-y", "docker-cli"])
-        self.process.ensure(["systemctl", "enable", "docker"])
-        self.process.ensure(["systemctl", "start", "docker"])
+        self.process.ensure(["dnf", "install", "-y", "podman"])
         self.logger.log(f"Setting up timezone...")
         self.process.ensure(["timedatectl", "set-timezone", self.config.TIMEZONE])
         for project in self.list_projects():
@@ -1277,12 +1273,10 @@ server {{
             new: '858b34e097c8563a60db02b19582136ad23057ae'
             user_id: None
             date: '2025-01-01T12:00:00+00:00'
-        RUN => ['id', '-u']
-        RUN => ['id', '-g']
         LOG => 'Building Dockerfile.ci...'
-        RUN => ['sudo', 'docker', 'build', '-f', 'Dockerfile.ci', '-q', '.']
+        RUN => ['podman', 'build', '-f', 'Dockerfile.ci', '-q', '.']
         LOG => 'Running Dockerfile.ci...'
-        RUN => ['sudo', 'timeout', '-k', '5s', '1m', 'docker', 'run', '--init', '--rm', '-v', '/opt/rlprojects/tmp/tmp123/repo:/repo:z', '-w', '/repo', '--user', ':', '']
+        RUN => ['timeout', '-k', '5s', '1m', 'podman', 'run', '--init', '--rm', '-v', '/opt/rlprojects/tmp/tmp123/repo:/repo:z', '-w', '/repo', '']
         ENSURE_FOLDER =>
           name: '/opt/rlprojects/web/test-project'
           user: 'scm'
@@ -1322,7 +1316,7 @@ server {{
                     tar_path = join(tmp, "archive.tar")
                     self.process.ensure(["git", "archive", "--format", "tar", "-o", tar_path, new])
                     self.process.ensure(["tar", "-x", "-f", tar_path, "-C", repo_path])
-                    self.run_docker_ci(repo_path, old, new)
+                    self.run_ci(repo_path, old, new)
 
     def pretxnchangegroup(self):
         """
@@ -1375,9 +1369,9 @@ server {{
             old = self.env.get("HG_NODE")
             new = self.env.get("HG_NODE_LAST")
             self.process.ensure(["hg", "clone", "-u", new, ".", tmp], capture=False)
-            self.run_docker_ci(tmp, old, new)
+            self.run_ci(tmp, old, new)
 
-    def run_docker_ci(self, repo_path, old, new):
+    def run_ci(self, repo_path, old, new):
         project = self.env.get(self.config.PROJECT_NAME_ENV_VAR)
         event_path = self.event_service.write(path=self.config.EVENTS_ROOT, project=project, event={
             "project": project,
@@ -1387,12 +1381,9 @@ server {{
             "date": self.clock.isonow(),
         })
         for dockerfile in self.filesystem.ls(f"{repo_path}/Dockerfile*.ci"):
-            user_id = self.process.ensure(['id', '-u']).stdout.strip()
-            group_id = self.process.ensure(['id', '-g']).stdout.strip()
             self.logger.log(f"Building {basename(dockerfile)}...")
             image = self.process.ensure([
-                "sudo",
-                "docker",
+                "podman",
                 "build",
                 "-f", dockerfile,
                 "-q",
@@ -1400,14 +1391,12 @@ server {{
             ]).stdout.strip()
             self.logger.log(f"Running {basename(dockerfile)}...")
             self.process.ensure([
-                "sudo",
                 "timeout", "-k", "5s", "1m",
-                "docker", "run",
+                "podman", "run",
                 "--init",
                 "--rm",
                 "-v", f"{repo_path}:/repo:z",
                 "-w", "/repo",
-                "--user", f"{user_id}:{group_id}",
                 image
             ], capture=False)
         site_root = self.config.get_site_root(project)

2025-07-05 19:44 Rickard pushed to timeline

changeset:   7981:e97ea1bdd985
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Fri Jul 04 17:23:47 2025 +0200
summary:     Only clean pyc files once.

diff -r 4ab1b37abf3c -r e97ea1bdd985 tools/timelinetools/packaging/archive.py
--- a/tools/timelinetools/packaging/archive.py Thu Jul 03 15:13:50 2025 +0200
+++ b/tools/timelinetools/packaging/archive.py Fri Jul 04 17:23:47 2025 +0200
@@ -58,7 +58,7 @@
         self._run_tool("execute-specs.py")
 
     def create_zip_archive(self):
-        self._clean_pyc_files()
+        self.clean_pyc_files()
         zip_name = "%s.zip" % self.get_basename()
         subprocess.check_call([
             "zip",
@@ -69,6 +69,12 @@
         ], cwd=self.get_dirname())
         return timelinetools.packaging.zipfile.ZipFile(self.get_dirname(), zip_name)
 
+    def clean_pyc_files(self):
+        for root, dirs, files in os.walk(self.get_path()):
+            for f in files:
+                if f.endswith(".pyc"):
+                    os.remove(os.path.join(root, f))
+
     def _run_tool(self, tool):
         run_python_script_and_exit_if_fails(
             os.path.join(
@@ -80,13 +86,6 @@
 
     def _change_version_constant(self, constant, value):
         _change_constant(self._get_version_path(), constant, value)
-        self._clean_pyc_files()
-
-    def _clean_pyc_files(self):
-        for root, dirs, files in os.walk(self.get_path()):
-            for f in files:
-                if f.endswith(".pyc"):
-                    os.remove(os.path.join(root, f))
 
     def _get_readme_path(self):
         return os.path.join(self.get_path(), "README")

changeset:   7986:b21b1bd519b5
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sat Jul 05 18:53:16 2025 +0200
summary:     Attempte to modify exe build scripts to work on build server.

diff -r 4c7bbdb609ee -r b21b1bd519b5 Dockerfile.zip-to-exe.ci
--- a/Dockerfile.zip-to-exe.ci Fri Jul 04 19:55:58 2025 +0200
+++ b/Dockerfile.zip-to-exe.ci Sat Jul 05 18:53:16 2025 +0200
@@ -1,34 +1,29 @@
 FROM fedora:41
 
+ENV WINEDEBUG=-all
+ENV WINEPREFIX=/opt/wine
+ENV PYTHON_DIR=C:\\Python
+ENV PYTHON_PIP_EXE=C:\\Python\\Scripts\\pip.exe
+ENV INNO_DIR=C:\\Inno
+
 RUN dnf update -y
 RUN dnf install -y wine
-RUN dnf install -y winetricks
 RUN dnf install -y wget
-
-RUN wget https://www.python.org/ftp/python/3.13.5/python-3.13.5-amd64.exe
-
 RUN dnf install -y xorg-x11-server-Xvfb
 
-ENV WINEPREFIX=/opt/wine
-ENV PYTHON_ROOT=/opt/wine/drive_c/users/root/AppData/Local/Programs/Python/Python313
-
-RUN xvfb-run bash -c "wine python-3.13.5-amd64.exe /quiet && wineserver -w"
-
-RUN xvfb-run bash -c "wine $PYTHON_ROOT/python.exe --version; wineserver -w"
-
-RUN xvfb-run bash -c "wine $PYTHON_ROOT/Scripts/pip.exe install --no-warn-script-location pyinstaller; wineserver -w"
-
-RUN xvfb-run bash -c "wine $PYTHON_ROOT/Scripts/pip.exe install --no-warn-script-location wxpython==4.2.3; wineserver -w"
-
-RUN xvfb-run bash -c "wine $PYTHON_ROOT/Scripts/pip.exe install --no-warn-script-location humblewx; wineserver -w"
-
-RUN xvfb-run bash -c "wine $PYTHON_ROOT/Scripts/pip.exe install --no-warn-script-location markdown; wineserver -w"
-
-RUN xvfb-run bash -c "wine $PYTHON_ROOT/Scripts/pip.exe install --no-warn-script-location icalendar; wineserver -w"
-
+RUN wget https://www.python.org/ftp/python/3.13.5/python-3.13.5-amd64.exe
 RUN wget https://files.innosetup.nl/innosetup-6.4.3.exe
 
 # https://jrsoftware.org/ishelp/index.php?topic=setupcmdline
-RUN xvfb-run bash -c "wine innosetup-6.4.3.exe /SP- /VERYSILENT /SUPPRESSMSGBOXES && wineserver -w"
+
+RUN umask 0 && xvfb-run bash -c '\
+       wine python-3.13.5-amd64.exe /quiet TargetDir=$PYTHON_DIR \
+    && wine $PYTHON_PIP_EXE install --no-warn-script-location pyinstaller \
+    && wine $PYTHON_PIP_EXE install --no-warn-script-location wxpython==4.2.3 \
+    && wine $PYTHON_PIP_EXE install --no-warn-script-location humblewx \
+    && wine $PYTHON_PIP_EXE install --no-warn-script-location markdown \
+    && wine $PYTHON_PIP_EXE install --no-warn-script-location icalendar \
+    && wine innosetup-6.4.3.exe /SP- /VERYSILENT /SUPPRESSMSGBOXES /DIR=$INNO_DIR \
+    && wineserver -w'
 
 CMD ["xvfb-run", "python", "tools/build-windows-exe-from-source-zip.py", "--output-list", "Dockerfile.zip-to-exe.ci.files"]
diff -r 4c7bbdb609ee -r b21b1bd519b5 tools/build-windows-exe-from-source-zip.py
--- a/tools/build-windows-exe-from-source-zip.py Fri Jul 04 19:55:58 2025 +0200
+++ b/tools/build-windows-exe-from-source-zip.py Sat Jul 05 18:53:16 2025 +0200
@@ -18,6 +18,7 @@
 # along with Timeline.  If not, see <http://www.gnu.org/licenses/>.
 
 
+from os import environ
 from os.path import basename, join
 import argparse
 import glob
@@ -69,7 +70,7 @@
     archive.clean_pyc_files()
     subprocess.check_call([
         "wine",
-        "/opt/wineprefix/drive_c/users/root/AppData/Local/Programs/Python/Python313/Scripts/pyinstaller.exe",
+        f"{environ['PYTHON_DIR']}\\Scripts\\pyinstaller.exe",
         "timeline.spec",
     ], cwd=archive.get_path("tools/winbuildtools"))
     subprocess.check_call([
@@ -85,7 +86,7 @@
     ])
     subprocess.check_call([
         "wine",
-        "/root/.wine/drive_c/Program Files (x86)/Inno Setup 6/ISCC.exe",
+        f"{environ['INNO_DIR']}\\ISCC.exe",
         "inno/timeline2Win32.iss",
     ], cwd=archive.get_path("tools/winbuildtools"))
     subprocess.check_call([