FreeCAD WebAssembly development snapshot
Some checks failed
FreeCAD master CI / Prepare (push) Has been cancelled
FreeCAD master CI / Pixi (push) Has been cancelled
FreeCAD master CI / Ubuntu (push) Has been cancelled
FreeCAD master CI / Windows (push) Has been cancelled
FreeCAD master CI / Lint (push) Has been cancelled
FreeCAD master CI / WrapUp (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Stale Issues / stale (push) Has been cancelled

This commit is contained in:
2026-08-20 07:00:53 +08:00
commit b5a086fda5
12718 changed files with 7841410 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
FROM archlinux:base
COPY arch.sh /tmp
RUN pacman --sync --refresh --sysupgrade --noconfirm && \
sh /tmp/arch.sh && \
mkdir /builds
WORKDIR /builds
VOLUME [ "/builds" ]

View File

@@ -0,0 +1,18 @@
FROM ubuntu:22.04
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates \
curl \
&& \
rm -rf /var/lib/apt/lists/*
# install Miniforge3
RUN curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh" && \
bash Miniforge3-$(uname)-$(uname -m).sh -b -p /opt/miniforge3 && \
rm "Miniforge3-$(uname)-$(uname -m).sh" && \
/opt/miniforge3/bin/conda init
WORKDIR /builds
VOLUME [ "/builds" ]

View File

@@ -0,0 +1,27 @@
FROM debian:12
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Etc/UTC
ENV UTC=true
ENV ARC=false
COPY debian.sh /tmp
RUN apt-get update && \
apt-get upgrade --yes && \
sh /tmp/debian.sh && \
mkdir /builds
# Debian 12 (bookworm) contains opencascade 7.6.x which contains deprecated constructs
# (inheriting from std::iterator) which have been fixed in 7.7.x
# lets bring the changes here to have less warnings during the build
# https://github.com/Open-Cascade-SAS/OCCT/commit/8af9bbd59aecf24c765e7ea0eeeb8c9dd5c1f8db
RUN apt-get install curl --yes
RUN curl --silent --location --output-dir /usr/include/opencascade/ \
--remote-name https://github.com/Open-Cascade-SAS/OCCT/raw/8af9bbd59aecf24c765e7ea0eeeb8c9dd5c1f8db/src/NCollection/NCollection_StlIterator.hxx --next \
--remote-name https://github.com/Open-Cascade-SAS/OCCT/raw/8af9bbd59aecf24c765e7ea0eeeb8c9dd5c1f8db/src/OSD/OSD_Parallel.hxx
WORKDIR /builds
VOLUME [ "/builds" ]

View File

@@ -0,0 +1,11 @@
FROM fedora:latest
COPY fedora.sh /tmp
RUN dnf update --assumeyes && \
sh /tmp/fedora.sh && \
mkdir /builds
WORKDIR /builds
VOLUME [ "/builds" ]

View File

@@ -0,0 +1,11 @@
FROM manjarolinux/base
COPY manjaro.sh /tmp
RUN pacman --sync --refresh --sysupgrade --noconfirm && \
sh /tmp/manjaro.sh && \
mkdir /builds
WORKDIR /builds
VOLUME [ "/builds" ]

View File

@@ -0,0 +1,17 @@
FROM ubuntu:rolling
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Etc/UTC
ENV UTC=true
ENV ARC=false
COPY ubuntu.sh /tmp
RUN apt-get update && \
apt-get upgrade --yes && \
sh /tmp/ubuntu.sh && \
mkdir -p /builds
WORKDIR /builds
VOLUME [ "/builds" ]

View File

@@ -0,0 +1,17 @@
FROM ubuntu:rolling
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Etc/UTC
ENV UTC=true
ENV ARC=false
COPY ubuntuQt6.sh /tmp
RUN apt-get update && \
apt-get upgrade --yes && \
sh /tmp/ubuntuQt6.sh && \
mkdir -p /builds
WORKDIR /builds
VOLUME [ "/builds" ]

View File

@@ -0,0 +1,26 @@
========================
FreeCAD build with Conda
========================
Container
=========
Build and run the `freecad-conda` container with the following commands:
.. code-block:: console
docker build --file tools/build/Docker/Dockerfile.Conda --tag freecad-conda .
docker run --rm --interactive --tty --volume $(pwd):/builds:z freecad-conda
Build Code
==========
Within the `freecad-conda` container, install the build dependencies and build
FreeCAD using the following commands:
.. code-block:: console
conda/setup-environment.sh
conda activate freecad
cmake --preset conda-linux-debug
cmake --build build/debug

View File

@@ -0,0 +1,136 @@
==========================
FreeCAD build dependencies
==========================
Distributions
=============
The following commands are used to install the packages needed to build FreeCAD from source on the following distributions.
Arch Linux
----------
The following command is used to install the required packages used to compile FreeCAD on Arch Linux.
.. code-block:: console
sudo sh tools/build/Docker/arch.sh
Debian
------
The following command is used to install the required packages used to compile FreeCAD on Debian.
.. code-block:: console
sudo sh tools/build/Docker/debian.sh
Fedora
------
The following command is used to install the required packages used to compile FreeCAD on Fedora.
.. code-block:: console
sudo sh tools/build/Docker/fedora.sh
Manjaro
-------
The following command is used to install the required packages used to compile FreeCAD on Manjaro Linux.
.. code-block:: console
sudo sh tools/build/Docker/manjaro.sh
Ubuntu
------
The following command is used to install the required packages used to compile FreeCAD on Ubuntu Linux.
.. code-block:: console
sudo sh tools/build/Docker/ubuntu.sh
Containers
==========
The following will create containers that have all the required dependencies
pre-installed that are needed to build FreeCAD from source.
Arch Linux
-----------
The following commands are used to create and run a Arch Linux build environment.
.. code-block:: console
docker build --file tools/build/Docker/Dockerfile.Arch --tag freecad-arch
docker run --rm --interactive --tty --volume $(pwd):/builds:z freecad-arch
Debian
------
The following commands are used to create and run a Debian build environment.
.. code-block:: console
docker build --file tools/build/Docker/Dockerfile.Debian --tag freecad-debian
docker run --rm --interactive --tty --volume $(pwd):/builds:z freecad-debian
Fedora
------
The following commands are used to create and run a Fedora build environment.
.. code-block:: console
docker build --file tools/build/Docker/Dockerfile.Fedora --tag freecad-fedora
docker run --rm --interactive --tty --volume $(pwd):/builds:z freecad-fedora
Manjaro
-------
The following commands are used to create and run a Manjaro build environment.
.. code-block:: console
docker build --file tools/build/Docker/Dockerfile.Manjaro --tag freecad-manjaro
docker run --rm --interactive --tty --volume $(pwd):/builds:z freecad-manjaro
Ubuntu
------
The following commands are used to create and run a Ubuntu build environment.
.. code-block:: console
docker build --file tools/build/Docker/Dockerfile.Ubuntu --tag freecad-ubuntu
docker run --rm --interactive --tty --volume $(pwd):/builds:z freecad-ubuntu
Build Code
==========
To build the FreeCAD code inside one of the running containers the following
commands should be used
.. code-block:: console
mkdir freecad-build
cd freecad-build
cmake ../freecad-source
make -j$(nproc --ignore=2)

View File

@@ -0,0 +1,7 @@
#!/bin/sh
pacman --noconfirm --sync boost boost-libs cmake coin curl desktop-file-utils doxygen \
eigen gc gcc-fortran git glew gnu-free-fonts guile hicolor-icon-theme jsoncpp \
libspnav libtool make med opencascade openmpi pyside2 pyside2-tools \
python-matplotlib python-netcdf4 python-pivy qt5-svg qt5-tools \
shared-mime-info shiboken2 swig texinfo xerces-c

16
tools/build/Docker/debian.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
apt-get install --no-install-recommends --yes build-essential cmake doxygen \
git libboost-date-time-dev libboost-dev libboost-filesystem-dev \
libboost-graph-dev libboost-iostreams-dev libboost-program-options-dev \
libboost-python-dev libboost-regex-dev libboost-serialization-dev \
libboost-thread-dev libcoin-dev libeigen3-dev libgtkglext1-dev \
libkdtree++-dev libkml-dev libmedc-dev libocct-data-exchange-dev \
libocct-draw-dev libocct-foundation-dev libocct-modeling-algorithms-dev \
libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev \
libopencv-dev libproj-dev libpyside2-dev libqt5svg5-dev \
libshiboken2-dev libvtk9-dev libvtk9-qt-dev \
libvtk-dicom-dev libx11-dev libxerces-c-dev libxmu-dev libxmuu-dev \
libzipios++-dev netgen netgen-headers pyside2-tools python3-dev \
python3-matplotlib python3-pivy python3-ply python3-pyside2.qtsvg \
python3-pyside2.qtuitools qtchooser qttools5-dev shiboken2 swig \
python3-pyside2.qtnetwork libyaml-cpp-dev

View File

@@ -0,0 +1,9 @@
#!/bin/sh
dnf --assumeyes install boost-devel cmake Coin3 Coin3-devel desktop-file-utils doxygen \
eigen3-devel freeimage-devel freetype freetype-devel gcc gettext git libspnav-devel \
libXmu-devel med med-devel mesa-libGLU-devel ode-devel opencascade-devel \
opencv-devel openmpi-devel pcl-devel pyside2-tools python3 python3-devel \
python3-matplotlib python3-pivy python3-pyside2-devel qt5-qtsvg-devel \
qt5-qttools-static qt-devel \
smesh-devel SoQt-devel swig tbb-devel vtk xerces-c xerces-c-devel

7
tools/build/Docker/manjaro.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
pacman --noconfirm --sync boost boost-libs cmake coin curl desktop-file-utils doxygen \
eigen gc gcc-fortran git glew gnu-free-fonts guile hicolor-icon-theme jsoncpp \
libspnav libtool make med opencascade pyside2 pyside2-tools python-matplotlib \
python-netcdf4 python-pivy qt5-svg qt5-tools \
shared-mime-info shiboken2 swig texinfo xerces-c

View File

@@ -0,0 +1,16 @@
#!/bin/sh
apt-get install --no-install-recommends --yes build-essential cmake doxygen git \
libboost-date-time-dev libboost-dev libboost-filesystem-dev \
libboost-graph-dev libboost-iostreams-dev libboost-program-options-dev \
libboost-python-dev libboost-regex-dev libboost-serialization-dev \
libboost-thread-dev libcoin-dev libeigen3-dev libgtkglext1-dev \
libkdtree++-dev libkml-dev libmedc-dev libocct-data-exchange-dev \
libocct-draw-dev libocct-foundation-dev libocct-modeling-algorithms-dev \
libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev \
libopencv-dev libproj-dev libpyside2-dev libqt5svg5-dev\
libshiboken2-dev libvtk9-dev libvtk9-qt-dev \
libvtk-dicom-dev libx11-dev libxerces-c-dev libxmu-dev libxmuu-dev \
libyaml-cpp-dev libzipios++-dev netgen netgen-headers pyside2-tools python3-dev \
python3-matplotlib python3-pivy python3-ply python3-pyside2.qtsvg \
python3-pyside2.qtuitools qtchooser qttools5-dev shiboken2 swig \
qtwayland5 python3-pyside2.qtnetwork python3-yaml

View File

@@ -0,0 +1,20 @@
#!/bin/sh
apt-get install --no-install-recommends --yes build-essential cmake doxygen git \
libtool lsb-release \
python3 swig \
libboost-dev libboost-date-time-dev libboost-filesystem-dev libboost-graph-dev libboost-iostreams-dev \
libboost-program-options-dev libboost-python-dev libboost-regex-dev libboost-serialization-dev libboost-thread-dev \
libcoin-dev \
libeigen3-dev libgts-bin libgts-dev libkdtree++-dev libmedc-dev libopencv-dev libproj-dev libvtk9-dev \
libx11-dev libxerces-c-dev libyaml-cpp-dev libzipios++-dev libpcl-dev python3-yaml \
\
pybind11-dev qt6-base-dev qt6-svg-dev qt6-tools-dev qt6-webengine-dev libpyside6-dev libshiboken6-dev pyside6-tools \
pyqt6-dev-tools python3-dev python3-matplotlib python3-packaging python3-pivy python3-ply python3-pyside6.qtcore \
python3-pyside6.qtgui python3-pyside6.qtnetwork python3-pyside6.qtsvg python3-pyside6.qtwebchannel \
python3-pyside6.qtwebenginecore python3-pyside6.qtwebenginequick python3-pyside6.qtwebenginewidgets python3-pyside6.qtwidgets \
\
libocct-data-exchange-dev libocct-draw-dev libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev \
libocct-ocaf-dev libocct-visualization-dev occt-draw \
\
libsimage-dev doxygen libcoin-doc libspnav-dev checkinstall

View File

@@ -0,0 +1,299 @@
# Contents
<a name="contents"></a>
- [Building FreeCAD on Mac OS 10.15.x -- Catalina](#build-freecad-macos-catalina)
- [Directions](#directions)
- [Install Xcode Command line tools](#install-xcode-cli-tools)
- [Install Conda](#install-conda)
- [Run the shell script](#run-the-shell-script)
- [Building FreeCAD on macOS using homebrew packages with & without formula file](#homebrew-build-fc-on-macos)
- [Requirements](#homebrew-requirements)
- [Install required FreeCAD dependencies](#homebrew-install-required-deps)
- [Limitations of using freecad formula file](#homebrew-limits-of-formula-file)
- [Directions, Installing FreeCAD using brew packages without formula file](#homebrew-install-no-form-file)
- [Expanded Directions](#homebrew-expanded-directions)
- [Boost v1.75 fix](#homebrew-boost-175-fix)
- [Errors, Issues, & Possible Solutions](#errors-issues-solutions)
<!-- SEE: https://stackoverflow.com/a/7335259/708807 for explanation of using `name` attribute for in page linking within a MD doc -->
# Building FreeCAD on Mac OS 10.15.x -- Catalina #
<a name="build-freecad-macos-catalina"></a>
General notes on how the tooling works:
This setup uses [conda](https://docs.conda.io) for dependency management.
Conda is able to pull the deps from a repository called conda-forge and
setup an isolated build environment. Not quite as isolated as docker, but
it is a good option for Mac and is what the FreeCAD CI system uses.
Once the dependencies are installed into a conda environment, then the
build uses the standard `cmake` configuration process to configure the build
and `ninja` to invoke the actual compilation against whatever host compiler
(gcc, clang, etc.) was installed by conda's `cxx-compiler` package for
that architecture.
All of this, and some sanity checks, are in a unified shell script. See below.
# Directions
<a name="directions"></a>
## Install XCode Command line tools ##
<a name="install-xcode-cli-tools"></a>
Run `xcode-select --install` and click through.
## Install Conda ##
<a name="install-conda"></a>
Refer to [MiniConda Docs](https://docs.conda.io/en/latest/miniconda.html).
## Run the shell script ##
<a name="run-the-shell-script"></a>
Run the `./build_unix_dev_conda.sh` and go get coffee. Builds take
an hour+ on a 2012 Retina MacBook.
Output binaries will be in the `./build/bin/FreeCAD` *and*
`${CONDA_PREFIX}/bin/FreeCAD` directories.
You can code/build/test using the cmake configuration folder `./build` in
the standard way *from within the freecad_dev conda environment*.
---
# Building FreeCAD on macOS using homebrew with & without a formula file
<a name="homebrew-build-fc-on-macos"></a>
> The below procedure provides an alternative way to install FreeCAD from the git source on macOS without having to use conda, but rather relies on [**mac homebrew**][lnk1] to manage dependencies.
## Requirements
<a name="homebrew-requirements"></a>
- macOS, running High Sierra _10.13_ or later
- homebrew installed and working
- All required dependencies to build FreeCAD installed using `brew install`
There is an official [**homebrew tap**][lnk2] that provides a list of formulas along with FreeCAD to setup all the dependencies to build FreeCAD from source on macOS, and also provides prebuilt bottles to install FreeCAD from a package rather than building from source.
> 💡 The below steps will build a FreeCAD binary that will launch FreeCAD from a command line interface, and will **NOT** build the **FreeCAD.app** bundle, ie. a double clickable app icon that can be launched from a Finder window.
### Install required FreeCAD dependencies
<a name="homebrew-install-required-deps"></a>
- Setup homebrew to use the official [**freecad-homebrew**][lnk2] tap.
```shell
brew tap FreeCAD/freecad
```
- Install FreeCAD dependencies provided by the tap
```shell
brew install --only-dependencies freecad
```
> The above step will install FreeCAD dependencies provided by the tap, and if a _bottle_ is provided by the tap homebrew will install the bottled version of the dep rather than building from source, unless the `install` command explicitly uses a _flag_ to build from source.
After all the dependencies have been installed, it should be possible to install FreeCAD from the provided bottle.
```shell
brew install freecad/freecad/freecad
```
> As of writing this, there are bottles provided for macOS Catalina and Big Sur
> > If running a different version of macOS then building FreeCAD from source will be required.
To explicitly build FreeCAD from source using the formula file provided by the tap
```shell
brew install freecad/freecad/freecad --build-from-source --HEAD --verbose
```
The above command will grab the latest git source of FreeCAD and output the build process to the terminal.
> NOTE: On a MacBookPro 2013 late model it takes ~60 minutes to build FreeCAD from source.
After the _make_ and _make install_ process completes it should be possible to launch FreeCAD from any directory using a terminal with the below commands,
```shell
FreeCAD
FreeCADCmd
```
- `FreeCAD` will launch a GUI version of FreeCAD
- `FreeCADCmd` will launch **only** a command line version of FreeCAD
## Limitations of using the FreeCAD formula file
<a name="homebrew-limits-of-formula-file"></a>
If FreeCAD is installed via the bottle then one will have to wait for a new bottle to be generated to install a later version of FreeCAD. However, if FreeCAD is built from source, then FreeCAD will have all the updates up to the time the build process was started.
If any of the dependencies FreeCAD relies on is updated FreeCAD will likely require a rebuild. Mac homebrew does provide a feature to pin packages at specific versions to prevent them from updating, and also allows setting of an environment variable to prevent homebrew from automatically checking for updates (which can slow things down). All that said, FreeCAD can be built using all the dependencies provided by Mac homebrew, but not using the formula file: instead cloning the source to an arbitrary path on a local filesystem. This provides a couple of advantages:
- If `brew cleanup` is run and FreeCAD was installed using the above-provided command, all source tarballs or bottles that were _checked out_ or downloaded during the install process will be deleted from the system. If a reinstall or upgrade is later required then homebrew will have to refetch the bottles, or reclone the git source again.
- Mac homebrew provides a method, _install flag_, for keeping the source regardless if the build succeeds or fails. The options are limited, however, and performing a standard `git clone` outside of homebrew is **much** preferred.
- Cloning the FreeCAD source allows passing **any** cmake flags not provided by the formula file
- Allowing the use of other build systems such as _ninja_
- Allowing the use of alternate compilers, e.g. _ccache_
- Pulling in subsequent updates are quicker because the `git clone` of the FreeCAD source will remain on the local filesystem even if a `brew cleanup` is run
- Subsequent recompiles should not take 60 minutes if using a caching strategy such as _ccache_.
## Directions, Installing FreeCAD using brew packages without a formula file
<a name="homebrew-install-no-form-file"></a>
> ⚠️ The below directions assume macOS High Sierra or later is being used, homebrew is setup properly, and all dependencies were installed successfully.
**TL;DR**
- Clone the FreeCAD source, pass cmake args/flags within source dir, run make, and make install, then profit 💰
### Expanded Directions
<a name="homebrew-expanded-directions"></a>
- Clone the FreeCAD source from GitHub
```shell
git clone https://github.com/freecad/freecad
cd ./freecad
git fetch
```
> The above _fetch_ cmd will take some time to fetch the commit history for the repo, but if a shallow clone is performed then FreeCAD will not show to correct build number in the About dialog [**learn more**][lnk3].
Advanced users may alter the process below to build in a different location, use a different compiler, etc. but these instructions represent a procedure that works successfully for this author.
Set the path / environment variables for specifying the compilers to use
```
export CC="/usr/local/opt/llvm/bin/clang"
export CXX="/usr/local/opt/llvm/bin/clang++"
```
- Linking the brew-provided install of python 3 will be required in order for cmake to find the proper python and python libraries.
```shell
brew link python@3.9
```
#### Boost v1.75 fix
<a name="homebrew-boost-175-fix"></a>
- Due to recent changes in boost v1.75, building FreeCAD will fail with the below linking error message (for a more exhaustive error message, [**learn more**][lnk4])
To work around the linking issue until the [**PR**][lnk5] is merged install boost will the patches applied within the PR.
```shell
ld: library not found for -licudata
```
```shell
git checkout -b mybuild
cmake \
-DCMAKE_C_FLAGS_RELEASE=-DNDEBUG \
-DCMAKE_CXX_FLAGS_RELEASE=-DNDEBUG \
-DCMAKE_INSTALL_PREFIX=/opt/beta/freecad \
-DCMAKE_INSTALL_LIBDIR=lib \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_FIND_FRAMEWORK=LAST
-DCMAKE_VERBOSE_MAKEFILE=ON \
-Wno-dev \
-DCMAKE_OSX_SYSROOT=/Library/Developer/CommandLineTools/SDKs/macOSX10.14.sdk \
-std=c++14 \
-DCMAKE_CXX_STANDARD=14 \
-DBUILD_ENABLE_CXX_STD:STRING=C++14 \
-Wno-deprecated-declarations \
-DUSE_PYTHON3=1 -DPYTHON_EXECUTABLE=/usr/local/bin/python3 \
-DBUILD_FEM_NETGEN=1 \
-DBUILD_FEM=1 \
-DBUILD_TECHDRAW=0 \
-DFREECAD_USE_EXTERNAL_KDL=ON \
-DFREECAD_CREATE_MAC_APP=OFF \
-DCMAKE_PREFIX_PATH="/usr/local/opt/qt/lib/cmake;/usr/local/opt/nglib/Contents/Resources;/usr/local/opt/vtk@8.2/lib/cmake;/usr/local;" .
```
After the configuration completes run the below commands to start the build & install process
```shell
make
make install
```
> 💡 Author's note: The above cmake build flags are the ones I've had good luck with, but that's not to say other ones can be added or removed. And for reasons unknown to me the above build process takes ~ twice along than using `brew install --build-from-source`
If everything goes well FreeCAD should be able to launch from a terminal
## Errors and Issues + possible solutions
<a name="errors-issues-solutions"></a>
Some common pitfalls are listed in this section.
---
<details>
<summary><strong>error:</strong> no member named </summary>
```shell
[ 18%] Building CXX object src/Gui/CMakeFiles/FreeCADGui.dir/DlgProjectInformationImp.cpp.o
cd /opt/code/github/public/forks/freecad/build/src/Gui && /usr/local/bin/ccache /usr/local/opt/llvm/bin/clang++ -DBOOST_ALL_NO_LIB -DBOOST_FILESYSTEM_DYN_LINK -DBOOST_PP_VARIADICS=1 -DBOOST_PROGRAM_OPTIONS_DYN_LINK -DBOOST_REGEX_DYN_LINK -DBOOST_SYSTEM_DYN_LINK -DBOOST_THREAD_DYN_LINK -DBUILD_ADDONMGR -DCMAKE_BUILD_TYPE=\"Release\" -DFreeCADGui_EXPORTS -DGL_SILENCE_DEPRECATION -DHAVE_CONFIG_H -DHAVE_FREEIMAGE -DHAVE_PYSIDE2 -DHAVE_RAPIDJSON -DHAVE_SHIBOKEN2 -DHAVE_TBB -DNDEBUG -DOCC_CONVERT_SIGNALS -DPYSIDE_QML_SUPPORT=1 -DQT_CORE_LIB -DQT_GUI_LIB -DQT_NETWORK_LIB -DQT_NO_DEBUG -DQT_OPENGL_LIB -DQT_PRINTSUPPORT_LIB -DQT_SVG_LIB -DQT_UITOOLS_LIB -DQT_WIDGETS_LIB -DQT_XML_LIB -D_OCC64 -I/opt/code/github/public/forks/freecad/build -I/opt/code/github/public/forks/freecad/build/src -I/opt/code/github/public/forks/freecad/src -I/opt/code/github/public/forks/freecad/src/Gui -I/opt/code/github/public/forks/freecad/src/Gui/Quarter -I/opt/code/github/public/forks/freecad/build/src/Gui -I/opt/code/github/public/forks/freecad/src/Gui/.. -I/opt/code/github/public/forks/freecad/build/src/Gui/.. -I/opt/code/github/public/forks/freecad/build/src/Gui/Language -I/opt/code/github/public/forks/freecad/build/src/Gui/propertyeditor -I/opt/code/github/public/forks/freecad/build/src/Gui/TaskView -I/opt/code/github/public/forks/freecad/build/src/Gui/Quarter -I/opt/code/github/public/forks/freecad/build/src/Gui/DAGView -I/usr/local/include/eigen3 -I/usr/local/include/PySide2/QtCore -I/usr/local/include/PySide2/QtGui -I/usr/local/include/PySide2/QtWidgets -isystem /usr/local/include -isystem /usr/local/Frameworks/Python.framework/Versions/3.9/include/python3.9 -iframework /usr/local/opt/qt/lib -isystem /usr/local/opt/qt/lib/QtCore.framework/Headers -isystem /usr/local/opt/qt/./mkspecs/macx-clang -isystem /usr/local/opt/qt/lib/QtWidgets.framework/Headers -isystem /usr/local/opt/qt/lib/QtGui.framework/Headers -isystem /Library/Developer/CommandLineTools/SDKs/macOSX10.14.sdk/System/Library/Frameworks/OpenGL.framework/Headers -isystem /usr/local/opt/qt/lib/QtOpenGL.framework/Headers -isystem /usr/local/opt/qt/lib/QtPrintSupport.framework/Headers -isystem /usr/local/opt/qt/lib/QtSvg.framework/Headers -isystem /usr/local/opt/qt/lib/QtNetwork.framework/Headers -isystem /usr/local/opt/qt/include -isystem /usr/local/opt/qt/include/QtUiTools -isystem /usr/local/include/shiboken2 -isystem /usr/local/include/PySide2 -isystem /usr/local/opt/qt/lib/QtXml.framework/Headers -Wall -Wextra -Wpedantic -Wno-write-strings -Wno-undefined-var-template -DNDEBUG -isysroot /Library/Developer/CommandLineTools/SDKs/macOSX10.14.sdk -fPIC -I/usr/local/Cellar/open-mpi/4.0.5/include -fPIC -std=gnu++14 -o CMakeFiles/FreeCADGui.dir/DlgProjectInformationImp.cpp.o -c /opt/code/github/public/forks/freecad/src/Gui/DlgProjectInformationImp.cpp
/opt/code/github/public/forks/freecad/src/Gui/DlgProjectInformationImp.cpp:56:9: error: no member named 'lineEditProgramVersion' in 'Gui::Dialog::Ui_DlgProjectInformation'
ui->lineEditProgramVersion->setText(QString::fromUtf8(doc->getProgramVersion()));
~~ ^
1 error generated.
make[2]: *** [src/Gui/CMakeFiles/FreeCADGui.dir/DlgProjectInformationImp.cpp.o] Error 1
make[1]: *** [src/Gui/CMakeFiles/FreeCADGui.dir/all] Error 2
make: *** [all] Error 2
```
</details>
FreeCAD may fail to build if creating a build directory within the _src_ directory and running `cmake ..` within the newly created _build_ directory. As it currently stands, `cmake` needs run within the _src_ directory or the this error message will likely appear during the build process.
---
<details>
<summary> ⚠️ <strong>warning:</strong> specified path differs in case from file name on disk</summary>
```
/opt/code/github/public/forks/FreeCAD/src/Gui/moc_DlgParameterFind.cpp:10:10: warning: non-portable path to file '"../../../FreeCAD/src/Gui/DlgParameterFind.h"'; specified path differs in case from file name on disk [-Wnonportable-include-path]
#include "../../../freecad/src/Gui/DlgParameterFind.h"
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"../../../FreeCAD/src/Gui/DlgParameterFind.h"
1 warning generated.
```
</details>
On macOS most filesystems are case **insensitive**, whereas most GNU+Linux distros use _case sensitive_ file systems. So if FreeCAD source is cloned within a `FreeCAD` directory the build process on macOS may look for a `freecad` that is _case sensitive_ however the file system isn't case sensitive, thus the compiler will provide the above warning message.
One way to resolve such error message is to rename `FreeCAD` to `freecad`
```shell
mv FreeCAD freecadd;
mv freecadd freecad;
```
---
<!-- links -->
[lnk1]: <http://brew.sh>
[lnk2]: <https://github.com/FreeCAD/homebrew-freecad>
[lnk3]: <https://forum.freecad.org/viewtopic.php?f=4&t=51981#p446796>
[lnk4]: <https://gist.github.com/ipatch/6116824ab1f2a99b526cb07e43317b91#gistcomment-3577066>
[lnk5]: <https://github.com/Homebrew/homebrew-core/pull/67615>

103
tools/lint/black.py Executable file
View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python3
import argparse
import sys
import os
import re
import logging
from typing import Tuple
from utils import (
run_command,
init_environment,
append_file,
emit_problem_matchers,
write_file,
add_common_arguments,
)
DEFAULT_LINE_LENGTH_LIMIT = 100
def parse_black_output(output: str) -> Tuple[int, int]:
"""
Parse Black output to determine how many files would be reformatted and how many would fail.
Returns:
A tuple (black_reformats, black_fails).
"""
black_reformats = 0
black_fails = 0
# Look for a pattern like: "X files would be reformatted"
reformats_matches = re.findall(r"(\d+)(?=\s+fil.+ would be reformatted)", output)
if reformats_matches:
black_reformats = int(reformats_matches[0])
# Look for a pattern like: "Y files would fail to reformat"
fails_matches = re.findall(r"(\d+)(?=\s+fil.+ would fail to reformat)", output)
if fails_matches:
black_fails = int(fails_matches[0])
return black_reformats, black_fails
def generate_markdown_report(black_reformats: int, black_fails: int, log_file: str) -> str:
"""Generate a Markdown report section based on Black results and log file."""
report_lines = []
if black_reformats > 0 or black_fails > 0:
report_lines.append(
f"<details><summary>:pencil2: Black would reformat {black_reformats} file(s)"
f" and {black_fails} file(s) would fail to reformat</summary>"
)
else:
report_lines.append(
"<details><summary>:heavy_check_mark: Black would reformat no file</summary>"
)
report_lines.append("")
report_lines.append("````")
with open(log_file, "r", encoding="utf-8") as f:
report_lines.append(f.read())
report_lines.append("````")
report_lines.append("</details>")
report_lines.append("")
return "\n".join(report_lines)
def main():
parser = argparse.ArgumentParser(
description="Run Black in check mode on Python files and append a Markdown report."
)
add_common_arguments(parser)
args = parser.parse_args()
init_environment(args)
logging.info("Installing black (if needed)...")
run_command(["pipx", "install", "-q", "black"], check=True)
cmd = [
"black",
"--line-length",
str(DEFAULT_LINE_LENGTH_LIMIT),
"--check",
] + args.files.split()
stdout, stderr, exit_code = run_command(cmd, check=False)
output = stdout + "\n" + stderr
log_file = os.path.join(args.log_dir, "black.log")
write_file(log_file, output)
emit_problem_matchers(log_file, "blackWarning.json", "black-warning")
black_reformats, black_fails = parse_black_output(output)
logging.info(
"Found %s files would be reformatted and %s files would fail to reformat",
black_reformats,
black_fails,
)
report = generate_markdown_report(black_reformats, black_fails, log_file)
append_file(args.report_file, report)
sys.exit(0 if exit_code == 0 else 1)
if __name__ == "__main__":
main()

169
tools/lint/changed_lines.py Normal file
View File

@@ -0,0 +1,169 @@
#!/usr/bin/env python3
# Modified to generate output compatible with `clang-tidy`'s `--line-filter` option
#
# Based on https://github.com/hestonhoffman/changed-lines/blob/main/main.py
#
# Original License
#
# The MIT License (MIT)
#
# Copyright (c) 2023 Heston Hoffman
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
Uses GitHub API to grab patch data for a PR and calculate changed lines
"""
import argparse
import json
import os
import re
import requests
class MissingPatchData(Exception):
"""Raised when the patch data is missing"""
def fetch_patch(args):
"""Grabs the patch data from the GitHub API."""
git_session = requests.Session()
headers = {
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
}
if args.token:
headers["Authorization"] = f"Bearer {args.token}"
git_request = git_session.get(
f"{args.api_url}/repos/{args.repo}/pulls/{args.pr}/files", headers=headers
)
return git_request.json()
def parse_patch_file(entry):
"""Parses the individual file changes within a patch"""
line_array = []
sublist = []
patch_array = re.split("\n", entry["patch"])
# clean patch array
patch_array = [i for i in patch_array if i]
for item in patch_array:
# Grabs hunk annotation and strips out added lines
if item.startswith("@@ -"):
if sublist:
line_array.append(sublist)
sublist = [re.sub(r"\s@@(.*)", "", item.split("+")[1])]
# We don't need removed lines ('-')
elif not item.startswith("-") and not item == "\\ No newline at end of file":
sublist.append(item)
if sublist:
line_array.append(sublist)
return line_array
def parse_patch_data(patch_data):
"""Takes the patch data and returns a dictionary of files and the lines"""
final_dict = {}
for entry in patch_data:
# We don't need removed files
if entry["status"] == "removed":
continue
# We can only operate on files with additions and a patch key
# Some really big files don't have a patch key because GitHub
# returns a message in the PR that the file is too large to display
if entry["additions"] != 0 and "patch" in entry:
line_array = parse_patch_file(entry)
final_dict[entry["filename"]] = line_array
return final_dict
def get_lines(line_dict):
"""Takes the dictionary of files and lines and returns a dictionary of files and line numbers"""
final_dict = {}
for file_name, sublist in line_dict.items():
line_array = []
for array in sublist:
line_number = 0
if "," not in array[0]:
line_number = int(array[0]) - 1
else:
line_number = int(array[0].split(",")[0]) - 1
start = -1
end = -1
for line in array:
if line.startswith("+"):
if start < 0:
start = line_number
end = line_number
# line_array.append(line_number)
line_number += 1
line_array.append([start, end])
# Remove deleted/renamed files (which appear as empty arrays)
if line_array:
final_dict[file_name] = line_array
return final_dict
def main():
"""main()"""
parser = argparse.ArgumentParser(
prog="changed_lines.py",
description="Identifies the changed files and lines in a GitHub PR.",
)
parser.add_argument("--token")
parser.add_argument("--api-url", default="https://api.github.com")
parser.add_argument("--repo", default="FreeCAD/FreeCAD")
parser.add_argument("--ref", required=True)
parser.add_argument("--pr", required=True)
parser.add_argument("--file-filter", default="")
args = parser.parse_args()
data = fetch_patch(args)
added_line_data = parse_patch_data(data)
added_lines = get_lines(added_line_data)
if args.file_filter:
args.file_filter = set(args.file_filter.replace(" ", "").split(","))
filename_list = []
line_filter = []
for filename, _ in added_lines.items():
if (not args.file_filter) or (
os.path.splitext(filename)[1] in args.file_filter
):
filename_list.append(filename)
lines_modified = {}
lines_modified["name"] = filename
lines_modified["lines"] = added_lines[filename]
line_filter.append(lines_modified)
print(f"{json.dumps(line_filter)}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python3
import argparse
import sys
import os
import re
import logging
from utils import (
run_command,
init_environment,
append_file,
emit_problem_matchers,
write_file,
add_common_arguments,
)
def count_clang_format_errors(log_text: str) -> int:
"""
Count lines that indicate clang-format violations.
The pattern looks for lines ending with "[-Wclang-format-violations]".
"""
pattern = r"\[-Wclang-format-violations\]$"
matches = re.findall(pattern, log_text, re.MULTILINE)
logging.debug("Matches found: %s", matches)
return len(matches)
def generate_markdown_report(clang_format_errors: int, output: str) -> str:
"""Generate a Markdown report section for the clang-format output."""
report_lines = []
if clang_format_errors > 0:
report_lines.append(
f"<details><summary>:pencil2: Clang-format would reformat"
f" {clang_format_errors} file(s)</summary>"
)
else:
report_lines.append(
"<details><summary>:heavy_check_mark: Clang-format would reformat no file</summary>"
)
report_lines.append("")
report_lines.append("````")
report_lines.append(output)
report_lines.append("````")
report_lines.append("</details>")
report_lines.append("")
return "\n".join(report_lines)
def main():
parser = argparse.ArgumentParser(
description="Run clang-format in dry-run mode on C++ files and append a Markdown report."
)
add_common_arguments(parser)
parser.add_argument(
"--clang-style",
required=True,
help="Clang-format style (e.g., 'file' to use .clang-format or a specific style).",
)
args = parser.parse_args()
init_environment(args)
cmd = (
f"clang-format --dry-run --ferror-limit=1 --verbose "
f"--style={args.clang_style} {args.files}"
)
stdout, stderr, _ = run_command(cmd)
output = stdout + "\n" + stderr
log_file = os.path.join(args.log_dir, "clang-format.log")
write_file(log_file, output)
emit_problem_matchers(log_file, "clang.json", "clang")
clang_format_errors = count_clang_format_errors(output)
logging.info("Found %s clang-format errors", clang_format_errors)
report = generate_markdown_report(clang_format_errors, output)
append_file(args.report_file, report)
sys.exit(0 if clang_format_errors == 0 else 1)
if __name__ == "__main__":
main()

140
tools/lint/clang_tidy.py Normal file
View File

@@ -0,0 +1,140 @@
#!/usr/bin/env python3
import argparse
import sys
import os
import re
import logging
from utils import (
run_command,
init_environment,
add_common_arguments,
emit_problem_matchers,
write_file,
append_file,
)
def generate_markdown_report(
errors_count: int,
warnings_count: int,
notes_count: int,
clang_tidy_output: str,
enabled_checks_content: str,
) -> str:
"""Generate a Markdown report section for clang-tidy results and enabled checks."""
report_lines = []
if errors_count > 0:
report_lines.append(
f"<details><summary>:fire: Clang-Tidy found :fire: {errors_count} errors, "
f":warning: {warnings_count} warnings and :pencil2: {notes_count} notes</summary>"
)
elif warnings_count > 0:
report_lines.append(
f"<details><summary>:warning: Clang-Tidy found :warning: {warnings_count} "
f"warnings and :pencil2: {notes_count} notes</summary>"
)
elif notes_count > 0:
report_lines.append(
f"<details><summary>:pencil2: Clang-Tidy found :pencil2: {notes_count} notes</summary>"
)
else:
report_lines.append(
"<details><summary>:heavy_check_mark: Clang-Tidy found no errors, warnings or notes</summary>"
)
report_lines.append("")
report_lines.append("````")
report_lines.append(clang_tidy_output)
report_lines.append("````")
report_lines.append("</details>")
report_lines.append("")
report_lines.append(
"<details><summary>:information_source: Enabled checks</summary>"
)
report_lines.append("")
report_lines.append("````")
report_lines.append(enabled_checks_content)
report_lines.append("````")
report_lines.append("</details>")
report_lines.append("")
return "\n".join(report_lines)
def count_occurrences(pattern: str, text: str) -> int:
"""Count all occurrences of a regex pattern in text using MULTILINE mode."""
matches = re.findall(pattern, text, re.MULTILINE)
logging.info("Pattern '%s' found %d matches", pattern, len(matches))
return len(matches)
def main():
parser = argparse.ArgumentParser(
description="Run clang-tidy on provided C++ files and append a Markdown report."
)
add_common_arguments(parser)
parser.add_argument(
"--clang-style",
required=True,
help="Clang-format style (e.g., 'file' to use .clang-format or a specific style).",
)
parser.add_argument(
"--line-filter",
required=False,
help='Line-filter for clang-tidy (i.e. [{"name":"file1.cpp","lines":[[1,3],[5,7]]},...])',
)
args = parser.parse_args()
init_environment(args)
build_dir = "build"
clang_tidy_base_cmd = [
"clang-tidy",
"--quiet",
f"--format-style={args.clang_style}",
"--export-fixes=clang-tidy.yaml",
"-p",
build_dir,
]
enabled_cmd = clang_tidy_base_cmd + ["--explain-config"]
enabled_stdout, enabled_stderr, _ = run_command(enabled_cmd)
enabled_output = enabled_stdout + enabled_stderr
enabled_checks_log = os.path.join(args.log_dir, "clang-tidy-enabled-checks.log")
write_file(enabled_checks_log, enabled_output)
clang_cmd = clang_tidy_base_cmd
if args.line_filter:
clang_cmd = clang_cmd + [f"--line-filter={args.line_filter}"]
clang_cmd = clang_cmd + args.files.split()
print("clang_cmd = ", clang_cmd)
clang_stdout, clang_stderr, _ = run_command(clang_cmd)
clang_tidy_output = clang_stdout + clang_stderr
clang_tidy_log = os.path.join(args.log_dir, "clang-tidy.log")
write_file(clang_tidy_log, clang_tidy_output)
emit_problem_matchers(clang_tidy_log, "clang.json", "clang")
error_pattern = r"^.+:\d+:\d+: error: .+$"
warning_pattern = r"^.+:\d+:\d+: warning: .+$"
note_pattern = r"^.+:\d+:\d+: note: .+$"
errors_count = count_occurrences(error_pattern, clang_tidy_output)
warnings_count = count_occurrences(warning_pattern, clang_tidy_output)
notes_count = count_occurrences(note_pattern, clang_tidy_output)
logging.info(
"Found %d errors, %d warnings, %d notes",
errors_count,
warnings_count,
notes_count,
)
report = generate_markdown_report(
errors_count, warnings_count, notes_count, clang_tidy_output, enabled_output
)
append_file(args.report_file, report)
sys.exit(0 if errors_count == 0 else 1)
if __name__ == "__main__":
main()

119
tools/lint/clazy.py Normal file
View File

@@ -0,0 +1,119 @@
#!/usr/bin/env python3
import argparse
import sys
import os
import re
import logging
from utils import (
run_command,
init_environment,
write_file,
append_file,
emit_problem_matchers,
)
def generate_markdown_report(
aggregated_output: str, errors_count: int, warnings_count: int, notes_count: int
) -> str:
"""Generate a Markdown report based on clazy output and issue counts."""
report_lines = []
if errors_count > 0:
report_lines.append(
f"<details><summary>:fire: Clazy found :fire: {errors_count} errors, "
f":warning: {warnings_count} warnings and :pencil2: {notes_count} notes</summary>"
)
elif warnings_count > 0:
report_lines.append(
f"<details><summary>:warning: Clazy found :warning: {warnings_count} warnings "
f"and :pencil2: {notes_count} notes</summary>"
)
elif notes_count > 0:
report_lines.append(
f"<details><summary>:pencil2: Clazy found :pencil2: {notes_count} notes</summary>"
)
else:
report_lines.append(
"<details><summary>:heavy_check_mark: Clazy found no errors, warnings or notes</summary>"
)
report_lines.append("")
report_lines.append(
"[List of checks](https://github.com/KDE/clazy#list-of-checks), "
"[This explains some of the clazy warnings](https://www.kdab.com/uncovering-32-qt-best-practices-compile-time-clazy/)"
)
report_lines.append("````")
report_lines.append(aggregated_output)
report_lines.append("````")
report_lines.append("</details>")
report_lines.append("")
return "\n".join(report_lines)
def main():
parser = argparse.ArgumentParser(
description="Run Clazy on provided C++ files and append a Markdown report."
)
parser.add_argument(
"--files",
required=True,
help="A space-separated list (or glob-expanded string) of C++ files to check.",
)
parser.add_argument(
"--clazy-checks",
required=True,
help="Comma-separated list of clazy checks to run.",
)
parser.add_argument(
"--log-dir", required=True, help="Directory where the log file will be written."
)
parser.add_argument(
"--report-file",
required=True,
help="Path to the Markdown report file to append results.",
)
parser.add_argument("--verbose", action="store_true", help="Enable verbose output.")
args = parser.parse_args()
init_environment(args)
build_compile_commands = "build/compile_commands.json"
fix_file = "clazy.yaml"
output = ""
file_list = args.files.split()
for file in file_list:
cmd = [
"clazy-standalone",
f"--export-fixes={fix_file}",
f"-checks={args.clazy_checks}",
"-p",
build_compile_commands,
file,
]
stdout, stderr, _ = run_command(cmd)
output += stdout + "\n" + stderr + "\n"
log_file = os.path.join(args.log_dir, "clazy.log")
write_file(log_file, output)
emit_problem_matchers(log_file, "clang.json", "clang")
error_pattern = r"^.+:\d+:\d+: error: .+$"
warning_pattern = r"^.+:\d+:\d+: warning: .+$"
note_pattern = r"^.+:\d+:\d+: note: .+$"
errors_count = len(re.findall(error_pattern, output, re.MULTILINE))
warnings_count = len(re.findall(warning_pattern, output, re.MULTILINE))
notes_count = len(re.findall(note_pattern, output, re.MULTILINE))
logging.info(
f"Found {errors_count} errors, {warnings_count} warnings, {notes_count} notes"
)
report = generate_markdown_report(output, errors_count, warnings_count, notes_count)
append_file(args.report_file, report + "\n")
sys.exit(0 if errors_count == 0 else 1)
if __name__ == "__main__":
main()

120
tools/lint/clazy_qt6.py Normal file
View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
import argparse
import sys
import os
import re
import logging
from utils import (
run_command,
init_environment,
write_file,
append_file,
emit_problem_matchers,
)
def count_occurrences(pattern, text):
matches = re.findall(pattern, text, re.MULTILINE)
logging.debug(f"Found {len(matches)} matches for pattern: {pattern}")
return len(matches)
def generate_markdown_report(
output: str, errors_count: int, warnings_count: int, notes_count: int
) -> str:
"""Generate a Markdown report based on Clazy QT6 output and issue counts."""
report_lines = []
if errors_count > 0:
report_lines.append(
f"<details><summary>:fire: Clazy found :fire: {errors_count} errors, "
f":warning: {warnings_count} warnings and :pencil2: {notes_count} notes for porting to QT6</summary>"
)
elif warnings_count > 0:
report_lines.append(
f"<details><summary>:warning: Clazy found :warning: {warnings_count} warnings and "
f":pencil2: {notes_count} notes for porting to QT6</summary>"
)
elif notes_count > 0:
report_lines.append(
f"<details><summary>:pencil2: Clazy found :pencil2: {notes_count} notes for porting to QT6</summary>"
)
else:
report_lines.append(
"<details><summary>:heavy_check_mark: Clazy found no errors, warnings or notes for porting to QT6</summary>"
)
report_lines.append("")
report_lines.append("````")
report_lines.append(output)
report_lines.append("````")
report_lines.append("</details>")
report_lines.append("")
return "\n".join(report_lines)
def main():
parser = argparse.ArgumentParser(
description="Run Clazy checks for QT6 on provided C++ files and append a Markdown report."
)
parser.add_argument(
"--files",
required=True,
help="A space-separated list (or glob-expanded string) of C++ files to check.",
)
parser.add_argument(
"--clazy-qt6-checks",
required=True,
help="Comma-separated list of clazy QT6 checks to run.",
)
parser.add_argument(
"--log-dir", required=True, help="Directory where the log file will be written."
)
parser.add_argument(
"--report-file",
required=True,
help="Path to the Markdown report file to append results.",
)
parser.add_argument("--verbose", action="store_true", help="Enable verbose output.")
args = parser.parse_args()
# Initialize logging and required directories using a shared utility.
init_environment(args)
build_dir = "build"
fix_file = "clazyQT6.yaml"
# Build the clazy command as a list.
cmd = [
"clazy-standalone",
f"--export-fixes={fix_file}",
f"-checks={args.clazy_qt6_checks}",
"-p",
build_dir,
] + args.files.split()
stdout, stderr, _ = run_command(cmd)
output = stdout + stderr
log_file = os.path.join(args.log_dir, "clazyQT6.log")
write_file(log_file, output)
emit_problem_matchers(log_file, "clang.json", "clang")
error_pattern = r"^.+:\d+:\d+: error: .+$"
warning_pattern = r"^.+:\d+:\d+: warning: .+$"
note_pattern = r"^.+:\d+:\d+: note: .+$"
errors_count = count_occurrences(error_pattern, output)
warnings_count = count_occurrences(warning_pattern, output)
notes_count = count_occurrences(note_pattern, output)
logging.info(
f"Found {errors_count} errors, {warnings_count} warnings, {notes_count} notes"
)
report = generate_markdown_report(output, errors_count, warnings_count, notes_count)
append_file(args.report_file, report + "\n")
sys.exit(0 if errors_count == 0 else 1)
if __name__ == "__main__":
main()

133
tools/lint/codespell.py Normal file
View File

@@ -0,0 +1,133 @@
#!/usr/bin/env python3
import argparse
import sys
import os
import urllib.request
import logging
from utils import (
run_command,
init_environment,
write_file,
append_file,
emit_problem_matchers,
)
def download_dictionary(url, dest):
logging.info(f"Downloading dictionary from {url} to {dest}")
try:
urllib.request.urlretrieve(url, dest)
except Exception as e:
logging.error(f"Error downloading dictionary: {e}", file=sys.stderr)
sys.exit(1)
def generate_markdown_report(misspellings: int, log_file: str) -> str:
"""
Generate a Markdown report section based on the codespell results and log file.
"""
report_lines = []
if misspellings > 0:
report_lines.append(
f"<details><summary>:pencil2: Codespell found {misspellings} misspellings</summary>"
)
else:
report_lines.append(
"<details><summary>:heavy_check_mark: Codespell found no misspellings</summary>"
)
report_lines.append("")
report_lines.append(
"To ignore false positives, append the word to the [.github/codespellignore]"
"(https://github.com/FreeCAD/FreeCAD/blob/master/.github/codespellignore) file (lowercase)"
)
report_lines.append("````")
with open(log_file, "r", encoding="utf-8") as lf:
report_lines.append(lf.read())
report_lines.append("````")
report_lines.append("</details>")
report_lines.append("")
return "\n".join(report_lines)
def main():
parser = argparse.ArgumentParser(
description="Run codespell on files and append a Markdown report."
)
parser.add_argument(
"--files",
action="extend",
nargs="+",
required=True,
help="List of files to spell check."
)
parser.add_argument(
"--ignore-words",
required=True,
help="Comma-separated list of words to ignore (from codespellignore).",
)
parser.add_argument(
"--log-dir",
required=True,
help="Directory to store the log file."
)
parser.add_argument(
"--report-file",
required=True,
help="Name of the report file."
)
parser.add_argument(
"--skip", required=True, help="Comma-separated list of file patterns to skip."
)
parser.add_argument(
"--verbose",
action='store_true',
help="Use verbose output."
)
args = parser.parse_args()
init_environment(args)
dictionary_file = "dictionary.txt"
if not os.path.exists(dictionary_file):
dictionary_url = "https://raw.githubusercontent.com/codespell-project/codespell/master/codespell_lib/data/dictionary.txt"
logging.info("Dictionary not found; downloading...")
download_dictionary(dictionary_url, dictionary_file)
run_command(["pip", "install", "-q", "codespell"], check=True)
cmd = [
"codespell",
"--quiet-level",
"3",
"--summary",
"--count",
"--ignore-words",
args.ignore_words,
"--skip",
args.skip,
"-D",
dictionary_file,
] + args.files
stdout, stderr, _ = run_command(cmd)
output = stdout + "\n" + stderr + "\n"
log_file = os.path.join(args.log_dir, "codespell.log")
write_file(log_file, output)
emit_problem_matchers(log_file, "codespell.json", "codespell")
try:
misspellings = int(stdout.strip())
except ValueError:
logging.info(f"Could not parse misspellings count from output:\n{stdout}")
misspellings = 0
logging.info(f"Found {misspellings} misspellings")
report = generate_markdown_report(misspellings, log_file)
append_file(args.report_file, report + "\n")
sys.exit(0 if misspellings == 0 else 1)
if __name__ == "__main__":
main()

65
tools/lint/cpplint.cfg Normal file
View File

@@ -0,0 +1,65 @@
+build/deprecated,
-build/endif_comment,
+build/explicit_make_pair,
-build/forward_decl,
-build/header_guard,
+build/include,
-build/include_subdir,
-build/include_alpha,
-build/include_order,
+build/include_what_you_use,
+build/namespaces_headers,
+build/namespaces_literals,
-build/namespaces,
+build/printf_format,
+build/storage_class,
-legal/copyright,
+readability/alt_tokens,
-readability/braces,
+readability/casting,
+readability/check,
+readability/constructors,
+readability/fn_size,
+readability/inheritance,
+readability/multiline_comment,
+readability/multiline_string,
+readability/namespace,
-readability/nolint,
+readability/nul,
+readability/strings,
-readability/todo,
+readability/utf8,
+runtime/arrays,
+runtime/casting,
+runtime/explicit,
+runtime/int,
+runtime/init,
+runtime/invalid_increment,
+runtime/member_string_references,
+runtime/memset,
+runtime/operator,
+runtime/printf,
+runtime/printf_format,
+runtime/references,
+runtime/string,
+runtime/threadsafe_fn,
+runtime/vlog,
-whitespace/blank_line,
-whitespace/braces,
-whitespace/comma,
-whitespace/comments,
-whitespace/empty_conditional_body,
-whitespace/empty_if_body,
-whitespace/empty_loop_body,
-whitespace/end_of_line,
-whitespace/ending_newline,
-whitespace/forcolon,
-whitespace/indent,
-whitespace/indent_namespace,
-whitespace/line_length,
-whitespace/newline,
-whitespace/operators,
-whitespace/parens,
-whitespace/semicolon,
-whitespace/tab,
-whitespace/todo

90
tools/lint/cpplint.py vendored Normal file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env python3
import argparse
import sys
import os
import re
import logging
from utils import (
run_command,
init_environment,
write_file,
append_file,
emit_problem_matchers,
add_common_arguments,
)
DEFAULT_LINE_LENGTH_LIMIT = 100
def load_cpplint_filters(conf_file="cpplint.cfg"):
"""Load cpplint filters from a configuration file."""
try:
with open(conf_file, "r", encoding="utf-8") as f:
filters = f.read().strip()
return filters
except Exception as e:
logging.warning(f"Could not load {conf_file}: {e}")
return ""
def count_cpplint_errors(log_text: str) -> int:
"""
Count the number of cpplint error/warning lines.
"""
matches = re.findall(r"\[[0-9]+\]$", log_text, re.MULTILINE)
return len(matches)
def generate_markdown_report(aggregated_output: str, errors_count: int) -> str:
"""Generate a Markdown report section based on cpplint output and error count."""
report_lines = []
if errors_count > 0:
report_lines.append(
f"<details><summary>:warning: CppLint found {errors_count} errors / warnings</summary>"
)
else:
report_lines.append(
"<details><summary>:heavy_check_mark: No cpplint errors found </summary>"
)
report_lines.append("")
report_lines.append("````")
report_lines.append(aggregated_output)
report_lines.append("````")
report_lines.append("</details>")
report_lines.append("")
return "\n".join(report_lines)
def main():
parser = argparse.ArgumentParser(
description="Run cpplint on provided C++ files and append a Markdown report."
)
add_common_arguments(parser)
args = parser.parse_args()
init_environment(args)
run_command(["pip", "install", "-q", "cpplint"], check=True)
cpplint_filters = load_cpplint_filters("cpplint.cfg")
aggregated_output = ""
for file in args.files.split():
cmd = [
"cpplint",
f"--filters={cpplint_filters}",
f"--linelength={DEFAULT_LINE_LENGTH_LIMIT}",
file,
]
stdout, stderr, _ = run_command(cmd)
aggregated_output += stdout + stderr + "\n"
cpplint_log = os.path.join(args.log_dir, "cpplint.log")
write_file(cpplint_log, aggregated_output)
emit_problem_matchers(cpplint_log, "cpplint.json", "cpplint")
errors_count = count_cpplint_errors(aggregated_output)
logging.info(f"Found {errors_count} cpplint errors")
report = generate_markdown_report(aggregated_output, errors_count)
append_file(args.report_file, report)
sys.exit(0 if errors_count == 0 else 1)
if __name__ == "__main__":
main()

165
tools/lint/generic_checks.py Executable file
View File

@@ -0,0 +1,165 @@
import argparse
import glob
import os
from utils import (
add_common_arguments,
init_environment,
write_file,
emit_problem_matchers,
)
def check_line_endings(file_paths):
"""Check if files have non-Unix (CRLF or CR) line endings.
Returns a dict mapping file path to an issue description.
"""
issues = {}
for path in file_paths:
try:
with open(path, "rb") as f:
content = f.read()
if b"\r\n" in content or (b"\r" in content and b"\n" not in content):
issues[path] = "File has non-Unix line endings."
except Exception as e:
issues[path] = f"Error reading file: {e}"
return issues
def check_trailing_whitespace(file_paths):
"""Check for trailing whitespace in files.
Returns a dict mapping file paths to a list of line numbers with trailing whitespace.
"""
issues = {}
for path in file_paths:
try:
with open(path, "r", encoding="utf-8") as f:
lines = f.read().splitlines()
error_lines = [
idx
for idx, line in enumerate(lines, start=1)
if line != line.rstrip()
]
if error_lines:
issues[path] = error_lines
except Exception as e:
issues[path] = f"Error reading file: {e}"
return issues
def check_tabs(file_paths):
"""Check for usage of tab characters in files.
Returns a dict mapping file paths to a list of line numbers containing tabs.
"""
issues = {}
for path in file_paths:
try:
with open(path, "r", encoding="utf-8") as f:
lines = f.read().splitlines()
tab_lines = [
idx for idx, line in enumerate(lines, start=1) if "\t" in line
]
if tab_lines:
issues[path] = tab_lines
except Exception as e:
issues[path] = f"Error reading file: {e}"
return issues
def format_report(section_title, issues):
"""Format a report section with a Markdown details block."""
report = []
if issues:
count = len(issues)
report.append(
f"<details><summary>:information_source: Found {count} issue(s) in {section_title}</summary>\n"
)
report.append("```")
for file, details in issues.items():
report.append(f"{file}: {details}")
report.append("```\n</details>\n")
else:
report.append(f":heavy_check_mark: No issues found in {section_title}\n")
return "\n".join(report)
def main():
parser = argparse.ArgumentParser(description="Run formatting lint checks.")
add_common_arguments(parser)
parser.add_argument(
"--lineendings-check",
action="store_true",
help="Enable check for non-Unix line endings.",
)
parser.add_argument(
"--whitespace-check",
action="store_true",
help="Enable check for trailing whitespace.",
)
parser.add_argument(
"--tabs-check", action="store_true", help="Enable check for tab characters."
)
args = parser.parse_args()
init_environment(args)
file_list = glob.glob(args.files, recursive=True)
file_list = [f for f in file_list if os.path.isfile(f)]
report_sections = []
# Check non-Unix line endings.
if args.lineendings_check:
le_issues = check_line_endings(file_list)
for file, detail in le_issues.items():
print(f"::warning file={file},title={file}::{detail}")
report_sections.append(format_report("Non-Unix Line Endings", le_issues))
# Check trailing whitespace.
if args.whitespace_check:
ws_issues = check_trailing_whitespace(file_list)
if ws_issues:
ws_output_lines = []
for file, details in ws_issues.items():
if isinstance(details, list):
for line in details:
ws_output_lines.append(f"{file}:{line}: trailing whitespace")
else:
ws_output_lines.append(f"{file}: {details}")
ws_output = "\n".join(ws_output_lines)
ws_log_file = os.path.join(args.log_dir, "whitespace.log")
write_file(ws_log_file, ws_output)
emit_problem_matchers(
ws_log_file, "grepMatcherWarning.json", "grepMatcher-warning"
)
report_sections.append(format_report("Trailing Whitespace", ws_issues))
# Check tab usage.
if args.tabs_check:
tab_issues = check_tabs(file_list)
if tab_issues:
tab_output_lines = []
for file, details in tab_issues.items():
if isinstance(details, list):
for line in details:
tab_output_lines.append(f"{file}:{line}: contains tab")
else:
tab_output_lines.append(f"{file}: {details}")
tab_output = "\n".join(tab_output_lines)
tab_log_file = os.path.join(args.log_dir, "tabs.log")
write_file(tab_log_file, tab_output)
emit_problem_matchers(
tab_log_file, "grepMatcherWarning.json", "grepMatcher-warning"
)
report_sections.append(format_report("Tab Usage", tab_issues))
report_content = "\n".join(report_sections)
write_file(args.report_file, report_content)
print("Lint report generated at:", args.report_file)
if __name__ == "__main__":
main()

160
tools/lint/pylint.py Executable file
View File

@@ -0,0 +1,160 @@
#!/usr/bin/env python3
import argparse
import sys
import re
import os
import logging
from utils import (
run_command,
add_common_arguments,
init_environment,
write_file,
append_file,
emit_problem_matchers,
)
def get_enabled_checks(log_dir: str) -> str:
"""Run pylint to list enabled messages and write to a log file."""
enabled_log_path = os.path.join(log_dir, "pylint-enabled-checks.log")
stdout, _, _ = run_command(["pylint", "--list-msgs-enabled"])
write_file(enabled_log_path, stdout)
return enabled_log_path
def run_pylint(files: str, disable: str, log_dir: str) -> str:
"""Run pylint on the provided files and log the output."""
pylint_log_path = os.path.join(log_dir, "pylint.log")
cmd = ["pylint", f"--disable={disable}"] + files.split()
stdout, stderr, _ = run_command(cmd, check=False)
combined_output = stdout + "\n" + stderr
write_file(pylint_log_path, combined_output)
return pylint_log_path
def count_pattern(pattern: str, text: str) -> int:
"""Count numeric values found by a regex pattern in the text."""
matches = re.findall(pattern, text)
logging.debug("Pattern '%s' found matches: %s", pattern, matches)
return sum(int(match) for match in matches) if matches else 0
def parse_pylint_output(log_path: str) -> dict:
"""Parse pylint log file for error, warning, refactor, and convention counts."""
with open(log_path, "r", encoding="utf-8") as f:
content = f.read()
errors = count_pattern(r"error\s+\|(\d+)", content)
warnings = count_pattern(r"warning\s+\|(\d+)", content)
refactorings = count_pattern(r"refactor\s+\|(\d+)", content)
conventions = count_pattern(r"convention\s+\|(\d+)", content)
logging.info(
"Pylint issues - Errors: %s, Warnings: %s, Refactorings: %s, Conventions: %s",
errors,
warnings,
refactorings,
conventions,
)
return {
"errors": errors,
"warnings": warnings,
"refactorings": refactorings,
"conventions": conventions,
"full_output": content,
}
def generate_markdown_report(
pylint_counts: dict, enabled_checks_log: str, pylint_log: str
) -> str:
"""Generate a Markdown-formatted report based on pylint results and logs."""
def generate_summary(counts: dict) -> str:
errors = counts.get("errors", 0)
warnings = counts.get("warnings", 0)
refactorings = counts.get("refactorings", 0)
conventions = counts.get("conventions", 0)
if errors:
return (
f":fire: Pylint found :fire: {errors} errors, "
f":warning: {warnings} warnings, :construction: {refactorings} refactorings and "
f":pencil2: {conventions} conventions"
)
if warnings:
return (
f":warning: Pylint found :warning: {warnings} warnings, "
f":construction: {refactorings} refactorings and "
f":pencil2: {conventions} conventions"
)
if refactorings:
return (
f":construction: Pylint found :construction: {refactorings} refactorings and "
f":pencil2: {conventions} conventions"
)
if conventions:
return f":pencil2: Pylint found :pencil2: {conventions} conventions"
return ":heavy_check_mark: No pylint errors found"
def read_file_contents(file_path: str) -> str:
with open(file_path, "r", encoding="utf-8") as f:
return f.read()
report_lines = []
summary = generate_summary(pylint_counts)
report_lines.append(f"<details><summary>{summary}</summary>")
report_lines.append("")
report_lines.append(
"<details><summary>:information_source: Enabled checks</summary>"
)
report_lines.append("")
report_lines.append("````")
report_lines.append(read_file_contents(enabled_checks_log))
report_lines.append("````")
report_lines.append("</details>")
report_lines.append("")
report_lines.append("<details>")
report_lines.append("````")
report_lines.append(read_file_contents(pylint_log))
report_lines.append("````")
report_lines.append("</details>")
report_lines.append("</details>")
return "\n".join(report_lines)
def main():
parser = argparse.ArgumentParser(
description="Run Pylint on provided Python files and append a Markdown report."
)
add_common_arguments(parser)
parser.add_argument(
"--disable",
default="C0302,C0303",
help="Comma-separated list of pylint checks to disable.",
)
args = parser.parse_args()
init_environment(args)
logging.info("Installing pylint (if needed)...")
run_command(["pip", "install", "-q", "pylint"], check=True)
enabled_checks_log = get_enabled_checks(args.log_dir)
pylint_log = run_pylint(args.files, args.disable, args.log_dir)
emit_problem_matchers(pylint_log, "pylintError.json", "pylint-error")
emit_problem_matchers(pylint_log, "pylintWarning.json", "pylint-warning")
pylint_counts = parse_pylint_output(pylint_log)
summary_msg = (
f"Found {pylint_counts['errors']} errors, {pylint_counts['warnings']} warnings, "
f"{pylint_counts['refactorings']} refactorings, {pylint_counts['conventions']} conventions"
)
logging.info(summary_msg)
report = generate_markdown_report(pylint_counts, enabled_checks_log, pylint_log)
append_file(args.report_file, report)
sys.exit(0 if pylint_counts["errors"] == 0 else 1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env python3
import argparse
import os
import re
import sys
from utils import (
add_common_arguments,
init_environment,
write_file,
append_file,
emit_problem_matchers,
)
def check_qt_connections(file_path):
"""
Scan the file for lines containing 'SIGNAL' or 'SLOT' and return a list of matching lines
with line numbers and a suggestion.
"""
matches = []
try:
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
for lineno, line in enumerate(f, start=1):
if re.search(r"\b(SIGNAL|SLOT)\b", line):
matches.append(
f"{file_path}:{lineno}: {line.rstrip()} <-- Consider using functor-based connections"
)
except Exception as e:
logging.debug(f"Error reading {file_path}: {e}")
return matches
def generate_markdown_report(all_matches):
"""Generate a Markdown report based on the list of QT connection matches."""
count = len(all_matches)
if count > 0:
md_report = (
f"<details><summary>:information_source: Found {count} QT string-based connections → consider using QT functor-based connections</summary>\n"
"\nFor more information see: https://wiki.qt.io/New_Signal_Slot_Syntax or https://github.com/FreeCAD/FreeCAD/issues/6166\n"
"```\n" + "\n".join(all_matches) + "\n```\n</details>\n\n"
)
else:
md_report = ":heavy_check_mark: No string-based connections found\n\n"
return md_report
def main():
parser = argparse.ArgumentParser(
description="Check for old Qt string-based connections in files and append a Markdown report."
)
add_common_arguments(parser)
args = parser.parse_args()
init_environment(args)
all_matches = []
for file_path in args.files.split():
logging.debug(f"Checking file: {file_path}")
matches = check_qt_connections(file_path)
all_matches.extend(matches)
log_file = os.path.join(args.log_dir, "qtConnections.log")
write_file(log_file, "\n".join(all_matches))
emit_problem_matchers(log_file, "grepMatcherWarning.json", "grepMatcher-warning")
count = len(all_matches)
logging.info(f"Found {count} QT string-based connections")
md_report = generate_markdown_report(all_matches)
append_file(args.report_file, md_report)
sys.exit(0 if count == 0 else 1)
if __name__ == "__main__":
main()

95
tools/lint/utils.py Normal file
View File

@@ -0,0 +1,95 @@
#!/usr/bin/env python3
import os
import sys
import logging
import subprocess
import argparse
from typing import Tuple
def run_command(cmd, check=False) -> Tuple[str, str, int]:
"""
Run a command using subprocess.run and return stdout, stderr, and exit code.
"""
try:
result = subprocess.run(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=check, text=True
)
return result.stdout, result.stderr, result.returncode
except subprocess.CalledProcessError as e:
return e.stdout, e.stderr, e.returncode
def setup_logger(verbose: bool):
"""
Setup the logging level based on the verbose flag.
"""
level = logging.DEBUG if verbose else logging.INFO
logging.basicConfig(level=level, format="%(levelname)s: %(message)s")
def write_file(file_path: str, content: str):
"""Write content to the specified file."""
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
logging.info("Wrote file: %s", file_path)
def append_file(file_path: str, content: str):
"""Append content to the specified file."""
with open(file_path, "a", encoding="utf-8") as f:
f.write(content + "\n")
logging.info("Appended content to file: %s", file_path)
def emit_problem_matchers(log_path: str, matcher_filename: str, remove_owner: str):
"""
Emit GitHub Actions problem matcher commands using the given matcher file.
This function will:
1. Check if the log file exists.
2. Use the RUNNER_WORKSPACE environment variable to construct the matcher path.
3. Print the add-matcher command, then the log content, then the remove-matcher command.
"""
if os.path.isfile(log_path):
runner_workspace = os.getenv("RUNNER_WORKSPACE")
matcher_path = os.path.join(
runner_workspace, "FreeCAD", ".github", "problemMatcher", matcher_filename
)
print(f"::add-matcher::{matcher_path}")
with open(log_path, "r", encoding="utf-8") as f:
sys.stdout.write(f.read())
print(f"::remove-matcher owner={remove_owner}::")
def add_common_arguments(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
"""
Add common command-line arguments shared between tools.
"""
parser.add_argument(
"--files",
required=True,
help="A space-separated list or glob pattern of files to check.",
)
parser.add_argument(
"--log-dir", required=True, help="Directory where log files will be written."
)
parser.add_argument(
"--report-file",
required=True,
help="Path to the Markdown report file to append results.",
)
parser.add_argument("--verbose", action="store_true", help="Enable verbose output.")
return parser
def init_environment(args: argparse.Namespace):
"""
Perform common initialization tasks:
- Set up logging.
- Create the log directory.
- Create the directory for the report file.
"""
setup_logger(args.verbose)
os.makedirs(args.log_dir, exist_ok=True)
os.makedirs(os.path.dirname(args.report_file), exist_ok=True)

307
tools/prepare_web_dependencies.sh Executable file
View File

@@ -0,0 +1,307 @@
#!/usr/bin/env bash
set -euo pipefail
source_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
workspace_dir=$(cd "$source_dir/.." && pwd)
base_lib_dir=${FREECAD_WEB_BASE_LIB_DIR:-"$workspace_dir/FreeCADBaseLib"}
build_root=${FREECAD_WEB_BUILD_ROOT:-"$source_dir/build-web"}
jobs=${FREECAD_WEB_JOBS:-8}
yaml_source="$base_lib_dir/sources/src/YAML-CPP-0.8.0"
eigen_source="$base_lib_dir/sources/src/Eigen-3.4.0"
boost_source="$base_lib_dir/sources/src/Boost-1.83.0"
zlib_source="$base_lib_dir/sources/src/zlib-1.3"
xerces_source="$base_lib_dir/sources/src/Xerces-C-3.2.4"
qt_source="$base_lib_dir/sources/src/Qt-5.15.13/qtbase"
python_source="$base_lib_dir/sources/src/Python-3.12.3"
yaml_build="$build_root/yaml-cpp"
yaml_install="$build_root/yaml-install"
eigen_build="$build_root/eigen"
eigen_install="$build_root/eigen-install"
boost_build="$build_root/boost-build"
boost_install="$build_root/boost-install"
zlib_build="$build_root/zlib"
zlib_install="$build_root/zlib-install"
xerces_build="$build_root/xerces"
xerces_install="$build_root/xerces-install"
qt_build="$build_root/qt-probe"
qt_install="$build_root/qt-install"
python_build="$build_root/python-probe"
python_install="$build_root/python-install"
icu_install="$build_root/icu-install"
occt_install="$build_root/occt-install"
icu_download="$build_root/icu-download/icu4c-68_2-src.tgz"
icu_port_dir="$build_root/emscripten-cache/ports/icu"
emscripten_cache="$build_root/emscripten-cache"
icu_domestic_url=${FREECAD_WEB_ICU_DOMESTIC_URL:-"https://mirrors.aliyun.com/blfs/conglomeration/icu/icu4c-68_2-src.tgz"}
icu_official_url=${FREECAD_WEB_ICU_OFFICIAL_URL:-"https://github.com/unicode-org/icu/releases/download/release-68-2/icu4c-68_2-src.zip"}
icu_domestic_sha512="24f3b7981667b37df58118f56ae76a09b719358c1efa8d6f044d28ef8df1c722a894957acadfb3c37aee39ac845893924fc3370d6bbe09875aec8d937059f84b"
icu_official_sha512="12c3db5966c234c94e7918fb8acc8bd0838edc36a620f3faa788e7ff27b06f1aa431eb117401026e3963622b9323212f444b735d5c9dd3d0b82d772a4834b993"
icu_archive_format=tgz
mkdir -p "$build_root" "$source_dir/../docs" "$emscripten_cache"
exec > >(tee "$source_dir/../docs/web-dependency-install.log") 2>&1
printf 'ICU domestic mirror preference: %s\n' "$icu_domestic_url"
printf 'ICU official fallback: %s\n' "$icu_official_url"
if [[ ! -f "$yaml_source/.source-complete" ||
! -f "$boost_source/.source-complete" ||
! -f "$eigen_source/.source-complete" ||
! -f "$zlib_source/.source-complete" ||
! -f "$xerces_source/.source-complete" ||
! -f "$qt_source/configure" ||
! -f "$python_source/configure" ]]; then
"$base_lib_dir/download_sources.sh"
fi
toolchain=${FREECAD_WEB_EMSCRIPTEN_TOOLCHAIN:-}
if [[ -z "$toolchain" ]]; then
emcc_path=$(realpath "$(command -v emcc)")
toolchain="$(dirname "$emcc_path")/cmake/Modules/Platform/Emscripten.cmake"
fi
if [[ ! -f "$toolchain" ]]; then
printf 'Emscripten toolchain not found: %s\n' "$toolchain" >&2
exit 1
fi
emscripten_root=$(cd "$(dirname "$toolchain")/../../.." && pwd)
emscripten_config=${FREECAD_WEB_EMSCRIPTEN_CONFIG:-"$build_root/emscripten-config.py"}
if [[ -z "${FREECAD_WEB_EMSCRIPTEN_CONFIG:-}" ]] &&
{ [[ ! -f "$emscripten_config" ]] ||
! grep -Fq "CACHE = r'$emscripten_cache'" "$emscripten_config"; }; then
cp "$emscripten_root/.emscripten" "$emscripten_config"
sed -i "s|^FROZEN_CACHE = True.*|FROZEN_CACHE = False\nCACHE = r'$emscripten_cache'\nPORTS = r'$emscripten_cache/ports'|" \
"$emscripten_config"
fi
mkdir -p "$(dirname "$icu_download")"
if [[ ! -f "$icu_download" ]]; then
printf 'Downloading ICU from domestic mirror: %s\n' "$icu_domestic_url"
if ! curl --fail --location --retry 2 --connect-timeout 10 --max-time 600 \
"$icu_domestic_url" -o "$icu_download"; then
printf 'Domestic ICU mirror failed; falling back to official source: %s\n' "$icu_official_url"
icu_archive_format=zip
curl --fail --location --retry 2 --connect-timeout 10 --max-time 600 \
"$icu_official_url" -o "$icu_download"
fi
fi
actual_icu_sha512=$(sha512sum "$icu_download" | awk '{print $1}')
printf 'ICU source archive: %s (SHA-512 %s)\n' "$icu_download" "$actual_icu_sha512"
if [[ "$actual_icu_sha512" != "$icu_domestic_sha512" &&
"$actual_icu_sha512" != "$icu_official_sha512" ]]; then
printf 'Unexpected ICU archive SHA-512: %s\n' "$actual_icu_sha512" >&2
exit 1
fi
if [[ "$actual_icu_sha512" == "$icu_official_sha512" ]]; then
icu_archive_format=zip
else
icu_archive_format=tgz
fi
# Initialize the selected Emscripten cache before seeding the local port. A
# changed config can clear the cache during this first compiler invocation.
printf 'int main() { return 0; }\n' | \
EM_CONFIG="$emscripten_config" EM_FROZEN_CACHE= \
em++ -x c++ - -sWASM=1 -o "$build_root/emscripten-sanity.js"
# Emscripten 3.1.6's ICU port hard-codes a GitHub URL. Seed its local port
# cache from the verified domestic archive so the compiler never needs that
# slow network path during a Web dependency preparation run.
if [[ ! -f "$icu_port_dir/icu/source/common/unicode/utypes.h" ]]; then
mkdir -p "$icu_port_dir"
if [[ "$icu_archive_format" == zip ]]; then
unzip -q "$icu_download" -d "$icu_port_dir"
else
tar -xzf "$icu_download" -C "$icu_port_dir"
fi
fi
printf '%s\n' "$icu_official_url" > "$icu_port_dir/.emscripten_url"
printf 'Building Emscripten ICU port from the verified local source\n'
printf 'int main() { return 0; }\n' | \
EM_CONFIG="$emscripten_config" EM_FROZEN_CACHE= EM_PORTS="$emscripten_cache/ports" \
em++ -x c++ - -sUSE_ICU=1 -sWASM=1 \
-o "$build_root/icu-port-smoke.js"
mkdir -p "$icu_install/include" "$icu_install/lib"
rm -rf "$icu_install/include/unicode"
cp -a "$emscripten_cache/sysroot/include/unicode" "$icu_install/include/"
cp "$emscripten_cache/sysroot/lib/wasm32-emscripten/libicu_common.a" "$icu_install/lib/libicuuc.a"
cp "$emscripten_cache/sysroot/lib/wasm32-emscripten/libicu_i18n.a" "$icu_install/lib/libicui18n.a"
cp "$emscripten_cache/sysroot/lib/wasm32-emscripten/libicu_stubdata.a" "$icu_install/lib/libicudata.a"
cmake -S "$yaml_source" -B "$yaml_build" -G Ninja \
-DCMAKE_TOOLCHAIN_FILE="$toolchain" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$yaml_install" \
-DYAML_BUILD_SHARED_LIBS=OFF \
-DYAML_CPP_BUILD_TESTS=OFF \
-DYAML_CPP_BUILD_TOOLS=OFF \
-DYAML_CPP_INSTALL=ON
cmake --build "$yaml_build" --parallel "$jobs"
cmake --install "$yaml_build"
cmake -S "$eigen_source" -B "$eigen_build" -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$eigen_install" \
-DEIGEN_BUILD_DOC=OFF \
-DBUILD_TESTING=OFF
cmake --install "$eigen_build" --component Devel
boost_b2=${FREECAD_WEB_B2_EXECUTABLE:-}
if [[ -z "$boost_b2" ]]; then
boost_b2=$(command -v b2 || command -v bjam || true)
fi
if [[ -z "$boost_b2" ]]; then
printf 'B2 executable not found; set FREECAD_WEB_B2_EXECUTABLE.\n' >&2
exit 1
fi
boost_user_config="$build_root/boost-user-config.jam"
printf 'using clang : wasm : %s ;\n' "$(command -v em++)" > "$boost_user_config"
"$boost_b2" -q \
--user-config="$boost_user_config" \
--build-dir="$boost_build" \
--prefix="$boost_install" \
--layout=system -j"$jobs" \
toolset=clang-wasm variant=release link=static runtime-link=static \
threading=single cxxflags=-fexceptions linkflags=-fexceptions \
--with-program_options --with-regex --with-thread --with-date_time install
if [[ ! -f "$zlib_build/Makefile" ]]; then
mkdir -p "$zlib_build"
cp -a "$zlib_source/." "$zlib_build/"
(
cd "$zlib_build"
emconfigure ./configure --static --prefix="$zlib_install"
)
fi
emmake make -C "$zlib_build" --jobs="$jobs"
emmake make -C "$zlib_build" install
# Emscripten's link-only check_function_exists probes report arbitrary symbols
# as present. Xerces-C would then omit its portable replacements for the
# non-standard case-insensitive string functions and fail at compile time.
cmake -S "$xerces_source" -B "$xerces_build" -G Ninja \
-DCMAKE_TOOLCHAIN_FILE="$toolchain" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$xerces_install" \
-DCMAKE_INSTALL_LIBDIR=lib \
-DBUILD_SHARED_LIBS=OFF \
-Dnetwork=OFF \
-Dtranscoder=icu \
-Dmessage-loader=icu \
-Dmutex-manager=nothreads \
-DHAVE_STRICMP=0 \
-DHAVE_STRNICMP=0 \
-DICU_ROOT="$icu_install" \
-DICU_INCLUDE_DIR="$icu_install/include" \
-DICU_UC_LIBRARY="$icu_install/lib/libicuuc.a" \
-DICU_UC_LIBRARY_RELEASE="$icu_install/lib/libicuuc.a" \
-DICU_DATA_LIBRARY="$icu_install/lib/libicudata.a" \
-DICU_DATA_LIBRARY_RELEASE="$icu_install/lib/libicudata.a"
cmake --build "$xerces_build" --target xerces-c --parallel "$jobs"
# Installing the whole project also installs sample executables, which are
# intentionally not built for the dependency package. Install only the
# library, headers, and CMake package emitted by the src directory.
cmake -DCMAKE_INSTALL_COMPONENT=runtime \
-P "$xerces_build/src/cmake_install.cmake"
cmake -DCMAKE_INSTALL_COMPONENT=development \
-P "$xerces_build/src/cmake_install.cmake"
cmake -DCMAKE_INSTALL_COMPONENT=Unspecified \
-P "$xerces_build/src/cmake_install.cmake"
if [[ ! -f "$qt_build/Makefile" ]]; then
mkdir -p "$qt_build"
(
cd "$qt_build"
"$qt_source/configure" \
-xplatform wasm-emscripten \
-opensource -confirm-license -release -static \
-no-feature-thread -nomake examples -nomake tests \
-prefix "$qt_install"
)
fi
make -C "$qt_build/src" --jobs="$jobs" sub-network sub-xml
make -C "$qt_build/src" --jobs="$jobs" \
sub-corelib-install_subtargets \
sub-network-install_subtargets \
sub-xml-install_subtargets
make -C "$qt_build/src" --jobs="$jobs" sub-rcc
make -C "$qt_build/src" sub-rcc-install_subtargets
make -C "$qt_build" install_mkspecs
install -m 755 "$qt_build/bin/qmake" "$qt_install/bin/qmake"
install -m 644 "$qt_build/bin/qt.conf" "$qt_install/bin/qt.conf"
if [[ ! -f "$python_build/Makefile" ]]; then
mkdir -p "$python_build"
(
cd "$python_build"
CONFIG_SITE="$python_source/Tools/wasm/config.site-wasm32-emscripten" \
EM_CACHE="$emscripten_cache" EM_FROZEN_CACHE= \
emconfigure "$python_source/configure" -C \
--host=wasm32-unknown-emscripten \
--build=x86_64-pc-linux-gnu \
--with-emscripten-target=browser \
--with-build-python="${FREECAD_WEB_HOST_PYTHON_EXECUTABLE:-/usr/bin/python3.12}" \
--without-ensurepip
)
fi
EM_CACHE="$emscripten_cache" EM_FROZEN_CACHE= \
make -C "$python_build" --jobs="$jobs"
EM_CACHE="$emscripten_cache" EM_FROZEN_CACHE= \
make -C "$python_build" --jobs="$jobs" prefix="$python_install" install
required_files=(
"$yaml_install/lib/libyaml-cpp.a"
"$yaml_install/lib/cmake/yaml-cpp/yaml-cpp-config.cmake"
"$eigen_install/include/eigen3/signature_of_eigen3_matrix_library"
"$eigen_install/include/eigen3/Eigen/src/Core/util/Macros.h"
"$boost_install/include/boost/version.hpp"
"$boost_install/lib/libboost_program_options.a"
"$boost_install/lib/libboost_regex.a"
"$boost_install/lib/libboost_thread.a"
"$boost_install/lib/libboost_date_time.a"
"$boost_install/lib/libboost_atomic.a"
"$zlib_install/include/zlib.h"
"$zlib_install/include/zconf.h"
"$zlib_install/lib/libz.a"
"$xerces_install/lib/libxerces-c.a"
"$xerces_install/lib/cmake/XercesC/XercesCConfig.cmake"
"$qt_install/lib/libQt5Core.a"
"$qt_install/lib/libQt5Network.a"
"$qt_install/lib/libQt5Xml.a"
"$qt_install/lib/cmake/Qt5/Qt5Config.cmake"
"$python_install/include/python3.12/Python.h"
"$python_install/lib/libpython3.12.a"
"$icu_install/include/unicode/utypes.h"
"$icu_install/lib/libicuuc.a"
"$icu_install/lib/libicui18n.a"
"$icu_install/lib/libicudata.a"
"$occt_install/include/opencascade/Standard_Version.hxx"
"$occt_install/lib/libTKernel.a"
"$boost_install/lib/libboost_program_options.a" \
"$boost_install/lib/libboost_regex.a" \
"$boost_install/lib/libboost_thread.a" \
"$boost_install/lib/libboost_date_time.a" \
"$boost_install/lib/libboost_atomic.a"
"$occt_install/lib/cmake/opencascade/OpenCASCADEConfig.cmake"
)
for required_file in "${required_files[@]}"; do
if [[ ! -f "$required_file" ]]; then
printf 'Missing Web dependency artifact: %s\n' "$required_file" >&2
exit 1
fi
done
file "$yaml_install/lib/libyaml-cpp.a" \
"$zlib_install/lib/libz.a" \
"$xerces_install/lib/libxerces-c.a" \
"$qt_install/lib/libQt5Core.a" \
"$qt_install/lib/libQt5Network.a" \
"$qt_install/lib/libQt5Xml.a" \
"$python_install/lib/libpython3.12.a" \
"$icu_install/lib/libicuuc.a" \
"$icu_install/lib/libicui18n.a" \
"$icu_install/lib/libicudata.a" \
"$occt_install/lib/libTKernel.a"
printf 'Web dependency installations are ready under %s\n' "$build_root"

37
tools/profile/perftest.sh Executable file
View File

@@ -0,0 +1,37 @@
#! /bin/bash
# Example file to drive the performance profiling python test from the shell.
# Built to support the Topological Naming Problem fixes from early 2024;
# this will likely need tweaking for your use.
notnp=<Path to your first executable to profile goes here> #<dir>/bin/FreeCAD${cmd}
tnp=<Path to your second executable to profile goes here> #<dir/bin/FreeCAD${cmd}
results="results.txt" # File to append measurements to. We do not clear so xargs works on script
save="--save" # or "" Whether to save the resulting file from the tnp executable for further testing.
perf record -o "$1.perf" $notnp -t TestPerf --pass "$@"
perf record -o "$1.tnp.perf" $tnp -t TestPerf --pass "$save" "$@"
# For interactive walking of the details using perf:
#perf report -i $1.perf
# After the two test runs above, process the resulting files to get the numbers.
# For perf, use the 'script' command to pull the info out of the file, and then do a little
# old school unix manipulation
times=($(perf script -F time -i "$1.perf" | sed -e's/://' -e'1p;$!d')) # first, last timestamps
totaltime=$(echo ${times[1]} - ${times[0]} | bc)
memory=$(grep .size: "$1.mprofile" | cut -f2 -d\ )
memory2=$(perf script --header -i "$1.perf" | grep "data size" | cut -f2 -d:)
timestnp=($(perf script -F time -i "$1.tnp.perf" | sed -e's/://' -e'1p;$!d')) # first, last times
totaltimetnp=$(echo ${timestnp[1]} - ${timestnp[0]} | bc)
memorytnp=$(grep .size: "$1.tnp.mprofile" | cut -f2 -d\ )
memory2tnp=$(perf script --header -i "$1.tnp.perf" | grep "data size" | cut -f2 -d:)
# To calculate in this script instead of externally, you could do something like this:
# delta=$(echo ${totaltimetnp} - ${totaltime} | bc)
# percent=$(echo "scale=6; ${delta} / ${totaltime} * 100" | bc)
# Summarize the run of one document into a CSV line suitable for importing into a spreadsheet.
echo $totaltime,$totaltimetnp,$memory,$memorytnp,$memory2,$memory2tnp,"$1" >>"${results}"