Skip to content

Commit d6a0449

Browse files
authored
freecad: make customizable (#347776)
2 parents 76193bc + a7ab6aa commit d6a0449

File tree

7 files changed

+322
-20
lines changed

7 files changed

+322
-20
lines changed

nixos/doc/manual/release-notes/rl-2411.section.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,8 @@
898898

899899
- `virtualisation.incus` module gained new `incus-user.service` and `incus-user.socket` systemd units. It is now possible to add a user to `incus` group instead of `incus-admin` for increased security.
900900

901+
- `freecad` now supports addons and custom configuration in nix-way, which can be used by calling `freecad.customize`.
902+
901903
## Detailed Migration Information {#sec-release-24.11-migration}
902904

903905
### `sound` options removal {#sec-release-24.11-migration-sound}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
From 23ddb6ff148ec5c27da050ba0eb7a2e449b8450b Mon Sep 17 00:00:00 2001
2+
From: Yury Shvedov <[email protected]>
3+
Date: Mon, 4 Nov 2024 14:22:22 +0300
4+
Subject: [PATCH] Gui: take in account module-path argument
5+
6+
Use paths passed with `--module-path` argument to search for preference
7+
packs
8+
9+
Change-Id: If168dbd99a826757290ee6b918f5b712305fe2bb
10+
---
11+
src/Gui/DlgPreferencePackManagementImp.cpp | 16 +++++----
12+
src/Gui/PreferencePackManager.cpp | 39 +++++++++++++++++-----
13+
src/Gui/PreferencePackManager.h | 5 +++
14+
3 files changed, 44 insertions(+), 16 deletions(-)
15+
16+
diff --git a/src/Gui/DlgPreferencePackManagementImp.cpp b/src/Gui/DlgPreferencePackManagementImp.cpp
17+
index a1a0dad41a..50f3982f21 100644
18+
--- a/src/Gui/DlgPreferencePackManagementImp.cpp
19+
+++ b/src/Gui/DlgPreferencePackManagementImp.cpp
20+
@@ -54,7 +54,7 @@ void DlgPreferencePackManagementImp::showEvent(QShowEvent* event)
21+
// but can only disable individual installed packs (though we can completely uninstall the pack's
22+
// containing Addon by redirecting to the Addon Manager).
23+
auto savedPreferencePacksDirectory = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks";
24+
- auto modDirectory = fs::path(App::Application::getUserAppDataDir()) / "Mod";
25+
+ auto modDirectories = Application::Instance->prefPackManager()->modPaths();
26+
auto resourcePath = fs::path(App::Application::getResourceDir()) / "Gui" / "PreferencePacks";
27+
28+
// The displayed tree has two levels: at the toplevel is either "User-Saved Packs" or the name
29+
@@ -66,12 +66,14 @@ void DlgPreferencePackManagementImp::showEvent(QShowEvent* event)
30+
auto builtinPacks = getPacksFromDirectory(resourcePath);
31+
32+
std::map<std::string, std::vector<std::string>> installedPacks;
33+
- if (fs::exists(modDirectory) && fs::is_directory(modDirectory)) {
34+
- for (const auto& mod : fs::directory_iterator(modDirectory)) {
35+
- auto packs = getPacksFromDirectory(mod);
36+
- if (!packs.empty()) {
37+
- auto modName = mod.path().filename().string();
38+
- installedPacks.emplace(modName, packs);
39+
+ for (const auto& modDirectory : modDirectories) {
40+
+ if (fs::exists(modDirectory) && fs::is_directory(modDirectory)) {
41+
+ for (const auto& mod : fs::directory_iterator(modDirectory)) {
42+
+ auto packs = getPacksFromDirectory(mod);
43+
+ if (!packs.empty()) {
44+
+ auto modName = mod.path().filename().string();
45+
+ installedPacks.emplace(modName, packs);
46+
+ }
47+
}
48+
}
49+
}
50+
diff --git a/src/Gui/PreferencePackManager.cpp b/src/Gui/PreferencePackManager.cpp
51+
index dfc54240c0..83e32fa05e 100644
52+
--- a/src/Gui/PreferencePackManager.cpp
53+
+++ b/src/Gui/PreferencePackManager.cpp
54+
@@ -30,6 +30,7 @@
55+
#endif
56+
57+
#include <boost/filesystem.hpp>
58+
+#include <boost/algorithm/string.hpp>
59+
#include <QDir>
60+
61+
#include "PreferencePackManager.h"
62+
@@ -134,12 +135,11 @@ void PreferencePack::applyConfigChanges() const
63+
}
64+
65+
PreferencePackManager::PreferencePackManager()
66+
+ : _preferencePackPaths(modPaths())
67+
{
68+
- auto modPath = fs::path(App::Application::getUserAppDataDir()) / "Mod";
69+
auto savedPath = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks";
70+
auto resourcePath = fs::path(App::Application::getResourceDir()) / "Gui" / "PreferencePacks";
71+
- _preferencePackPaths.push_back(resourcePath);
72+
- _preferencePackPaths.push_back(modPath);
73+
+ _preferencePackPaths.insert(_preferencePackPaths.begin(), resourcePath);
74+
_preferencePackPaths.push_back(savedPath);
75+
rescan();
76+
77+
@@ -232,6 +232,26 @@ void Gui::PreferencePackManager::importConfig(const std::string& packName,
78+
rescan();
79+
}
80+
81+
+// TODO(Shvedov): Is this suitable place for this method? It is more generic,
82+
+// and maybe more suitable place at Application?
83+
+std::vector<boost::filesystem::path> Gui::PreferencePackManager::modPaths() const
84+
+{
85+
+ auto userModPath = fs::path(App::Application::getUserAppDataDir()) / "Mod";
86+
+
87+
+ auto& config = App::Application::Config();
88+
+ auto additionalModules = config.find("AdditionalModulePaths");
89+
+ std::vector<boost::filesystem::path> result;
90+
+
91+
+ if (additionalModules != config.end()) {
92+
+ boost::split(result,
93+
+ additionalModules->second,
94+
+ boost::is_any_of(";"),
95+
+ boost::token_compress_on);
96+
+ }
97+
+ result.emplace_back(userModPath);
98+
+ return result;
99+
+}
100+
+
101+
void Gui::PreferencePackManager::FindPreferencePacksInPackage(const fs::path &mod)
102+
{
103+
try {
104+
@@ -528,7 +548,6 @@ std::vector<PreferencePackManager::TemplateFile> PreferencePackManager::template
105+
// (alternate spellings are provided for packages using CamelCase and snake_case, and both major English dialects)
106+
107+
auto resourcePath = fs::path(App::Application::getResourceDir()) / "Gui";
108+
- auto modPath = fs::path(App::Application::getUserAppDataDir()) / "Mod";
109+
110+
std::string group = "Built-In";
111+
if (fs::exists(resourcePath) && fs::is_directory(resourcePath)) {
112+
@@ -536,11 +555,13 @@ std::vector<PreferencePackManager::TemplateFile> PreferencePackManager::template
113+
std::copy(localFiles.begin(), localFiles.end(), std::back_inserter(_templateFiles));
114+
}
115+
116+
- if (fs::exists(modPath) && fs::is_directory(modPath)) {
117+
- for (const auto& mod : fs::directory_iterator(modPath)) {
118+
- group = mod.path().filename().string();
119+
- const auto localFiles = scanForTemplateFiles(group, mod);
120+
- std::copy(localFiles.begin(), localFiles.end(), std::back_inserter(_templateFiles));
121+
+ for (const auto& modPath : modPaths()) {
122+
+ if (fs::exists(modPath) && fs::is_directory(modPath)) {
123+
+ for (const auto& mod : fs::directory_iterator(modPath)) {
124+
+ group = mod.path().filename().string();
125+
+ const auto localFiles = scanForTemplateFiles(group, mod);
126+
+ std::copy(localFiles.begin(), localFiles.end(), std::back_inserter(_templateFiles));
127+
+ }
128+
}
129+
}
130+
131+
diff --git a/src/Gui/PreferencePackManager.h b/src/Gui/PreferencePackManager.h
132+
index 301e160df2..e5776e47a0 100644
133+
--- a/src/Gui/PreferencePackManager.h
134+
+++ b/src/Gui/PreferencePackManager.h
135+
@@ -191,6 +191,11 @@ namespace Gui {
136+
*/
137+
void importConfig(const std::string &packName, const boost::filesystem::path &path);
138+
139+
+ /**
140+
+ * Get a list of all mod directories.
141+
+ */
142+
+ std::vector<boost::filesystem::path> modPaths() const;
143+
+
144+
private:
145+
146+
void FindPreferencePacksInPackage(const boost::filesystem::path& mod);
147+
--
148+
2.44.1
149+
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
{
2+
runCommand,
3+
buildEnv,
4+
makeWrapper,
5+
lib,
6+
python311,
7+
writeShellScript,
8+
}:
9+
let
10+
wrapPathsStr =
11+
flag: values:
12+
builtins.concatStringsSep " " (
13+
builtins.concatMap (p: [
14+
"--add-flags"
15+
flag
16+
"--add-flags"
17+
p
18+
]) values
19+
);
20+
21+
wrapCfgStr =
22+
typ: val:
23+
let
24+
installer = writeShellScript "insteller-${typ}" ''
25+
dst="$HOME/.config/FreeCAD/${typ}.cfg"
26+
if [ ! -f "$dst" ]; then
27+
mkdir -p "$(dirname "$dst")"
28+
cp --no-preserve=mode,ownership '${val}' "$dst"
29+
fi
30+
'';
31+
in
32+
lib.optionalString (val != null) "--run ${installer}";
33+
34+
pythonsProcessed = builtins.map (
35+
pyt:
36+
if builtins.isString pyt then
37+
pyt
38+
else if builtins.isFunction pyt then
39+
"${(python311.withPackages pyt)}/lib/python3.11/site-packages"
40+
else
41+
throw "Expected string or function as python paths for freecad"
42+
);
43+
44+
makeCustomizable =
45+
freecad:
46+
freecad
47+
// {
48+
customize =
49+
{
50+
name ? freecad.name,
51+
modules ? [ ],
52+
pythons ? [ ],
53+
makeWrapperFlags ? [ ],
54+
userCfg ? null,
55+
systemCfg ? null,
56+
}:
57+
let
58+
modulesStr = wrapPathsStr "--module-path" modules;
59+
pythonsStr = wrapPathsStr "--python-path" (pythonsProcessed pythons);
60+
makeWrapperFlagsStr = builtins.concatStringsSep " " (builtins.map (f: "'${f}'") makeWrapperFlags);
61+
62+
userCfgStr = wrapCfgStr "user" userCfg;
63+
systemCfgStr = wrapCfgStr "system" systemCfg;
64+
65+
bin = runCommand "${name}-bin" { nativeBuildInputs = [ makeWrapper ]; } ''
66+
mkdir -p "$out/bin"
67+
for exe in FreeCAD{,Cmd}; do
68+
if [[ ! -e ${freecad}/bin/$exe ]]; then
69+
echo "No binary $exe in freecad package"
70+
false
71+
fi
72+
dest="$out/bin/$exe";
73+
makeWrapper "${freecad}/bin/$exe" "$dest" \
74+
--inherit-argv0 \
75+
${modulesStr} \
76+
${pythonsStr} \
77+
${userCfgStr} \
78+
${systemCfgStr} \
79+
${makeWrapperFlagsStr}
80+
done
81+
ln -s FreeCAD $out/bin/freecad
82+
ln -s FreeCADCmd $out/bin/freecadcmd
83+
'';
84+
in
85+
makeCustomizable (buildEnv {
86+
inherit name;
87+
paths = [
88+
(lib.lowPrio freecad)
89+
bin
90+
];
91+
});
92+
override = f: makeCustomizable (freecad.override f);
93+
overrideAttrs = f: makeCustomizable (freecad.overrideAttrs f);
94+
};
95+
in
96+
{
97+
inherit makeCustomizable;
98+
}

pkgs/by-name/fr/freecad/package.nix

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{ lib
2+
, callPackage
23
, cmake
34
, coin3d
45
, doxygen
56
, eigen
67
, fetchFromGitHub
78
, fmt
8-
, freecad
99
, gfortran
1010
, gts
1111
, hdf5
@@ -22,7 +22,6 @@
2222
, opencascade-occt_7_6
2323
, pkg-config
2424
, python311Packages
25-
, runCommand # for passthru.tests
2625
, spaceNavSupport ? stdenv.hostPlatform.isLinux
2726
, stdenv
2827
, swig
@@ -60,8 +59,9 @@ let
6059
scipy
6160
shiboken2
6261
;
62+
freecad-utils = callPackage ./freecad-utils.nix { };
6363
in
64-
stdenv.mkDerivation (finalAttrs: {
64+
freecad-utils.makeCustomizable (stdenv.mkDerivation (finalAttrs: {
6565
pname = "freecad";
6666
version = "1.0rc4";
6767

@@ -131,6 +131,7 @@ stdenv.mkDerivation (finalAttrs: {
131131
patches = [
132132
./0001-NIXOS-don-t-ignore-PYTHONPATH.patch
133133
./0002-FreeCad-OndselSolver-pkgconfig.patch
134+
./0003-Gui-take-in-account-module-path-argument.patch
134135
];
135136

136137
cmakeFlags = [
@@ -174,22 +175,7 @@ stdenv.mkDerivation (finalAttrs: {
174175
ln -s $out/bin/FreeCADCmd $out/bin/freecadcmd
175176
'';
176177

177-
passthru.tests = {
178-
# Check that things such as argument parsing still work correctly with
179-
# the above PYTHONPATH patch. Previously the patch used above changed
180-
# the `PyConfig_InitIsolatedConfig` to `PyConfig_InitPythonConfig`,
181-
# which caused the built-in interpreter to attempt (and fail) to doubly
182-
# parse argv. This should catch if that ever regresses and also ensures
183-
# that PYTHONPATH is still respected enough for the FreeCAD console to
184-
# successfully run and check that it was included in `sys.path`.
185-
python-path =
186-
runCommand "freecad-test-console"
187-
{
188-
nativeBuildInputs = [ freecad ];
189-
} ''
190-
HOME="$(mktemp -d)" PYTHONPATH="$(pwd)/test" FreeCADCmd --log-file $out -c "if not '$(pwd)/test' in sys.path: sys.exit(1)" </dev/null
191-
'';
192-
};
178+
passthru.tests = callPackage ./tests {};
193179

194180
meta = {
195181
homepage = "https://www.freecad.org";
@@ -214,4 +200,4 @@ stdenv.mkDerivation (finalAttrs: {
214200
maintainers = with lib.maintainers; [ gebner srounce ];
215201
platforms = lib.platforms.linux;
216202
};
217-
})
203+
}))
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
callPackage,
3+
}:
4+
{
5+
python-path = callPackage ./python-path.nix { };
6+
modules = callPackage ./modules.nix { };
7+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
freecad,
3+
runCommand,
4+
writeTextFile,
5+
}:
6+
let
7+
mkModule =
8+
n:
9+
writeTextFile {
10+
name = "module-${n}";
11+
destination = "/Init.py";
12+
text = ''
13+
import sys
14+
import os
15+
16+
out = os.environ['out']
17+
f = open(out + "/module-${n}.touch", "w")
18+
f.write("module-${n}");
19+
f.close()
20+
'';
21+
};
22+
module-1 = mkModule "1";
23+
module-2 = mkModule "2";
24+
freecad-customized = freecad.customize {
25+
modules = [
26+
module-1
27+
module-2
28+
];
29+
};
30+
in
31+
runCommand "freecad-test-modules"
32+
{
33+
nativeBuildInputs = [ freecad-customized ];
34+
}
35+
''
36+
mkdir $out
37+
HOME="$(mktemp -d)" FreeCADCmd --log-file $out/freecad.log -c "sys.exit(0)" </dev/null
38+
test -f $out/module-1.touch
39+
test -f $out/module-2.touch
40+
grep -q 'Initializing ${module-1}... done' $out/freecad.log
41+
grep -q 'Initializing ${module-2}... done' $out/freecad.log
42+
''
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
freecad,
3+
runCommand,
4+
}:
5+
# Check that things such as argument parsing still work correctly with
6+
# the above PYTHONPATH patch. Previously the patch used above changed
7+
# the `PyConfig_InitIsolatedConfig` to `PyConfig_InitPythonConfig`,
8+
# which caused the built-in interpreter to attempt (and fail) to doubly
9+
# parse argv. This should catch if that ever regresses and also ensures
10+
# that PYTHONPATH is still respected enough for the FreeCAD console to
11+
# successfully run and check that it was included in `sys.path`.
12+
runCommand "freecad-test-console"
13+
{
14+
nativeBuildInputs = [ freecad ];
15+
}
16+
''
17+
HOME="$(mktemp -d)" PYTHONPATH="$(pwd)/test" FreeCADCmd --log-file $out -c "if not '$(pwd)/test' in sys.path: sys.exit(1)" </dev/null
18+
''

0 commit comments

Comments
 (0)