Compare commits

..
Author SHA1 Message Date
SoftFever 2b59b6e974 Update README.md 2022-07-18 13:03:21 +08:00
Jack Li 8f594471c0 update readme 2022-07-18 12:56:14 +08:00
Jack Li 0977e3077f Add Voron 0.1 and Prusa MK3S support 2022-07-18 12:34:25 +08:00
Jack Li f087cd0db9 fix voron image ratio issue 2022-07-18 11:05:34 +08:00
Jack Li 5e615feef5 reduce image size 2022-07-18 10:02:55 +08:00
Jack Li 5f1725246f Change name so that it can co-exist with official build 2022-07-18 10:02:36 +08:00
Jack Li 829fc90e6f Add missing process file 2022-07-18 10:01:57 +08:00
SoftFever cc94674e39 Update README.md 2022-07-17 20:45:21 +08:00
SoftFever f16521adf6 update build script 2022-07-17 19:30:48 +08:00
SoftFever fc9d18faff Add Voron process settings 2022-07-17 19:24:44 +08:00
Jack Li ff768a6f68 WIP: add voron process setting 2022-07-17 12:53:41 +08:00
Jack Li 9c0f6f1c72 Add Voron machine support 2022-07-17 12:53:06 +08:00
Jack Li 7a7369c9ce Allow users to export .gcode files 2022-07-17 10:33:57 +08:00
Jack Li 687593f8d2 add build scripts 2022-07-17 10:27:18 +08:00
21474 changed files with 510925 additions and 5074008 deletions
-120
View File
@@ -1,120 +0,0 @@
#
# http://clang.llvm.org/docs/ClangFormatStyleOptions.html
#
Language: Cpp
AccessModifierOffset: -4
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: true
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: DontAlign
AlignOperands: true
AlignTrailingComments: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortBlocksOnASingleLine: true
AllowShortCaseLabelsOnASingleLine: true
AllowShortFunctionsOnASingleLine: All
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: false
BinPackArguments: true
BinPackParameters: false
BraceWrapping:
AfterClass: true
AfterControlStatement: false
AfterEnum: false
AfterFunction: true
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: true
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: false
SplitEmptyRecord: false
SplitEmptyNamespace: false
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Custom
BreakBeforeInheritanceComma: false
BreakInheritanceList: BeforeColon
BreakBeforeTernaryOperators: false
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeComma
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: true
ColumnLimit: 140
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: true
ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
ForEachMacros:
- forever
- foreach
- Q_FOREACH
- BOOST_FOREACH
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '^<Q.*'
Priority: 200
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
Priority: 3
- Regex: '.*'
Priority: 1
IncludeIsMainRegex: '(Test)?$'
IndentCaseLabels: false
IndentPPDirectives: None
IndentWidth: 4
IndentWrappedFunctionNames: false
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
#KeepLineBreaksForNonEmptyLines: false
KeepEmptyLinesAtTheStartOfBlocks: false
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 4
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PenaltyBreakAssignment: 150
PenaltyBreakBeforeFirstCallParameter: 300
PenaltyBreakComment: 500
PenaltyBreakFirstLessLess: 400
PenaltyBreakString: 600
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 50
PenaltyReturnTypeOnItsOwnLine: 300
PointerAlignment: Left
ReflowComments: true
SortIncludes: false
SortUsingDeclarations: false
SpaceAfterCStyleCast: true
SpaceAfterTemplateKeyword: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInContainerLiterals: false
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Cpp11
TabWidth: 4
UseTab: Never
-19
View File
@@ -1,19 +0,0 @@
---
allowed-tools: Bash(git checkout --branch:*), Bash(git add:*), Bash(git status:*), Bash(git push:*), Bash(git commit:*), Bash(gh pr create:*)
description: Commit, push, and open a PR
---
## Context
- Current git status: !`git status`
- Current git diff (staged and unstaged changes): !`git diff HEAD`
- Current branch: !`git branch --show-current`
## Your task
Based on the above changes:
1. Create a new branch if on main
2. Create a single commit with an appropriate message
3. Push the branch to origin
4. Create a pull request using `gh pr create`
5. You have the capability to call multiple tools in a single response. You MUST do all of the above in a single message. Do not use any other tools or do anything else. Do not send any other text or messages besides these tool calls.
-38
View File
@@ -1,38 +0,0 @@
---
allowed-tools: Bash(gh issue view:*), Bash(gh search:*), Bash(gh issue list:*), Bash(gh api:*), Bash(gh issue comment:*)
description: Find duplicate GitHub issues
---
Find up to 3 likely duplicate issues for a given GitHub issue.
To do this, follow these steps precisely:
1. Use an agent to check if the Github issue (a) is closed, (b) does not need to be deduped (eg. because it is broad product feedback without a specific solution, or positive feedback), or (c) already has a duplicates comment that you made earlier. If so, do not proceed.
2. Use an agent to view a Github issue, and ask the agent to return a summary of the issue
3. Then, launch 5 parallel agents to search Github for duplicates of this issue, using diverse keywords and search approaches, using the summary from #1
4. Next, feed the results from #1 and #2 into another agent, so that it can filter out false positives, that are likely not actually duplicates of the original issue. If there are no duplicates remaining, do not proceed.
5. Finally, comment back on the issue with a list of up to three duplicate issues (or zero, if there are no likely duplicates)
Notes (be sure to tell this to your agents, too):
- Use `gh` to interact with Github, rather than web fetch
- Do not use other tools, beyond `gh` (eg. don't use other MCP servers, file edit, etc.)
- Make a todo list first
- For your comment, follow the following format precisely (assuming for this example that you found 3 suspected duplicates):
---
Found 3 possible duplicate issues:
1. <link to issue>
2. <link to issue>
3. <link to issue>
This issue will be automatically closed as a duplicate in 3 days.
- If your issue is a duplicate, please close it and 👍 the existing issue instead
- To prevent auto-closure, add a comment or 👎 this comment
🤖 OrcaSlicer bot
---
-40
View File
@@ -1,40 +0,0 @@
---
allowed-tools: Bash(gh issue list:*), Bash(gh issue view:*), Bash(gh issue edit:*), TodoWrite
description: Triage GitHub issues and label critical ones for oncall
---
You're an oncall triage assistant for GitHub issues. Your task is to identify critical issues that require immediate oncall attention and apply the "oncall" label.
Repository: OrcaSlicer/OrcaSlicer
Task overview:
1. First, get all open bugs updated in the last 3 days with at least 50 engagements:
```bash
gh issue list --repo OrcaSlicer/OrcaSlicer --state open --label bug --limit 1000 --json number,title,updatedAt,comments,reactions | jq -r '.[] | select((.updatedAt >= (now - 259200 | strftime("%Y-%m-%dT%H:%M:%SZ"))) and ((.comments | length) + ([.reactions[].content] | length) >= 50)) | "\(.number)"'
```
2. Save the list of issue numbers and create a TODO list with ALL of them. This ensures you process every single one.
3. For each issue in your TODO list:
- Use `gh issue view <number> --repo OrcaSlicer/OrcaSlicer --json title,body,labels,comments` to get full details
- Read and understand the full issue content and comments to determine actual user impact
- Evaluate: Is this truly blocking users from using Claude Code?
- Consider: "crash", "stuck", "frozen", "hang", "unresponsive", "cannot use", "blocked", "broken"
- Does it prevent core functionality? Can users work around it?
- Be conservative - only flag issues that truly prevent users from getting work done
4. For issues that are truly blocking and don't already have the "oncall" label:
- Use `gh issue edit <number> --repo OrcaSlicer/OrcaSlicer --add-label "oncall"`
- Mark the issue as complete in your TODO list
5. After processing all issues, provide a summary:
- List each issue number that received the "oncall" label
- Include the issue title and brief reason why it qualified
- If no issues qualified, state that clearly
Important:
- Process ALL issues in your TODO list systematically
- Don't post any comments to issues
- Only add the "oncall" label, never remove it
- Use individual `gh issue view` commands instead of bash for loops to avoid approval prompts
@@ -1,86 +0,0 @@
# DELEGATION SPECIFICATION: HARNESS-DRIVEN VALIDATION LOOP
slug: sketch-focus-arbiter · repo: /home/tommaso/projects/apps/orca_cad · branch: cad-mainline
## 1. TARGET GOAL
**Functional Objective.** Keyboard input in the Design tab is routed by WHAT THE KEY IS, not by
which widget the window manager decided to focus. Adopted from FreeCAD's
`DrawSketchKeyboardManager::detectKeyboardEventHandlingMode`
(src/Mod/Sketcher/Gui/DrawSketchKeyboardManager.cpp), which never queries focus at all:
- digit, `-`, `.`, `,` -> the open value field
- Backspace / Delete -> the open value field (when one is open)
- Enter / Return / Tab -> commit the field, control returns to the view
- a letter -> the sketch-tool shortcut map, as today
- Esc -> the existing CadLevel LIFO (DesignInteraction.hpp), unchanged
- anything else -> sticky: whoever had it keeps it
Observable postcondition: for EVERY sketch tool that opens a value field, a value typed
immediately after the field appears — with NO click into the field — is the value committed.
Today the prefill is committed instead whenever the WM withholds focus.
**Target Files / Scope (writable).**
src/slic3r/GUI/CAD/DesignPanel.cpp (the arbiter lives in the existing wxEVT_CHAR_HOOK)
src/slic3r/GUI/CAD/DesignCanvas.cpp/.hpp (forwarding entry points only)
src/slic3r/GUI/CAD/SketchInlineEditor.cpp/.hpp (accept a programmatically delivered character)
scripts/CAD/check-gui-click-edit.py (F2P oracle — authoring exception, see §4)
Everything else read-only. No dependency additions, no reformatting.
**Open Bindings.**
- The in-canvas ImGui field on wip/in-canvas-value-field is NOT in scope. Default: the arbiter
is implemented against the CURRENT wxFrame field on cad-mainline, because content-based
routing makes the window's focus irrelevant either way. If it later moves in-canvas the
arbiter is unchanged.
- Tools whose field is opened by a toolbar button rather than a gesture (Constrain path) are
covered by the same arbiter but are not in the F2P tool list. Default: assert them in P2P only.
## 2. HARNESS ENVIRONMENT & GROUND TRUTH
The rig container `orcacad-gui` on nativedev IS the harness. Xvfb `:11` + openbox, the app under
test, `xdotool` for synthetic input, and an MCP socket at `/tmp/mcp.sock` that reports sketch
state as JSON. It is a closed loop: drive input, read geometry back, assert. No window manager
politics, no human.
Harness interface (ordered; each slot one invocation, one exit code):
S1 sync docker cp <file> orcacad-gui:/OrcaSlicer/<path>
S2 build docker exec orcacad-gui ninja -C /OrcaSlicer/build orca-slicer
S3 restart docker exec orcacad-gui /OrcaSlicer/scripts/CAD/start-headless-gui.sh
S4 F2P docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-click-edit.py --attach
S5 P2P docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-sketching.py
**F2P.** `scripts/CAD/check-gui-click-edit.py`. For each of Line, Rectangle, Circle, Slot,
Polygon, Ellipse and Rounded rectangle: arm the tool, draw it, and type a value that differs
from the prefill WITHOUT clicking the field. Assert the committed value equals the typed value.
The ladder must FAIL against unmodified cad-mainline — that is what proves it asserts something.
**P2P.** `scripts/CAD/check-gui-sketching.py`, the existing gesture ladder, minus anything red at
baseline. NOTE: it calls `focus_field()` — one click into the field before typing — which is the
workaround this whole task removes. It stays green as a regression guard; it is NOT evidence.
**Test Integrity Constraint.** `focus_field()` in check-gui-sketching.py must NOT be deleted to
make things pass, and check-gui-click-edit.py must NOT be weakened. Either invalidates the run.
## 3. VERIFICATION COMMANDS
1. Static: `docker exec orcacad-gui ninja -C /OrcaSlicer/build orca-slicer` (warnings delta only;
this repo configures no linter — the compiler is the static gate. Absolute-zero is NOT the gate.)
2. Harness: `docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-click-edit.py --attach`
3. Regression: `docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-sketching.py`
## 4. CONVERGENCE LOOP — ceiling 8 iterations
EDIT (scoped) -> EXECUTE S1..S5 -> PARSE the ladder's per-tool assertions and the [UX]/[KEYTRACE]
lines -> PATCH from the parsed cause. On ceiling without convergence: stop, report the last diff
and the unresolved failure set. Do not report success.
F2P authoring exception: check-gui-click-edit.py is writable, and must be shown RED against
unmodified source before any source edit counts.
## 5. TERMINATION CRITERIA
- [ ] S2 exits 0, and introduces no compiler warning absent from the baseline.
- [ ] S4 ALL_PASSED — every tool commits the typed value, no click into the field.
- [ ] S5 shows zero regressions against its recorded baseline pass count.
- [ ] F2P proven red without the fix (source stashed, ladder re-run, must FAIL).
## 6. GUARDRAILS
Zero-assumption: no completion claim without captured stdout and exit codes. Oracle supremacy:
the ladder's verdict overrides my judgement. Blast radius: §1 files only. Baseline obligation:
run §3 once before the first edit and record it.
-117
View File
@@ -1,117 +0,0 @@
---
name: orca-profiles
description: Use when creating, modifying, reviewing or debugging OrcaSlicer FFF system profiles under resources/profiles, including printer/vendor/nozzle/material additions, bundle indexes and versions, preset renames, setting_id and filament_id. Also use for missing presets or vendors, ignored profile settings, ambiguous AMS filament matches, and failures from orca_profile_tool.py, check_profile.sh/.bat, OrcaSlicer_profile_validator or the Check profiles CI job.
---
# OrcaSlicer system profiles
A bundle is `resources/profiles/<Vendor>.json` plus `<Vendor>/`. The vendor id is the
filename stem, not the index's display `name`. The index is the loader's only entry point:
unindexed presets never load. `OrcaFilamentLibrary` is the shared filament bundle;
`blacklist.json` is data, not a bundle.
## Choose the reference for the task
Read the relevant reference before editing; load others only when the task crosses those areas.
Paths below are relative to this skill. Commands run from the repository root.
| Task | Read |
| --- | --- |
| Add or tune a filament, brand or material; fix compatibility / alias shadowing | [filament-profiles.md](references/filament-profiles.md) |
| Add a printer or nozzle; change models, variants, assets or extruder vectors | [machine-profiles.md](references/machine-profiles.md) |
| Add a quality tier or tune a process | [process-profiles.md](references/process-profiles.md) |
| Create a vendor bundle; diagnose loading or inheritance; migrate preset names | [vendor-bundle.md](references/vendor-bundle.md) |
| Change ids; diagnose AMS identity | [ids.md](references/ids.md), then `docs/HLSD/filament_id.md` for identity changes |
| Review a profile diff | [review-checklist.md](references/review-checklist.md) |
| Run checks, interpret failures, test another tree or verify in the app | [validation.md](references/validation.md) |
## Golden rules
1. **Bump every changed bundle's `version`**, including `OrcaFilamentLibrary.json` when affected.
Increment the last component; carry `.99` into the third component (`02.04.00.99`
`02.04.01.00`). The updater requires a strictly newer version. CI does not check this.
2. **Register every preset, bases included, parents before children.** `update-index` generates
the four `*_list` arrays; `check` requires its output. Index names must equal file `name` fields.
3. **Generate ids; never invent or copy them.** Keep existing ids during ordinary tuning. New
presets normally omit them until `generate-id`; bases must have no `setting_id`.
BBL's authoritative `setting_id` and a wrongly inherited `filament_id` need the explicit
handling in [ids.md](references/ids.md).
4. **Load failures can discard a whole vendor bundle.** Broken `inherits`, missing indexed files,
duplicate names, invalid model/variant references and unresolved filament ids affect more than
the edited preset. Inheritance stays within a bundle, except filaments may inherit the library.
5. **Preserve shipped selectable names.** Renaming, deleting or changing `instantiation` from
`"true"` to `"false"` needs `renamed_from` on a selectable successor. It is a `;`-separated string;
update in-tree references too. See [migration rules](references/vendor-bundle.md#renamed_from).
6. **Compatibility uses exact printer variant names.** Every instantiated non-library filament
needs a non-empty `compatible_printers` in its own file. Library fallbacks may omit it;
library printer-specific tunes use a non-empty list. Keep same-product tunes disjoint.
7. **Preset values are strings or arrays of strings.** Use `"instantiation": "false"`, not `false`.
Model `nozzle_diameter` is a `;`-separated string; machine `nozzle_diameter` is an array.
Wrong types can abort loading; see [failure scopes](references/vendor-bundle.md#failure-modes-ranked-by-blast-radius).
8. **Verify setting keys against the code.** Unknown keys are silently discarded. Check
`PrintConfig.cpp` definitions and `PrintConfigDef::handle_legacy`; neighbours can contain dead
keys. `normalize` removes known obsolete keys, but does not detect arbitrary misspellings.
9. **Run the full profile checks before reporting completion.** A vendor-scoped pass is only a
development loop. Review also covers version bumps, assets, non-default processes and hardware
tuning that CI cannot establish.
## Creating or modifying a profile
1. **Inspect the diff and neighbouring presets.** Read their `name`, parent chain and children;
edits to a base or a leaf with descendants propagate. Match the bundle's structure and write
only overrides. New files use tab indentation, LF and a trailing newline; preserve unrelated
formatting in existing files. Match filename case exactly and use cross-platform names.
2. **Author explicit metadata.** Set `type` yourself, especially for `machine` vs `machine_model`.
Use `"from": "system"` and string `instantiation` on config presets. Omit ids on new presets
unless [ids.md](references/ids.md) requires special handling; retain them on existing ones.
Complete compatibility, defaults, assets and any rename migration using the task reference.
3. **Bump the version**, then run the authoring commands in order for each affected bundle:
```bash
python3 scripts/orca_profile_tool.py normalize --vendor "<Vendor>"
python3 scripts/orca_profile_tool.py update-index --vendor "<Vendor>"
python3 scripts/orca_profile_tool.py generate-id --vendor "<Vendor>"
python3 scripts/orca_profile_tool.py check
```
Writing commands support `--dry-run`. Inspect their diffs: `normalize` changes content and can
reformat entire files. Stop and resolve command errors before proceeding.
**Do not use `trim` in this workflow:** it can delete newly authored, unindexed profiles.
Do not use `normalize --force` for routine edits.
4. **Validate:**
```bash
./scripts/check_profile.sh --vendor "<Vendor>" # development loop
./scripts/check_profile.sh # full tree before the PR
```
On Windows use `py -3` instead of `python3`, and `scripts\check_profile.bat -Vendor "<Vendor>"`
/ `scripts\check_profile.bat`. Logs: `.test/check_profiles/logs/<check>.log`.
Id checks remain tree-wide under `--vendor`; filament-only bundles skip the default slice check.
See [validation.md](references/validation.md) for flags, coverage and error remedies.
5. **Verify the changed behavior.** Slice newly added non-default processes explicitly, and
[test in the app](references/validation.md#testing-in-the-app) for selection or UI behavior.
Report checks actually run, failures/skips and any hardware tuning still unverified.
## Symptom → first reference
| Symptom | Start here |
| --- | --- |
| A vendor disappears | Loader log / `validate_system`; [bundle failure scopes](references/vendor-bundle.md#failure-modes-ranked-by-blast-radius) |
| A setting has no effect | Key spelling/type, `handle_legacy`, or a config key placed on a `machine_model` |
| A preset exists but is not selectable | Index registration, `instantiation`, installation and compatibility |
| A filament is missing, duplicated, or matches the wrong spool | [Compatibility and alias shadowing](references/filament-profiles.md#compatible_printers); [ids](references/ids.md) |
| A bed temperature is ignored | [Plate-specific temperature keys](references/filament-profiles.md#bed-temperature-is-twelve-keys-not-one) |
| A change is absent from the running app | Version bump and [installed profile location](references/validation.md#testing-in-the-app) |
| A check fails | [Error → remedy](references/validation.md#error--remedy) |
## Source of truth
When guidance and behavior disagree, inspect the current checkout:
`scripts/orca_profile_tool.py` for tooling and flags; `src/libslic3r/Preset*.cpp` for loading and
compatibility; `src/libslic3r/PrintConfig.cpp` for setting types and legacy handling;
`src/dev-utils/OrcaSlicer_profile_validator.cpp` and `.github/workflows/check_profiles.yml` for
validation coverage. `docs/HLSD/filament_id.md` defines filament identity. The
[profile development guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/how_to_create_profiles.md)
is a tutorial; confirm loader and CLI details against these sources.
@@ -1,207 +0,0 @@
# Filament profiles and OrcaFilamentLibrary
`OrcaFilamentLibrary` is the filament-only bundle the loader reads **first**; its config map
becomes the base bundle, so any vendor may inherit a library preset by name. It is the only cross-bundle
parent — vendor-to-vendor inheritance always fails.
## Where a filament goes
| Contribution | Location |
| --- | --- |
| Generic material for all printers | `OrcaFilamentLibrary/filament/Generic <mat> @System.json` |
| A brand's product, all printers | `OrcaFilamentLibrary/filament/<Brand>/` |
| A brand's tune for one printer | `OrcaFilamentLibrary/filament/<Brand>/<PrinterVendor>/` — recommended; `<PrinterVendor>/filament/<Brand>/` also works |
| A printer vendor's tune of a generic or its own product | `<Vendor>/filament/` |
Both locations for the last-but-one row are supported: `OrcaFilamentLibrary/filament/<Brand>/<PrinterVendor>/<Name>.json`
(the shape the wiki shows) and `<PrinterVendor>/filament/<Brand>/`. The library path is the one a
filament vendor should contribute to — `OrcaFilamentLibrary/filament/<Brand>/` is the brand's own
folder, while a printer vendor's folder belongs to that printer vendor. Brand tunes do ship under
printer vendors' folders today (Polymaker and SUNLU among others).
Library layout: `filament/base/fdm_filament_*.json` type roots, root-level `Generic <mat> @System.json`
generics, and one subfolder per brand, which may nest printer-specific tunes one level deeper. Adding
a brand means adding a folder here; the folder name is a directory label only — `filament_vendor` inside the JSON is the real vendor string.
## The three-part shape
```jsonc
// Fiberon PA6-CF @base.json — the product root, holds identity + material values
{ "type": "filament", "name": "Fiberon PA6-CF @base", "from": "system",
"instantiation": "false", "inherits": "fdm_filament_pa",
"filament_id": "OFkOviHk", // generated here; variants inherit it
"filament_vendor": ["Polymaker"], "filament_type": ["PA6-CF"], /* */ }
// Fiberon PA6-CF @System.json — the selectable shim, 7 keys
{ "type": "filament", "name": "Fiberon PA6-CF @System", "from": "system",
"instantiation": "true", "inherits": "Fiberon PA6-CF @base",
"setting_id": "…", "compatible_printers": [] }
// <PrinterVendor>/filament/Polymaker/Fiberon PA6-CF @BBL X1C.json — a printer tune
{ "inherits": "Fiberon PA6-CF @base", "filament_max_volumetric_speed": ["14"],
"compatible_printers": ["Bambu Lab X1 Carbon 0.4 nozzle", ] }
```
- `@base` is the convention for a root. A base carries **no** `setting_id`, no `compatible_printers`, no
`filament_settings_id`. Only the `setting_id` half is enforced, and nothing violates it; the other two
are unchecked and plenty of bases still carry them. Do not copy that from a neighbouring file.
- Every `@System` must be `"instantiation": "true"`. DREMC ships `@System` presets set to `"false"`,
which therefore ship but can never be selected; no check catches it.
- A duplicated brand `@base` across bundles is normal and intentional (`Fiberon PA6-CF @base` exists in
both the library and BBL with the same id, differing only in MVS) — bases never enter the preset
collection, so there is no duplicate-name error.
- You may inherit from an instantiated preset as well as from a base; it is common.
## The two most common contributions
**A printer vendor tuning a generic.** Keep the `Generic X` base name so the alias shadows the library
preset on your printers, inherit `Generic X @System`, declare **no** `filament_id` (inheriting the
library's is correct — the product really is the library's generic), and give it a non-empty
`compatible_printers` in its own body:
```jsonc
// <Vendor>/filament/Generic PETG @Acme One 0.4 nozzle.json
{ "type": "filament", "name": "Generic PETG @Acme One 0.4 nozzle", "from": "system",
"instantiation": "true", "inherits": "Generic PETG @System",
"filament_flow_ratio": ["0.95"], "filament_max_volumetric_speed": ["10"],
"compatible_printers": ["Acme One 0.4 nozzle"] }
```
**A printer vendor's own branded product.** Give it a `@base` root so `generate-id` can mint the id (see
[ids.md](ids.md) — inheriting `Generic X @System` directly makes the id unfixable by the tool), then one
instantiated leaf per printer in the same bundle. No `@System` shim: that is only for a product entering
OrcaFilamentLibrary.
```jsonc
// <Vendor>/filament/Acme Aura PETG @base.json — instantiation false, no setting_id
{ "type": "filament", "name": "Acme Aura PETG @base", "from": "system",
"instantiation": "false", "inherits": "fdm_filament_pet",
"filament_vendor": ["Acme"], "filament_type": ["PETG"] } // filament_id minted here
// <Vendor>/filament/Acme Aura PETG @Acme One 0.4 nozzle.json
{ "type": "filament", "name": "Acme Aura PETG @Acme One 0.4 nozzle", "from": "system",
"instantiation": "true", "inherits": "Acme Aura PETG @base",
"filament_max_volumetric_speed": ["11"],
"compatible_printers": ["Acme One 0.4 nozzle"] }
```
Omit `filament_settings_id` from new presets — it is runtime bookkeeping the app rewrites to the preset
name.
## `compatible_printers`
- **Library fallbacks:** empty `[]` or absent, so they are offered on all printers except where
[alias shadowing](#alias-shadowing) supplies a printer-specific tune.
- **Library printer-specific tunes:** non-empty, listing exact printer **variant** names. These can
supersede a same-alias fallback just like a tune in a printer vendor's bundle.
- **Instantiated filaments in every other vendor:** non-empty, listing exact printer **variant** names.
Enforced twice but not identically: the C++ `has_errors` reads the *flattened* config, so an inherited list satisfies it,
while the Python check reads the file's **own** key. Write the list in the file itself. This is the
most common filament CI failure.
- Emptying it to "make it apply everywhere" fails that check *and* creates a duplicate-`filament_id`
collision against the library generic on every printer.
- Copying a base's full printer list onto a nozzle-specific variant produces duplicate combobox entries —
a real shipped bug twice over.
## Alias shadowing
A printer-specific filament in either the library or a vendor bundle supersedes the library fallback
on the printers it lists. The matching key is the **alias**: the preset name up to the **first** `@`,
right-trimmed (no `@` → the whole name). So
`QIDI ABS-GF@Q2-Series` aliases to `QIDI ABS-GF`.
A library preset with an empty `compatible_printers` collects, into `m_excluded_from`, every printer named
by any same-alias preset that *has* a non-empty list, and is then hidden on those printers.
Two consequences:
- **Only an unrestricted library fallback can be shadowed.** Two printer-specific presets sharing
an alias do not exclude each other — overlapping lists for the same product trip the
duplicate-`filament_id` check instead.
- This is why adding `Generic PLA @<printer>` to a vendor silently removes the library `Generic PLA`
from that printer. Intended — and the reason a vendor tuning a generic must **keep the `Generic X`
base name**.
The literal spelling `Generic <mat> @System` is load-bearing beyond shadowing: `find_preset2` rewrites an
unresolved name containing "Generic" into that form and retries against the library, which is how 3MF
and project recovery works.
## `filament_id`, `filament_vendor`, `filament_type`
`filament_id` is minted from the triple `(filament_vendor, filament_type, name-before-first-@)`.
`filament_vendor` and `filament_type` are therefore **identity, not decoration** — editing either
re-mints the id. Read `docs/HLSD/filament_id.md` before changing any of them, and see
[ids.md](ids.md) for the tooling.
A filament with no resolvable `filament_id` anywhere in its `inherits` chain is a **hard load error** that
discards the vendor bundle. The id inherits across bundles, so a vendor's `Generic ABS @X` inheriting
`Generic ABS @System` gets the library's id for free; a vendor's own product must resolve its own.
- `filament_type` **must be a JSON array** — the one vector key the Python check enforces. A scalar
`"PP"` once hung the filament/printer selection UI.
- It is an **open** enum: an unlisted value is accepted silently and falls back to 190300 °C defaults
and adhesion 1.0. Off-list values do ship. Prefer a value from `MaterialType::all()` in
`src/libslic3r/MaterialType.cpp`, or add a row there.
- Generics use `filament_vendor: ["Generic"]`, which `fdm_filament_common` already defaults to.
## `"nil"`
Legal in any key whose `ConfigOptionDef` is `nullable`. In a filament preset that is most of the
`filament_*` family, plus `long_retractions_when_ec` and `retraction_distances_when_ec`. About half are
the extruder overrides (`filament_retraction_length`, `filament_z_hop`, `filament_wipe`,
`filament_retract_*`, `filament_retraction_speed`, `filament_deretraction_speed`,
`filament_retraction_minimum_travel`, `filament_wipe_distance`, `filament_long_retractions_when_cut`,
`filament_retraction_distances_when_cut`, …), where `nil` means *keep the printer/extruder's own value*.
The rest are ordinary nullable options (`filament_flow_ratio`, `filament_flush_temp`,
`filament_adaptive_volumetric_speed`, …) where it means *unset*.
Anywhere else it throws `Deserializing nil into a non-nullable object`. To not set a non-nullable key,
omit it — do not write `nil`.
## What to review per nozzle
Across `@X` / `@X 0.N nozzle` sibling pairs the keys that differ, most often first, are
`filament_max_volumetric_speed`, `filament_retraction_length`, `slow_down_min_speed`,
`filament_flow_ratio`, `slow_down_layer_time`, `nozzle_temperature` and `pressure_advance`.
`filament_cost`, `filament_density`, `filament_type` and `filament_vendor` belong on the `@base` and
should not appear in a printer tune.
Use measured values for the material, hotend, extruder and nozzle combination. Neither maximum
volumetric speed nor pressure advance has a universal nozzle-only lookup table. When cloning a
0.4 preset for a 0.2 nozzle, explicitly revisit flow limits; do not infer a pressure-advance value
or a required direction of change from diameter alone.
## Style
Overrides, not full copies: a typical instantiated filament preset carries around a dozen non-meta keys,
and a library leaf two or three. Presets that restate fifty-plus keys from their parent do still ship —
Phrozen's single filament preset is that style — but they are the pattern to move away from, not to
copy. Commit `6943b6ddc3` is the stated model (flip true bases to `instantiation: "false"`, strip
`compatible_printers`/`setting_id`/`filament_settings_id`, add `renamed_from` on the survivor).
Prefer the library's `fdm_filament_*` bases over a vendor-local copy. Phrozen's local
`fdm_filament_common` has drifted from the library's.
Canonical key order, written by `orca_profile_tool.py normalize` when it rewrites a file: `type`, `name`,
`renamed_from`, `inherits`, `from`, `setting_id`, `filament_id`, `instantiation`, then everything else in
the order you wrote it. Not enforced — a file that leads with `compatible_printers` passes `check`.
**Every vector-typed (`co*s`) key must be a JSON array.** Only `filament_type` is an outright error, but
`normalize` silently arrayifies five more (`filament_cost`, `filament_density`,
`temperature_vitrification`, `filament_max_volumetric_speed`, `filament_vendor`) and `check` fails when
it would. Every other vector key is on you — including `filament_start_gcode`, `filament_end_gcode`,
`filament_extruder_variant`, `compatible_printers` and the plate temperatures.
## Bed temperature is twelve keys, not one
There is no single "bed temperature". Which plate key applies depends on `curr_bed_type`, whose six
selectable values (`btPC`, `btEP`, `btPEI`, `btPTE`, `btPCT`, `btSuperTack`; `btDefault` maps to no key)
`get_bed_temp_key()` turns into `cool_plate_temp`, `eng_plate_temp`, `hot_plate_temp`,
`textured_plate_temp`, `textured_cool_plate_temp` and `supertack_plate_temp` — each with an
`*_initial_layer` twin.
`textured_cool_plate_temp` is the one most often forgotten. A printer with `support_multi_bed_types` off
hides the selector, and the printer preset's
`default_bed_type` decides which plate is selected for it, but `curr_bed_type` can still hold a stale
value carried over from another printer — so set every plate the printer plausibly has, as the sibling
presets in the bundle do.
@@ -1,149 +0,0 @@
# `setting_id` and `filament_id`
Orca-generated ids are deterministic hashes of identity. **Never invent an id or copy a sibling's
`setting_id`.** Use `scripts/orca_profile_tool.py`; the two special cases are
[a wrongly inherited filament id](#what-generate-id-does-and-does-not-fix) and
[BBL's authoritative setting ids](#bbls-exception-precisely).
`docs/HLSD/filament_id.md` is the authoritative design document for `filament_id` — the id landscape, the
checks CI runs, and the Bambu catalog map. This page is the tooling half.
| | `setting_id` | `filament_id` |
| --- | --- | --- |
| Identifies | one selectable preset | one filament **product** |
| Key hashed | `<vendor folder>/<type>/<name>` | `filament_product/<filament_vendor>/<filament_type>/<name-before-@>` |
| Shape | 16 base62 chars | `OF` + 6 base62 chars |
| Required on | every `instantiation: "true"` preset | every **instantiated** filament, own or inherited |
| Forbidden on | bases (`instantiation != "true"`) | — (a base is exactly where it belongs) |
| Scope | globally unique across the tree | shared by every variant of the product, in every bundle |
`<type>` is `machine` / `process` / `filament` — the vendor is the **folder** name (`BBL`), not the
display name (`Bambulab`). Renaming a preset changes its `setting_id`; renaming a filament, or editing
its `filament_vendor` or `filament_type`, also changes its `filament_id`.
## The tool
Use `scripts/orca_profile_tool.py` with a subcommand:
| Command | Does |
| --- | --- |
| `check` | everything CI's `profile_tool` step runs — see [validation.md](validation.md) |
| `generate-id` | writes `setting_id` and `filament_id` |
| `normalize` | rewrites profile files into their canonical shape |
| `trim` | deletes profile files no `<vendor>.json` list references |
| `update-index` | rebuilds the `*_list` sections from the files on disk |
The order after adding, renaming or deleting files — each step feeds the next, so it is not
interchangeable — is `normalize``update-index``generate-id``check`.
The [authoring workflow](../SKILL.md#creating-or-modifying-a-profile) has the commands.
> **`trim` deletes.** It removes every profile file the index does not list — including the one you just
> added and have not registered yet. Register first, or skip `trim` entirely; it is a cleanup sweep, not
> part of landing a profile. Preview with `--dry-run`.
**Register, then mint.** The `filament_id` pass reads `<Vendor>.json`'s `filament_list`, not the
filesystem (the `setting_id` pass walks the filesystem, so a bundle whose index has not landed yet is
still assignable). A new filament file is therefore invisible to `generate-id`'s filament_id pass until
it is registered — its `setting_id` is written regardless.
- `--dry-run` works on every writing command (`generate-id`, `normalize`, `trim`, `update-index`)
and writes nothing.
- `--filament-id` / `--setting-id` narrow `generate-id`; they exclude each other, and passing neither
writes both.
- `--vendor` is repeatable and narrows **only what is written** — the id is a function of the triple
alone, so a narrowed run writes exactly what a full run would. An unknown vendor exits 1 before any
write. `--vendor` on `check` narrows the per-vendor checks only; the `setting_id` and `filament_id`
passes stay tree-wide.
- `--profiles DIR` points any command at another tree — see
[Checking a copy of the tree](validation.md#checking-a-copy-of-the-tree).
- `--profile-type` narrows `normalize`, `trim` and `update-index` to `machine_model`, `process`,
`filament` or `machine`.
- Exit codes: 0 clean, 1 errors found (`generate-id` still writes what it could), 2 argparse misuse.
- Output is ANSI-coloured; searching for the literal `[ERROR]` still works.
`generate-id` is **idempotent and byte-preserving** — BOM and CRLF kept, one key line touched per pass.
A legitimate `generate-id` diff is one or two changed lines per file: a new instantiated filament gets
both a `filament_id` and a `setting_id`, and a BBL file with a misspelled `settings_id` has that line
dropped and its value restored under the right key. `normalize` is the opposite by design — it rewrites
whole files into canonical shape — which is why `check` demands it already be a no-op. Some bundles have
CRLF committed (OrcaFilamentLibrary, Anycubic and RH3D among them), so a `normalize` pass there rewrites
every line — read the diff before committing it.
On a clean tree `check` and `generate-id --dry-run` both exit 0 with zero findings. That is the
baseline to restore before opening a PR.
## What `generate-id` does and does not fix
Writes:
- a `setting_id` into any instantiated preset that lacks one, or whose value does not match the formula;
- strips a `setting_id` from a base;
- deletes the misspelled `settings_id` key;
- a `filament_id` into the id-less **root(s)** of an instantiated filament that resolves none;
- rewrites a **declared** `filament_id` that is not the mint of its own triple.
Refuses to write (reports only): a base62 collision between two products, an empty `filament_vendor` or
`filament_type`, a broken `inherits` chain, roots of one filament resolving divergent `(vendor, type)`
pairs.
**Does not fix: a preset that *inherits* a wrong `filament_id`.** This is check 2b, and it is the trap
most likely to bite. It happens when a branded filament inherits a generic for its settings:
```jsonc
{ "name": "Phrozen Aura PETG @Phrozen Arco 0.4 nozzle",
"inherits": "Generic PETG @System" } // resolves the OFL generic's id — wrong product
```
The preset resolves *an* id, so `generate-id` neither inserts nor rewrites, and `check` fails with
`inherits filament_id "X" but its own triple "V/T/N" mints "Y"`.
Two fixes, in order of preference:
1. **Give the product a `@base` root** inheriting a material base (`fdm_filament_pet`,
`fdm_filament_pla`, …). No `fdm_filament_*` base carries a `filament_id`, so the filament now resolves
none and `generate-id` mints it for you. This is also the shape the rest of the tree uses.
2. **Declare the tool-computed key on the preset itself.** Use the expected value reported by `check`
or compute it with the function below; this is not a manually chosen id. Make sure the preset
resolves the right `filament_vendor` and `filament_type` first — with
neither set, the triple resolves through the generic parent and the branded product is minted
under vendor `Generic`. If you need the id before the file exists:
```bash
python3 -c "import sys; sys.path.insert(0,'scripts'); from orca_profile_tool import generate_filament_id as g; print(g('Polymaker','PLA','PolyLite PLA'))"
# -> OF5CgdDq
```
The quoting works unchanged in cmd and PowerShell; only swap `python3` for `py -3`.
The `setting_id` equivalent is `generate_preset_setting_id('<vendor folder>', '<type>', '<name>')`.
## BBL's exception, precisely
`RESERVED_VENDORS = {"BBL"}` covers **`setting_id` assignment only**, keyed on the *folder* name:
- The tool never mints or replaces a BBL `setting_id`. A new instantiated BBL preset with no
`setting_id` therefore **cannot be fixed by the tool**, yet the presence rule still applies to it —
carry over Bambu's authoritative id by hand.
- BBL is not exempt from anything else: bases still get their `setting_id` stripped, ids must still be
globally unique, and BBL `filament_id`s are minted like everyone else's — every one of them is an
`OF*`.
## Ids other systems compose
No id from another system is the mint of a triple, so `check` rejects it like any other bad id — same
error, same remedy, whoever wrote it. Three such spaces exist near the tree; recognise them so you do
not copy one into a profile:
- **Bambu's `GF*` catalog** — external and opaque, correlated to Orca's ids by the generated
`resources/printers/bambu_filament_ids.json`. `GF` is a *prefix*, not a spelling the tree avoids: most
BBL `setting_id`s start with `G`, and `blacklist.json` and
`BBL/filament/filaments_color_codes.json` both reference Bambu catalog ids by design. The rule is
about `filament_id` and nothing else.
- **Qidi's `QD_*`** — composed at runtime by the box (`QD_<series>_<vendor>_<typeidx>`), not a preset id.
- **`P` + 7 hex, and `"null"`** — what `CreatePresetsDialog.cpp` gives a *user*-created filament.
## Tests
`python3 -m unittest discover -s scripts/tests -t scripts` (`py -3 -m …` on Windows). Note the
`-t scripts` argument; without it the imports fail. CI runs them as the first, non-`continue-on-error`
step of the profile job — see [validation.md](validation.md#ci).
@@ -1,194 +0,0 @@
# Printer models and variants
Both live in `resources/profiles/<Vendor>/machine/*.json`; models go in `machine_model_list`, variants
and shared bases in `machine_list`. Every one of them is registered. Some vendors (Elegoo, Eryone,
InfiMech, FlyingBear) nest a further subfolder under `machine/`, so recurse rather than globbing
`machine/*.json`.
## A `machine_model` is not a config preset
It is parsed by a hand-written key switch, and only these keys are stored (`version` and `url` are
matched and discarded):
`name`, `model_id`, `nozzle_diameter`, `machine_tech`, `family`, `bed_model`, `bed_texture`,
`hotend_model`, `default_materials`, `not_support_bed_type`, `image_bed_type`,
`bottom_texture_end_name`, `bottom_texture_rect`, `bottom_texture_rect_longer`, `middle_texture_rect`,
`use_double_extruder_default_texture`.
**Everything else is silently dropped.** Only `name` and `nozzle_diameter` are required. Dead keys ship
on real models today — `url`, `default_bed_type`, even a `desciption` typo — so a neighbour carrying a
key is no evidence it does anything. Printer config options belong on the `machine` preset, never here.
```json
{
"type": "machine_model",
"name": "Phrozen Arco",
"machine_tech": "FFF",
"family": "Phrozen",
"model_id": "Phrozen Arco",
"nozzle_diameter": "0.4",
"bed_model": "Phrozen Arco_buildplate_model.stl",
"bed_texture": "Phrozen Arco_buildplate_texture.svg",
"hotend_model": "",
"default_materials": "Generic PLA @Phrozen Arco 0.4 nozzle"
}
```
| Field | Notes |
| --- | --- |
| identity | **the `name` of the `machine_model_list` entry**, which is what a variant's `printer_model` must equal. `check_name_consistency` forces it to equal the file's `name`, so they coincide. |
| `model_id` | a *separate* cloud/device printer type. Optional, and not required to be unique. Not the model's identity. Changing it changes device matching. |
| `machine_tech` | only `starts_with("SL")` means SLA; everything else is FFF. Write `FFF`; a few models write `FGF`, which is a label with no effect. |
| `nozzle_diameter` | `;`-separated string, one token per available size. Order is free (Qidi writes `0.4;0.2;0.6;0.8` to put the default first). This list is the authoritative set of legal `printer_variant` values. |
| `default_materials` | `;`-separated filament **preset names**. Used to preselect in the wizard *and* by `PresetBundle::load_installed_filaments` to auto-install a printer's filaments on first run, so a dangling entry costs a real user a filament. Not `,`; case-sensitive (`@System`). `check` fails on a dangling name here or in `default_filament_profile`. |
| `family` | a wizard grouping label only; give every model one. |
### Assets
`bed_model`, `bed_texture` and `hotend_model` are paths relative to the **vendor folder** (by id).
Majority convention: `<Model>_buildplate_model.stl` and `<Model>_buildplate_texture.svg`. An empty string
is the legal "none", and is the norm for `hotend_model`.
**Nothing checks that the file exists.** A missing `hotend_model` falls back to
`resources/profiles/hotend.stl`; a missing `bed_model`/`bed_texture` just renders nothing. Broken
references already ship. Verify by hand.
Every model also has a `<Model>_cover.png` in the vendor folder — treat it as required, not optional.
240×240 is the cap `scripts/optimize_cover_images.py` enforces and the size most covers already use.
A missing cover degrades to a placeholder in both the wizard and the sidebar.
## The `machine` variant
```json
{
"type": "machine",
"name": "Phrozen Arco 0.4 nozzle",
"inherits": "fdm_machine_common",
"from": "system",
"setting_id": "lvaYKTUZr5C9jSwk",
"instantiation": "true",
"printer_model": "Phrozen Arco",
"printer_variant": "0.4",
"nozzle_diameter": ["0.4"],
"default_print_profile": "0.20mm Standard @Phrozen Arco 0.4 nozzle",
"default_filament_profile": ["Generic PLA @Phrozen Arco 0.4 nozzle"],
"printable_area": ["0x0", "300x0", "300x300", "0x300"],
"printable_height": "300"
}
```
Minimum viable key set: `type`, `name`, `from`, `instantiation`, `setting_id`, `inherits`,
`printer_model`, `printer_variant`, `nozzle_diameter`, `printable_area`, `printable_height`,
`default_print_profile`. The four keys without which the preset will not load at all are `name`,
`instantiation`, `printer_model` and `printer_variant`; `default_filament_profile` is an array
(`["Generic PLA @System"]`) and the model's `default_materials` a `;`-separated string. Unlike a
`machine_model`, a `machine` **is** config-loaded, so a key belonging to another preset type is a
reported error (a misspelled key is still silent).
### `printer_variant` — three hard rules
1. Non-empty, and an exact member of the model's `;`-separated `nozzle_diameter` list.
2. `printer_model` non-empty and naming a model of this vendor.
3. In validation mode, for instantiated presets only: split `printer_variant` on `+`, each token must
start with a number (a trailing non-numeric suffix such as `HF` is ignored), and the resulting **set**
must equal `set(nozzle_diameter)`.
Rules 1 and 2 are loader-enforced — failing either drops the preset *and* the whole bundle. Rule 3 only
raises a validation error: the preset still loads, but the validator exits non-zero.
`nozzle_diameter` lists one entry **per physical nozzle**; `printer_variant` lists the **distinct**
diameters joined with `+`. Snapmaker U1 is the worked case: `["0.4","0.4","0.6","0.6"]` against
`"0.4+0.6"` — it passes because the comparison is on sets.
The conventional values are `0.2`, `0.25`, `0.4`, `0.5`, `0.6`, `0.8` and `1.0`. Suffixed forms
(`0.4HF`, `0.6HF`, `0.8HF`, `0.4HS`) are Flashforge-only and the `+` form is rare. A variant is **not**
required to be unique within a model — Volumic ships `EXO42 IDRE`, `… COPY MODE` and `… MIRROR MODE` all
at `0.4` under the one model `EXO42 IDRE`.
The converse is **unchecked**: a nozzle size in the model's list with no matching variant is offered in
the wizard and resolves to nothing. `Wanhao France`'s `D12 500 PRO M2 DIRECT` ships that bug today.
### Other fields worth knowing
- `default_print_profile` is a **scalar**, matched by exact preset name. Not a `;` list. The named
process must be compatible with this printer through its resolved list or condition.
`validate_slice` attempts to select it and rejects generic Default fallbacks, but compatibility
updates can choose another compatible preset. Check the exact default reference yourself.
- `default_filament_profile` is an **array**, one name per element.
- `printable_area` is an array of `"XxY"` strings — four points for a rectangle, one per segment for a
delta or circular bed.
- `gcode_flavor` is usually set once in the base; `klipper`, `marlin`, `marlin2` and `reprapfirmware`
cover nearly every shipped printer.
- `printer_settings_id` is junk — most files carrying it disagree with their own name. Do not copy it
when cloning a bundle.
- `min_layer_height` / `max_layer_height` are **machine** keys (per extruder), never process keys.
## Bases
Nearly every machine-bearing vendor registers a base literally named `fdm_machine_common`, and Klipper
vendors add `fdm_klipper_common` on top of it. Two levels is the usual depth.
**There is no leading-underscore convention for bases.**
## Adding a printer to an existing bundle
1. Choose the names first — model, variant(s), process(es); everything else references them.
2. Add the model (`machine_model_list`) and one `machine` variant per nozzle; the minimum key sets are
above. Bed assets and `<Model>_cover.png` go directly in `<Vendor>/`.
3. Add at least one process per variant naming it in `compatible_printers`
([process-profiles.md](process-profiles.md#adding-a-quality-tier-or-a-nozzles-processes)).
4. Register everything (or run `update-index`), bump the version, run the id tool, validate.
## Adding a nozzle variant
1. Extend the model's `nozzle_diameter` (`"0.4"``"0.4;0.6"`).
2. Add the variant preset. Either inherit the shared base (the usual choice) or the 0.4 sibling (Elegoo,
BBL, Prusa and Qidi do this — smaller diff, but the sibling's edits now reach this file too).
3. Override what actually changes with nozzle: `nozzle_diameter`, `printer_variant`,
`default_print_profile`, `default_filament_profile`, `min_layer_height`/`max_layer_height`, and
retraction if the vendor tunes it.
4. Add at least one process for the new nozzle — see [process-profiles.md](process-profiles.md).
5. Register both, bump the version, run the id tool, validate.
## Multi-extruder, IDEX and tool-changers
Per-extruder vectors are **silently resized** to the nozzle count, with no error. Padding repeats the
**first** value, not the last — `["0.4","0.6"]` on a 4-nozzle machine becomes `0.4, 0.6, 0.4, 0.4`.
Longer vectors are truncated.
Note the two sizing families: the plain per-extruder keys (`extruder_offset`, `extruder_colour`,
`extruder_printable_height`, `min_layer_height`, `max_layer_height`, `nozzle_diameter`) are sized to the
extruder count, while `printer_options_with_variant_1` (`retraction_length`, `z_hop`, `wipe`,
`nozzle_type`, the rest of the retraction family) is sized to `printer_extruder_variant` instead.
- Give **one entry per extruder** for ordinary per-extruder vectors such as `extruder_offset`,
`extruder_colour`, `min_layer_height` and `max_layer_height`; size the variant-dependent family
to `printer_extruder_variant` instead.
A single `["0x0"]` `extruder_offset` on a dual or multi-tool machine — which already ships — pads every
toolhead to the same offset, so the offset never applies.
- Overriding `nozzle_diameter` to a different count without re-stating every per-extruder vector is the
other half of the trap — `Snapmaker U1 (0.4+0.6 nozzle)` inherits 5-entry vectors against 4 nozzles.
Copy targets: `Custom/machine/fdm_toolchanger_common.json` + `Custom/machine/MyToolChanger 0.4
nozzle.json` (a clean minimal variant on a base that gives every vector five entries), and
`Ratrig/machine/RatRig V-Core 4 IDEX 300 0.4 nozzle.json` for IDEX. The BBL extruder-variant machinery
(`extruder_variant_list`, `printer_extruder_id`, `default_nozzle_volume_type`) is used by a handful of
vendors — do not copy it into a new bundle (`nozzle_volume_type` itself is not a machine-preset key).
## Custom G-code
The keys are `machine_start_gcode`, `machine_end_gcode`, `change_filament_gcode`,
`machine_pause_gcode`, `before_layer_change_gcode` and `layer_change_gcode`. Both a single string with
embedded `\n` and a JSON array of lines are legal and both are in use — do not convert one into the
other. Conditionals are `{if …}` / `{elsif …}` / `{else}` / `{endif}`; `{elsif}` is rare but real (Qidi's
`layer_change_gcode` uses it).
Placeholder errors only surface when the config is actually expanded, which means `validate_slice`:
```bash
./scripts/check_profile.sh --vendor "<Vendor>" validate_slice
# Windows: scripts\check_profile.bat -Vendor "<Vendor>" validate_slice
```
What the sweep covers is in [validation.md](validation.md#validate_slice); no `CP TOOLCHANGE START` in
the output means `change_filament_gcode` never expanded.
@@ -1,145 +0,0 @@
# Process profiles
Processes live in `resources/profiles/<Vendor>/process/` — selectable leaves and shared bases alike, and
every one of them is registered in `process_list`. There are no global processes shared across vendors.
## Naming
`"<layer height>mm <quality> @<target>"` — near-universal, so match it.
Follow the bundle's existing quality vocabulary. BBL's common ladder relates the quality word to
the layer-height / nozzle ratio; it is a naming convention, not a loader constraint:
| Quality | Ratio | 0.2 nozzle | 0.4 | 0.6 | 0.8 |
| --- | --- | --- | --- | --- | --- |
| Extra Fine | 0.2× | — | 0.08 | — | — |
| Fine | 0.3× | 0.06 | 0.12 | 0.18 | 0.24 |
| Optimal | 0.4× | 0.08 | 0.16 | 0.24 | 0.32 |
| Standard | 0.5× | 0.10 | 0.20 | 0.30 | 0.40 |
| Draft | 0.6× | 0.12 | 0.24 | 0.36 | 0.48 |
| Extra Draft | 0.7× | 0.14 | 0.28 | 0.42 | 0.56 |
This is the `fdm_process_single_<lh>_nozzle_<n>` ladder; 0.4 is commonly the unsuffixed nozzle default.
Match neighbouring names rather than renaming shipped tiers to fit the table.
The `@target` is a human label, not a reference: most do not equal any real printer variant name.
Compatibility comes from the resolved list or condition, not this label.
## Shape
A selectable leaf's only truly universal keys are `type`, `setting_id`, `name` and `instantiation`;
`inherits` and `from` are near-universal — plus compatibility. No slicing key is universal; even
`layer_height` is more often inherited than restated. A base has `type`, `name`, `instantiation`, almost
always `from`, and **no** `setting_id`.
**Target shape: a 7-key leaf.** `OrcaArena` is the cleanest model —
`fdm_process_common``fdm_process_arena_common``fdm_process_arena_<lh>_nozzle_<n>` → leaf, where the
leaf carries only `type`, `name`, `inherits`, `from`, `setting_id`, `instantiation`,
`compatible_printers`, and the per-nozzle base holds the layer height and all eight line widths.
BBL, WonderMaker and Z-Bolt are uniform in *layering* — every leaf inherits a base, names its printers
directly and holds no layer height of its own — but not in key count. Imitate BBL's layering, not its
content: its leaves carry doubled `print_extruder_variant` arrays that no single-variant vendor needs.
Nearly every vendor ships its own `fdm_process_common` as the inherits-less root. Those files are not
identical; copying another vendor's version into a new bundle is normal.
Beware leaf-inherits-leaf: Prusa chains several levels deep through sibling leaves, and Elegoo and
Flashforge do it too, so editing one selectable process silently changes others. Check a leaf's children
before editing it.
## Compatibility
Most leaves set `compatible_printers` directly; some inherit it from a base, and Prusa's fall through to
`compatible_printers_condition`. After resolving `inherits`, **every selectable process has one or the
other** — that is the invariant to review against. Unlike filaments, inheriting `compatible_printers` is
legitimate for a process, and no check enforces its presence.
- A non-empty `compatible_printers` makes `compatible_printers_condition` **dead code**. Use one or
the other.
- A condition that fails to parse means *compatible with everything* — a warning, not an error. A typo
widens compatibility instead of narrowing it.
- Matching is `boost::regex` **`regex_match`** — a full-string match, which is why every shipped
condition wraps its keyword in `.*`. Because it is boost rather than `std`, `.` also spans the newlines
inside `printer_notes`.
- A `printer_notes` keyword that prefixes another model's keyword matches both. Prusa guards it:
```
printer_notes=~/.*PRINTER_MODEL_COREONE[^_a-zA-Z0-9].*/ and nozzle_diameter[0]==0.4 and printer_notes=~/.*HF_NOZZLE.*/
```
The `[^_a-zA-Z0-9]` exists because `PRINTER_MODEL_COREONE_L` also contains `PRINTER_MODEL_COREONE`.
`compatible_printers` is almost always one element. A leaf listing a whole model family is where a newly
added printer is usually forgotten.
## What to review per nozzle
| Key group | Review |
| --- | --- |
| `line_width` and per-region widths | resolved widths suit the nozzle and layer height |
| `layer_height`, `initial_layer_print_height` | within the printer's limits |
| print speeds | consistent with flow limits and hardware tuning |
| shell layers, wall loops, accelerations, support Z distances | preserve the intended thickness, motion and support behavior |
**A common starting pattern is nozzle + 0.02 mm**: 0.22 / 0.42 / 0.62 / 0.82 / 1.02. In that pattern, at 0.4,
`inner_wall_line_width`, `sparse_infill_line_width`, `skin_infill_line_width` and
`skeleton_infill_line_width` widen to 0.45 and `initial_layer_line_width` to 0.5; at 0.2,
`initial_layer_line_width` widens to 0.25. Also derived, and easily missed:
`ironing_inset = line_width / 2` (0.11 / 0.21 / 0.31 / 0.41).
These are examples, not required values; preserve intentional vendor tuning and percentage/automatic
widths, and validate their resolved values.
`min_layer_height` and `max_layer_height` are machine keys — no process file sets them.
## Slice-time content checks
`Print::validate()` enforces four rules at slice time:
1. `initial_layer_print_height` ≤ min `nozzle_diameter`
2. `layer_height` ≤ min `nozzle_diameter` — *"Layer height cannot exceed nozzle diameter."*
3. `line_width` and the seven per-region widths (inner/outer wall, sparse infill, internal solid infill,
top surface, skin, skeleton) > `layer_height` — *"Line width too small"*. `support_line_width` only
when the object has support or a raft; `initial_layer_line_width` is never checked.
4. every width ≤ 5 × max `nozzle_diameter` — *"Line width too large"*
Two further rules cover `bridge_line_width` (≤ nozzle diameter; > `layer_height` unless `thick_bridges`
and `thick_internal_bridges` are both on). The sweep starts from printer defaults rather than
enumerating every process. **A new non-default process gets no dedicated slice coverage in CI.**
## What CI checks on a process
Structure, not content: `process_list` name consistency **and** index coverage the other way, two files
claiming one process name, the `extruder_clearance_radius` / `extruder_clearance_max_radius` conflict
pair, duplicate JSON keys, a file `normalize` would rewrite, and the five `setting_id` rules (the fifth
rejects the misspelled key `settings_id`). `compatible_printers` presence is checked for **filaments
only**.
Note the C++ loader derives a missing `setting_id` on the fly, so the validator will not fail a process
without one — only `orca_profile_tool.py check` catches it. Running the validator alone gives a false
all-clear.
## Silent failures specific to processes
- **Unknown or misspelled keys are discarded with no error and no warning.** They ship all over the
process tree, both plain typos (`inital_layer_height`, `tree_support_bramch_diameter_angle`,
`sparse_infill_patter`) and keys copied from other slicers that Orca never defined.
- Keys on the tool's `OBSOLETE_KEYS` list (`adaptive_layer_height`, `overhang_totally_speed`, …) are
rejected by `check`'s normalization pass across preset types; `normalize` removes them.
The additional per-key obsolete warnings read `filament/` only.
- A dangling `compatible_printers` inside an `instantiation: "false"` base is invisible to
`check_preset_references`: a base never becomes a `Preset` at all (its config goes into `config_maps`
and the loader returns early), so it is in no collection for the check to walk.
- Orphan bases that nothing inherits are scattered through the tree — usually the leftover of a
half-finished nozzle addition.
## Adding a quality tier or a nozzle's processes
1. Choose the layer height and quality label using the vendor's existing ladder.
2. If the vendor has per-nozzle bases, add one (`fdm_process_<vendor>_<lh>_nozzle_<n>`) with the layer
height, nozzle-appropriate line widths, `initial_layer_print_height` and `ironing_inset`.
3. Add the leaf: 7 keys, `compatible_printers` naming the exact printer variant(s).
4. Register both in `process_list`, parent first. Bump the version, run the id tool, validate.
5. Slice this process explicitly with its intended printer; the sweep gives non-default tiers no
dedicated coverage. If it is a printer's `default_print_profile`, verify the exact name and
resolved compatibility too — the sweep may fall back or select another compatible process.
@@ -1,177 +0,0 @@
# Reviewing a profile change
Start with delivery, identity and backward compatibility, then check the affected preset types.
The table highlights gaps that need human review. What CI *does* run:
[validation.md](validation.md).
| Not checked by CI | Consequence |
| --- | --- |
| The `version` bump | The change never reaches an upgrading user |
| A misspelled setting key | Setting silently has no effect |
| A filename Windows cannot check out, or one that differs from its `sub_path` only in case | Works on the author's machine, breaks the bundle on another platform |
| `bed_model` / `bed_texture` / `hotend_model` pointing at a missing asset | Bed renders as Custom, hotend falls back to the generic model |
| A nozzle size in a model's list with no matching variant | The size is offered and resolves to nothing |
| A non-default process | `validate_slice` gives non-default quality tiers no dedicated coverage |
| Whether the intended default survived compatibility selection | The sweep can select a different compatible preset |
| A dangling `compatible_printers` inside an `instantiation: "false"` base | A base never becomes a `Preset`, so the reference check never sees it (a bad `inherits` in a base *is* caught) |
| A `renamed_from` whose old name is still a live preset | The redirect is inert while a live preset carries that name |
| Per-extruder vector length on a multi-nozzle printer | Silently padded (with the **first** value) or truncated |
## 1. Was the vendor `version` bumped?
For **every** bundle whose folder the diff touches, `resources/profiles/<Vendor>.json` must have its
`version` incremented — last component, carrying `.99` into the third component. A library change
means bumping `OrcaFilamentLibrary.json`.
*Why:* nothing in CI checks it, and `PresetUpdater` reinstalls only when `vendor_ver < resource_ver`
without a bump the change reaches neither an upgrading user nor the author's own running app.
## 2. Was the index rebuilt, and does the diff contain only this change?
`check` now fails on an unregistered file, on an index `update-index` would reorder, and on a file
`normalize` would rewrite — so a PR that skipped them arrives red, and you do not have to spot the
omission yourself. Three things are still yours:
- **The index diff belongs to this change.** `update-index` rewrites whole `*_list` sections. If the
bundle had drifted, the author's PR now carries someone else's reordering; ask for it in a separate
commit rather than reviewing it inline.
- **A deleted selectable preset needs a successor** as in item 4. `update-index` removes its
registration; `validate_custom` detects the break only for names covered by released fixtures.
- **`normalize` edits content, not just layout.** It drops `version` and `is_custom_defined` from preset
files, removes obsolete keys, deletes six print-speed keys from filament profiles, and resolves
`extruder_clearance_radius` against `extruder_clearance_max_radius` by keeping the larger.
Check that the keys it removed were meant to go.
Obsolete keys fail `check`'s normalization pass and should be removed with `normalize`.
`check` also reports per-key obsolete warnings for filament profiles in the selected vendors.
*Why:* the index is the loader's only entry point. Out-of-order entries fail with `can not find inherits`
and take the whole vendor bundle down; an unindexed file gets reviewed, merged and never loads.
## 3. Are ids generated, not written?
No hand-typed or copied `setting_id` / `filament_id`. Instantiated presets have a `setting_id`; bases do
not. `check` enforces all of that; what it cannot tell you is whether the identity *should* have moved.
A rewritten or removed `filament_id` means a product's identity moved — a rename, or an edited
`filament_vendor` / `filament_type` — and the old id is not forwarded anywhere. Confirm that was
intended, and that a new id is not a rename in disguise.
*Why:* a duplicate `filament_id` on one printer makes AMS spool matching a coin toss; a copied
`setting_id` breaks preset identity. See [ids.md](ids.md).
## 4. Does anything disappear for existing users?
A rename, a deletion, or a flip of `"instantiation": "true"``"false"` on a shipped preset removes the
name from the preset collection. It needs `renamed_from` on a successor — and only one preset may claim a
given old name. The claimed old name must **not** still be a live preset; the redirect is inert if it is.
*Why:* user presets inheriting it die with `can not find parent <name> for config <file>!`; 3MF-embedded
presets are dropped with no error at all. Commit `33923464ae` reverted exactly this for Cubicon;
`6943b6ddc3` redid it correctly. CI's `validate_custom` catches the shipped-name case — but not an inert
`renamed_from`.
## 5. Is `compatible_printers` right?
Exact printer **variant** names, non-empty on every instantiated filament outside OrcaFilamentLibrary
and written in the preset's own file — golden rule 6, with the flattened-vs-own-key trap in
[filament-profiles.md](filament-profiles.md#compatible_printers). Watch for a nozzle-specific variant that
inherited or copied the base's full printer list, and for two presets of one product with overlapping
lists — duplicate combobox entries and an ambiguous AMS match.
*Why:* real shipped bugs twice (`b7b3418baf` "showing up everywhere", `ff83aa41ef` duplicate Flashforge
entries).
## 6. Model ↔ variant ↔ process consistency
- New nozzle size → the model's `nozzle_diameter` list extended, a variant with a matching
`printer_variant`, and at least one process listing that variant.
- `default_print_profile` is one exact name (not a `;` list), and that process's resolved
compatibility list or condition includes this printer.
- `default_filament_profile` is an array of names that exist.
*Why:* an unlisted `printer_variant` is a hard bundle-load failure. Default process selection is
weaker: the sweep attempts the named default, then updates compatibility and rejects generic Default
fallbacks. Another compatible process can conceal a bad reference, so inspect it even after a pass.
## 7. Types and spellings
Every value a string or an array of strings; `filament_type` an array; `instantiation` the string
`"true"`/`"false"` — golden rule 7. Check index metadata and model `nozzle_diameter` especially;
wrong types there can abort loading for **every** vendor.
The part only a reviewer can do: check new setting keys against `src/libslic3r/PrintConfig.cpp`. A
misspelled key is silently discarded (rule 8), the single most common way a profile edit does nothing
while CI stays green.
## 8. Blast radius of a base edit
A change to `fdm_*_common.json` reaches every child at once. Ask which presets it touches — several
reverts in this repo are exactly this (`41d1b0d3c8`, `dc491166a8`). Also check whether the edited leaf has
children of its own: Prusa, Flashforge and Elegoo all chain leaf-inherits-leaf several levels deep.
## 9. Do the numbers make sense for the nozzle?
Check resolved widths and layer heights against the nozzle, and flow limits / pressure advance
against the actual hardware and material. The patterns in [process-profiles.md](process-profiles.md)
are examples, not mandatory values; [filament-profiles.md](filament-profiles.md) explains what to
revisit for a nozzle change. A cloned preset's unchanged MVS needs particular scrutiny.
Settings tuned for real hardware cannot be verified by reading the diff. Say so rather than approving
numbers nobody measured.
## 10. Asset references (not checked anywhere)
`bed_model`, `bed_texture`, `hotend_model` and `<Model>_cover.png` exist under
`resources/profiles/<vendor folder>/`. Broken references already ship; nothing checks them.
## 11. `default_materials` (checked by CI)
`check` fails on a `default_materials` / `default_filament_profile` name that resolves to no system
filament, so a dangling entry no longer reaches review. Scope the run while working on one vendor:
```bash
python3 scripts/orca_profile_tool.py check --vendor "<Vendor>" # py -3 on Windows
```
## 12. Per-extruder vector lengths (not checked)
One entry per extruder for the plain per-extruder vectors; the `printer_options_with_variant_1` keys are
sized to `printer_extruder_variant` instead. A wrong length is silently padded — repeating the **first**
value, not the last — or truncated. The two sizing families and the worked cases are in
[machine-profiles.md](machine-profiles.md#multi-extruder-idex-and-tool-changers).
## 13. Non-default processes get no slice coverage
`validate_slice` starts from printer defaults; it does not enumerate every process. Slice a new or
changed non-default tier explicitly with its intended printer.
## 14. Housekeeping worth a nit, not a block
`"from"` other than `"system"` (the preset-bundle loader ignores it, though the CLI's config-file loader
rejects anything but `system`/`user`/`User`), `printer_settings_id` copied from another
vendor, and a filename that disagrees with the preset's `name` (common; the loader keys off `name`).
## 15. Cross-platform filenames and paths (not checked)
Check for Windows-invalid characters, reserved device names, trailing path-component spaces/dots,
and case mismatches in `sub_path` or asset paths. See [cross-platform paths](validation.md#cross-platform-paths).
---
## Reporting the review
A finding is: **one defect**, its file, what breaks at runtime or in CI, and the fix. Split independent
defects into separate findings even when they live in one file — five id problems in one bullet get one
fix and four survivors.
Severity discriminates only if it is earned:
| Severity | Means |
| --- | --- |
| blocker | the bundle fails to load, or a preset is unreachable at runtime |
| major | CI fails, or existing users lose a preset |
| minor | wrong-but-working: dead keys, `from`, naming, redundant overrides |
Compute every number and id (`orca_profile_tool.py`, a scripted count) or omit it — one invented count
makes a reader stop trusting the right ones. Report a command's result only if you ran it.
@@ -1,255 +0,0 @@
# Validating profiles
```bash
./scripts/check_profile.sh # everything CI runs
./scripts/check_profile.sh --vendor "<Vendor>" # fast loop
./scripts/check_profile.sh profile_tool validate_slice # named checks only
```
```bat
scripts\check_profile.bat :: the same three, on Windows
scripts\check_profile.bat -Vendor "<Vendor>"
scripts\check_profile.bat profile_tool validate_slice
```
`check_profile.bat` is a shim around `check_profile.ps1` — same checks, same order, same logs;
the flags take PowerShell spellings (`-Vendor`, `-ProfilesDir`, `-Validator`, `-Download`, `-Refresh`,
`-WorkDir`, `-LogLevel`) and positional check names are unchanged. `-p`, `-v` and `-l` are aliases, so
`-v Elegoo -l 2` reads the same on both platforms. It passes `-ExecutionPolicy Bypass` because a
default Windows client refuses to run a checked-out `.ps1` at all. The `.ps1` finds Python itself,
probing `py -3`, then `python`, then `python3`; run the tool by hand with `py -3` for the same reason.
Every check in the run happens even after an earlier one fails; the script exits non-zero if any did, and writes
`.test/check_profiles/logs/<check>.log` plus, on failure, `.test/check_profiles/pr_comment.md` — the same
report CI posts on the PR. A stale `.test/check_profiles/.lock` after a crash must be removed by hand.
## The five checks
| Check | Command it runs | Catches |
| --- | --- | --- |
| `profile_tool` | `python3 scripts/orca_profile_tool.py check` | index coverage **both ways**, preset-name collisions, files `normalize`/`update-index` would still rewrite, duplicate JSON keys, filament `compatible_printers`, `filament_type` array, conflict keys, id length, **all `setting_id` and `filament_id` rules** |
| `validate_system` | `validator -p resources/profiles -l 2` | load errors, missing filament `compatible_printers`, dangling `inherits`/`compatible_*`, duplicate `filament_id` per printer |
| `validate_slice` | `validator -p … -s -l 2` | custom G-code expansion, unresolvable printer defaults |
| `validate_filament_subtypes` | `validator -p … -l 2 -f` | nothing extra — see below |
| `validate_custom` | `validator -p <tree+fixture> -l 2` | a shipped preset name that a past release offered no longer resolving |
**`-f` is a no-op.** It is declared `po::bool_switch()->default_value(true)`, so the duplicate-`filament_id`
check runs whether or not you pass it — `validate_system` already fails on duplicates. The binary's own
`--help` ("Off unless this flag is present") does not reflect that default.
### `validate_custom` — the backward-compatibility gate
Downloads one fixture archive per past release (v1.9.0 onwards) of *generated mock* user presets —
a `<vendor>_<preset>_orca_test` copy of every system preset that
release shipped, cut with the validator's own `-g 1` mode — unpacks each over a copy of the current tree
and loads it. Each entry holds only `inherits` plus a canned diff, so the one failure it adds over
`validate_system` is a shipped preset name disappearing. (The whole current tree sits under each fixture,
so every `validate_system` error fails it too.) This is what makes a rename or an
`instantiation` flip a CI failure rather than just a user complaint, and the reason `renamed_from` is
mandatory.
### `validate_slice`
Slices a two-colour cube on every instantiable printer in the tree, sequentially, forcing the prime tower.
It selects `default_print_profile` and the first `default_filament_profile`, then updates compatibility;
that update can select a different compatible preset. Confirm the intended defaults yourself rather
than treating a passing sweep as proof that those exact presets were sliced.
A printer fails if it cannot be selected, falls back to a Default preset, throws, produces no g-code, or
emits no `CP TOOLCHANGE START`. It cannot be scoped to a filament-only vendor
(`No instantiable printer presets found for vendor OrcaFilamentLibrary`); `check_profile.sh` records it
as SKIP for a vendor with no `machine/` folder.
## `orca_profile_tool.py check`
`check` is one subcommand of the tool that also owns
`generate-id`, `normalize`, `trim` and `update-index`; see [ids.md](ids.md) for the writing half.
| Per vendor | Catches |
| --- | --- |
| `check_preset_name_uniqueness` | two files in one bundle claiming one type + name — indexed or not |
| `check_index_coverage` | a file on disk that no `*_list` references (**an error, not a warning**) |
| `check_name_consistency` | an index entry whose `name` disagrees with the file, or whose `sub_path` is missing |
| `check_normalized` | a file `normalize` would rewrite, and an index `update-index` would rebuild |
| `check_filament_compatible_printers` | an instantiated non-library filament with no `compatible_printers` of its own |
| `check_conflict_keys` | `extruder_clearance_radius` alongside `extruder_clearance_max_radius` |
| `check_vector_type_keys` | a vector option written as a scalar (`"filament_type": "PLA"`) |
| `check_filament_id_length` | a declared `filament_id` longer than 8 characters |
| `check_machine_default_materials` | every `default_materials` / `default_filament_profile` name resolves |
| `check_obsolete_keys` | per-key warnings for ignored options; **filament files only** |
Tree-wide, **ignoring `--vendor` entirely**: `check_setting_id_uniqueness` and `check_filament_ids`. So a
vendor-scoped run can and does fail on another vendor's files — and it saves seconds, not minutes.
Unscoped, the per-vendor pass covers every bundle. The only exclusion is the stray `user/` directory
(see below); `OrcaFilamentLibrary` is held to the same rules as any vendor, its sole exemption being
that a library filament may leave `compatible_printers` empty — exactly what
`check_filament_compatible_printers` allows. `check_normalized` covers every bundle with an index.
Notes that matter:
- Exit codes: **0** clean, **1** errors found, **2** argparse misuse. Warnings never change the exit code.
- A nonexistent `--vendor` is a hard error — `[ERROR] unknown vendor "<V>" in <dir>`, exit 1.
- `--vendor ""` means all vendors; `check_profile.sh` relies on that. `--vendor` is repeatable.
- A **stray directory** under `resources/profiles/` still gets counted as a vendor by the per-vendor pass
and warned about (`No profiles found for vendor: <dir> at …/<dir>.json`, and the "Checked vendors" count
goes up by one). The one exception is `user/`, the validator's data dir, which an unscoped `check`
skips by name; `--vendor user` still checks and warns about it. Warnings never change the exit code.
`normalize`, `trim` and `update-index` ignore strays too — they define a bundle as *a directory with a
matching index file*.
- Each remedy is printed once for the whole run, not once per file, as a `[WARNING]` under the errors
("2 unreferenced file(s) above: delete them, or run … update-index"). Read those lines: they name the
command that fixes the batch.
- The trailing summary always suggests `normalize`. That is right for the shape errors and misleading for
everything else — an id error needs `generate-id`, a dangling `default_materials` needs a human.
- `resources/profiles/check_unused_setting_id.py` is a legacy BBL-only diagnostic, not part of
profile CI. Use `orca_profile_tool.py check` for current id validation.
### Obsolete-key diagnostics
`check` always reports per-key warnings for obsolete options in filament profiles.
The normalization check also rejects obsolete keys across preset types; `normalize` removes them.
### Default-material references
The materials check finds `default_materials` / `default_filament_profile` entries naming a preset
that does not exist. The three authoring errors it surfaces are `,` instead of `;`, wrong case
(`@system`), and a whole `;`-joined string stuffed into one array element.
### `normalize` and `update-index` are part of the check
`check` fails when either command would still change something, so they are not optional polish — the
file that gets reviewed has to be the file that ships. What `normalize` changes is narrow and fixed:
adds a missing `type`, deletes a `version` or `is_custom_defined` key from a *preset* file, deletes six
print-speed keys from filament profiles (`initial_layer_print_speed`, `outer_wall_speed`,
`inner_wall_speed`, `infill_speed`, `top_surface_speed`, `travel_speed`), deletes the
obsolete keys in `PrintConfigDef::handle_legacy`'s `ignore` set across preset types, resolves the
`extruder_clearance_*` conflict pair by keeping the larger, arrayifies five filament options besides
`filament_type`, and hoists `type`, `name`, `renamed_from`, `inherits`, `from`, `setting_id`,
`filament_id`, `instantiation` to the front. A file it changes is then rewritten whole — tab-indented,
LF, one trailing newline, keys reordered.
**Set `type` explicitly when authoring.** For a file in `machine/` without it, normalization guesses
`machine` only if its name contains `nozzle`, otherwise `machine_model`. That heuristic cannot
reliably classify shared machine bases or unusually named variants.
The Python obsolete-key set is checked against the C++ source by a unit test. Active options
and legacy aliases that the loader migrates (such as `extruder_type` and
`extruder_clearance_max_radius`) are preserved.
Two things it therefore does **not** enforce:
- **Formatting and key order on their own.** A file with none of those problems is skipped entirely, so
4-space indent, a missing trailing newline, and a file that leads with `compatible_printers` all pass
`check`. They stay latent until something else trips `normalize` and the whole file reformats inside an
unrelated diff. (`normalize --force` rewrites every file, which is not something to run on a shipped
bundle.)
- **A misspelled setting key.** `inital_layer_height` and `sparse_infill_densiti` pass `check` cleanly.
Verify new keys against `PrintConfig.cpp` and `PrintConfigDef::handle_legacy`.
## The validator binary
Built from `src/dev-utils/OrcaSlicer_profile_validator.cpp` (`-DORCA_TOOLS=ON`).
Both scripts find a local build under `build*/``check_profile.sh` tries Release, RelWithDebInfo, then
Debug, and `check_profile.ps1` adds MinSizeRel — else they download the nightly into
`.test/check_profiles/validator`. Pass `--download` / `-Download` to match CI exactly, since a stale
local build is used silently. Windows looks for `OrcaSlicer_profile_validator.exe`.
If your build lives somewhere else entirely, point at it with `--validator` / `-Validator`, or set
`ORCA_PROFILE_VALIDATOR` (`$env:ORCA_PROFILE_VALIDATOR` in PowerShell).
| Flag | Meaning |
| --- | --- |
| `-p <dir>` | profile tree (also becomes the data dir) |
| `-l <n>` | log level; CI uses 2 |
| `-v <Vendor>` | load only that vendor **plus** OrcaFilamentLibrary |
| `-s` | slice sweep |
| `-f` | no-op (see above) |
| `-g 1` | regenerate user-preset fixtures; takes a value, and wipes the user preset dir first |
On ARM64 Linux the nightly is x86-64 only — the script warns and downloads anyway, producing a binary
that will not run. Build it locally instead.
Running the validator directly uses the profile tree as its data directory and can create `user/`
there. Prefer the wrappers, which stash existing user presets and restore them afterward. After a
direct run, inspect `user/` and remove only empty directories created by that run; fixtures or
pre-existing user files may be present.
## Checking a copy of the tree
Use `--profiles DIR` on the Python tool and `-p DIR` on the validator. The wrappers' `--profiles` /
`-ProfilesDir` passes the tree to both, so one run validates a copy fully:
```bash
./scripts/check_profile.sh --profiles "<tree>"
```
On Windows use `scripts\check_profile.bat -ProfilesDir "<tree>"`.
## Testing in the app
Editing this checkout's `resources/profiles` does not update a separately installed application.
Test with a build using the edited resources and a bumped bundle version; the updater installs newer
bundles under `<data_dir>/system/`, and the preset cache also depends on the bundle version.
Use Help ▸ Show Configuration Folder to locate the active data directory:
| Platform | Default data directory |
| --- | --- |
| macOS | `~/Library/Application Support/OrcaSlicer` |
| Linux | `$XDG_CONFIG_HOME/OrcaSlicer`, or `~/.config/OrcaSlicer` when unset |
| Windows | `%APPDATA%\OrcaSlicer` |
A portable `data_dir` next to the executable takes precedence. Use a separate test configuration
for a clean-install check; preserve the normal configuration and user presets.
## Cross-platform paths
Match the exact case of each `sub_path` and asset filename; Linux filesystems commonly distinguish
case even when a macOS or Windows checkout does not. Preset-name references are case-sensitive
on every platform. Avoid Windows-invalid characters (`< > : " | ? *`), reserved device names
such as `CON` / `NUL` (including with extensions), and trailing spaces or dots in path components.
Keep stems tidy too, but a space immediately before `.json` is not a trailing path-component space.
## Error → remedy
| Message | Fix |
| --- | --- |
| `can not find inherits <parent> for <preset>` | parent missing, unregistered, or listed **after** the child |
| `can not find filament_id for <name>` | nothing in the chain declares one — run `generate-id` |
| `can not find parent <name> for config <user preset>!` | a shipped name disappeared — add `renamed_from` |
| `Missing instantiation attribute for <name>` | key absent **or** not the string `"true"`/`"false"` |
| `contains incorrect keys: <keys>, which were removed` | a key valid for a different preset type |
| `defines invalid printer variant "<v>"` | not in the model's `nozzle_diameter` list |
| `has printer_variant "<v>" that does not match its nozzle_diameter` | the set comparison in [machine-profiles.md](machine-profiles.md) |
| `references unknown compatible_printers "<p>"` | the printer was renamed or deleted; fix the reference |
| `references renamed compatible_printers "<old>" (now "<new>")` | in-tree references must name the current preset; `renamed_from` does not excuse them |
| `Filament preset "<f>" is missing compatible_printers setting` | non-library filaments need a non-empty list in their **own** file — the flattened-vs-own-key trap is in [filament-profiles.md](filament-profiles.md#compatible_printers) |
| `Ambiguous AMS filament match: N presets share filament_id "X" … printer "Y"` | make the lists disjoint, or fix an `inherits` pointing at another material's `@base` |
| `Layer height cannot exceed nozzle diameter.` / `Line width too small` | `Print::validate()` flow rules |
| `[ERROR] … no <V>.json list references it, so it never loads` | `update-index`, or delete the file |
| `[ERROR] … references it and it declares no profile type` | set the correct `type` explicitly, then `normalize` and `update-index` |
| `[ERROR] … normalize would <change>` / `<V>.json: update-index would rebuild <lists>` | run that command and commit the result |
| `[ERROR] <V> has N <type> profiles named "<name>"` | identify the intended preset and remove or rename the duplicate; use `trim --dry-run` only for deliberate unindexed-file cleanup |
| `[ERROR] … must not have a setting_id` / `is missing a setting_id` | `generate-id --setting-id` |
| `inherits filament_id "X" but its own triple … mints "Y"` | `generate-id` will **not** fix this — see [ids.md](ids.md) |
| `vendor <V>'s config version: <s> invalid` | the `version` string is not Semver-parseable |
| `[json.exception.type_error.302] type must be string` | locate the non-string value in the index or model; see [failure scopes](vendor-bundle.md#failure-modes-ranked-by-blast-radius) |
| `Printer "<p>" fell back to a default preset` | final process or filament selection is a generic Default preset; check named defaults, visibility and available compatible presets. An incompatible default may instead be replaced without this error |
| `Printer "<p>" sliced but the filament change never fired` | `change_filament_gcode` never expanded |
## CI
`.github/workflows/check_profiles.yml`, job **"Check profiles"**, on `pull_request` into `main` or
`release/*`, paths `resources/profiles/**`, `resources/printers/**`, `scripts/**` and the workflow itself.
There is no push trigger — a direct push to main runs no profile validation.
The job opens with `python3 -m unittest discover -s scripts/tests -t scripts`, the tool's own unit
tests. That step is deliberately **not** `continue-on-error`: a broken tool makes everything it then says
about the profiles worthless. Every check after it is `continue-on-error` with a final gate, so one run
reports all five results. On failure a second workflow posts or replaces a single PR comment marked
`<!-- profile-validation-comment -->`, with each failing log truncated to 30 KB; it deletes the comment
once the run is green.
The job name is also the required check for the delegated-merge bot, which lets a vendor maintainer
self-merge a `resources/profiles/<Their vendor>/` PR with no human review — so whatever CI does not check
is what ships unreviewed. Its denied patterns refuse `^scripts/` and any `.py`, so a PR that touches the
tooling always needs a maintainer.
@@ -1,175 +0,0 @@
# The vendor bundle and the loader
A bundle is `resources/profiles/<Vendor>.json` (the index) plus `resources/profiles/<Vendor>/`.
The **vendor id is the filename stem**, not the `name` inside — several differ (`BBL.json` is named
"Bambulab"). Asset paths and the `setting_id` formula use the id; the `validate_custom` fixture prefix
uses the `name`.
## The index
```json
{
"name": "Phrozen",
"version": "02.04.00.03",
"force_update": "0",
"description": "Phrozen configurations",
"machine_model_list": [ { "name": "...", "sub_path": "machine/....json" } ],
"machine_list": [ ... ],
"process_list": [ ... ],
"filament_list": [ ... ]
}
```
The loader reads `name`, `version`, `url` and the four `*_list` arrays.
`description` is only logged. `force_update` is read by `PresetUpdater`, never by the loader.
`sub_path` is relative to the **vendor folder**.
| List | Holds |
| --- | --- |
| `machine_model_list` | `machine_model` records (the printer product) |
| `machine_list` | printer variants **and** shared machine bases |
| `process_list` | selectable processes **and** shared process bases |
| `filament_list` | selectable filaments **and** shared filament bases |
### Three registration rules
1. **Everything is registered, bases included.** Every preset file on disk has exactly one entry in the
matching list, and no unindexed preset file is left in the tree.
2. **Parents before children.** `inherits` resolves against a per-kind map filled as the list is walked
(`configs.clear()` then process, filaments, printers). A parent listed after its child produces
`can not find inherits <parent> for <child>` and the bundle is discarded.
3. **The index entry's `name` must equal the `name` inside the sub_path file.** `check_name_consistency`
walks the index looking for the files; `check_index_coverage` walks the files looking for them in the
index. The `renamed_from` escape hatch `check_name_consistency`'s docstring promises is commented out.
All three are `check` errors now, and `update-index` writes an index that satisfies all three from the
files on disk — including the parents-first ordering, by topological sort. Hand-editing the index is
fine for a one-line addition, but the committed result must equal what `update-index` writes, because
`check` compares them.
The loader itself reports none of this: an unregistered file, or an entry with a typo'd key
(`"subpath"`), is silently dropped. (A typo'd `sub_path` is a `check` error naming the entry.)
`BBL/cli_config.json` and `BBL/filament/filaments_color_codes.json` are auxiliary data loaded by path,
not presets. The tool's `NON_PROFILE_FILES` excludes these basenames from preset maintenance.
## `version`
Parsed by a four-component Semver where the 4th is folded in as `patch = patch*100 + value`. Write it
zero-padded, `MM.mm.pp.bb`; a couple of bundles drop a component or the padding, but do not imitate them.
- **Bump the version for every bundle the PR touches.** `PresetUpdater` installs bundled resources
only when their version is newer than the installed version; the `.opc` preset cache is also
versioned. Nothing in profile CI checks the bump.
- **Keep the last component ≤ 99.** `02.04.00.100` and `02.04.01.00` both parse to `2.4.100`. A bundle
that reaches `.99` carries into the third component (`02.03.02.99``02.03.03.00`).
- An **absent** version is worse than a stale one: the validator still passes, but `Semver::valid()`
excludes `0.0.0`, so the vendor is dropped from the configuration wizard entirely and the preset cache
is disabled for it. An *unparseable* version is not silent — it throws and discards the whole bundle
(see the failure table below).
## Common preset keys
| Key | Value |
| --- | --- |
| `type` | `machine_model` / `machine` / `process` / `filament` |
| `name` | the preset name; the filename is *not* authoritative |
| `inherits` | the parent's exact `name` — no path, no `.json` |
| `instantiation` | the **string** `"true"` (selectable) or `"false"` (base) |
| `from` | `"system"` by convention; the vendor loader never reads it |
| `setting_id` | required on instantiated presets, forbidden on bases — generated |
| `renamed_from` | `;`-separated list of old names this preset supersedes |
These are config-preset keys; `machine_model` records have their own
[schema](machine-profiles.md#a-machine_model-is-not-a-config-preset). Keep `from` as `"system"`
for shipped presets. The vendor loader ignores it, but the CLI config-file loader accepts only
`system`, `user` or `User` and handles their inheritance differently.
`instantiation` is the one metadata key that is hard-gated: a missing key or any value other than the
strings `"true"`/`"false"` is an error (`Missing instantiation attribute for <name>`). A JSON boolean
`true` fails harder — it throws inside `load_from_json` and takes the **whole vendor bundle** down.
### `inherits`
Resolution is an exact-name lookup **within the same bundle**, plus one exception: filaments may inherit
from `OrcaFilamentLibrary`, which is loaded first and becomes the base bundle. Vendor-to-vendor
inheritance always fails. You can inherit from an instantiated preset as well as from a base; it is
common.
### `renamed_from`
One JSON string, `;`-separated for several old names.
- Write `"A;B"`, never `"A ; B"` — an unquoted item keeps its trailing space and can never match.
- When `renamed_from` is **absent** and the name contains `@`, the loader auto-adds the `@`-removed form
(`X @Y``X Y`) as a rename alias. Declaring an explicit `renamed_from` **suppresses** that, so a
preset that needs both the `@`-removed form and a real old name must list both. No shipped profile
currently does, which means any preset that gained a `renamed_from` quietly lost its `X Y` alias.
- It rescues names stored **outside** the tree: user presets and 3MF projects. It does **not** rescue
in-tree `inherits` (exact lookup), it does **not** satisfy `check_name_consistency`, the validator
reports an in-tree reference that only resolves through it (`references renamed compatible_printers
"OLD" (now "NEW")`), and `machine_model` records never read it at all.
- Only one preset may claim a given old name — two that do is a counted error
(`… was marked as renamed from "Y" … as well`). But the redirect is **inert while a live preset still
carries that name**, and nothing checks *that*; Z-Bolt ships a folder of such dead entries.
## Failure modes, ranked by blast radius
| Scope | Cause |
| --- | --- |
| **All vendors, zero system profiles** | a non-string `version`, `name` or `url` at the top level of a vendor index (`"version": 2`), or non-string `nozzle_diameter` on a model — `nlohmann::type_error` escapes the per-vendor `std::runtime_error` catch |
| **The whole vendor bundle** | unparseable `version`; index JSON parse error; a `sub_path` file missing or unparseable; unresolvable `inherits`; duplicate preset name within the vendor; empty/unknown `printer_model` or `printer_variant`; a filament resolving no `filament_id` |
| **One preset** | `instantiation` missing or a wrong string; keys belonging to another preset type (`contains incorrect keys: …, which were removed`); a non-string inside a `*_list` entry (`invalid value type for <key>`) |
| **Logged, not counted** | a raw JSON number in a preset — `invalid json type for <key>`, the value is dropped and the exit code stays 0 |
| **Nothing reported by the loader** | unregistered file; misspelled setting key; missing bed/hotend asset. Only the first of those is a `check` error; the other two reach users |
Deleting a file the index still lists surfaces as a *parse error* on line 1, not "file not found" — the
loader `ifstream`s the missing path and nlohmann reports `unexpected end of input`.
Preset names are a **single global namespace across every vendor**: a duplicate within one vendor is a
hard bundle failure, a duplicate across vendors is reported as `Found duplicated preset: <name> in
vendor: <vendor>` and still counts as an error. `check_preset_name_uniqueness` catches the within-bundle
case earlier and more precisely — including an *unindexed* twin, which is one `sub_path` edit away from
silently becoming the parent every child resolves to (`std::map::emplace` keeps the first insertion, so
index order decides). Base names, by contrast, repeat across bundles by design: `fdm_process_common`
exists in nearly all of them.
## Starting a whole new vendor bundle
Nothing generates one; copy the smallest bundle that resembles the hardware. **`Voxelab` or `M3D`** are
the minimal shape — a shared machine base, the model, one variant, a shared process base, two
processes, and an empty `filament_list` that takes the library generics. Do *not* start from `Phrozen`:
it carries local `fdm_filament_*` copies that have drifted from the library, and a filament preset that
restates most of its parent — the style this skill advises against.
Write the machine files **last**, so you only visit them once:
1. **Choose the names first** — model, variant(s), process(es). Everything else references them.
2. `resources/profiles/<Vendor>.json`: `name`, `version` (`01.00.00.00`), `force_update: "0"`,
`description`, and all four `*_list` arrays (an empty `filament_list` is fine).
3. The shared bases — `<Vendor>/machine/fdm_machine_common.json` and
`<Vendor>/process/fdm_process_common.json`, both `"instantiation": "false"` with no `setting_id`.
For a Klipper printer add your own `<Vendor>/machine/fdm_klipper_common.json` inheriting the machine
base; there is no shared one, because a `machine` preset can only inherit inside its own bundle.
4. One selectable process per variant, each naming its variant in `compatible_printers`.
5. Bed assets and `<Model>_cover.png`, all directly in `<Vendor>/`. None of them is needed for the
bundle to load, and nothing in CI checks them — but the bed files are inert unless the `machine_model`
names them in `bed_model` / `bed_texture`, and the cover is found by convention as
`<the name you gave the model in machine_model_list>_cover.png`.
6. The `machine_model` record and the `machine` variants, now that every value they reference exists —
the minimum key sets and the `default_*` shapes are in
[machine-profiles.md](machine-profiles.md#the-machine-variant).
7. Run the tool and validate — follow
[Creating or modifying a profile](../SKILL.md#creating-or-modifying-a-profile). `generate-id` is not
optional for a new bundle: the validator loads presets that have no `setting_id`, but `check` fails
every one of them. `update-index` will fill the four `*_list` arrays for you once the files exist, so
step 2 only needs the bundle metadata to be right.
## `resources/profiles_template/`
A separate tree (`Template.json` + `Template/`) holding filament and process templates. It is **not** a
scaffold for shipped profiles — `CreatePresetsDialog.cpp` reads it for the in-app "create a custom
printer/filament" wizard, so editing it changes what users get when they create a custom preset.
`check_profile.sh`'s validator checks default to `resources/profiles` (redirectable with `-p`), and so
does `orca_profile_tool.py` (redirectable with `--profiles`);
neither covers this tree.
-10
View File
@@ -1,10 +0,0 @@
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
deps/
build_*/
build/
localization/
sandboxes/
resources/*
!resources/web/
!resources/dailytip
!resources/shaders
-2
View File
@@ -1,2 +0,0 @@
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
-44
View File
@@ -1,44 +0,0 @@
{
"name": "OrcaSlicer",
"build": {
"dockerfile": "Dockerfile",
"args": {
"PLATFORM": "linux/amd64",
"BASE_IMAGE": "mcr.microsoft.com/devcontainers/cpp:ubuntu-24.04"
},
"options": ["--platform=linux/amd64"]
},
"runArgs": ["--platform=linux/amd64"],
"features": {
"ghcr.io/devcontainers/features/desktop-lite:1": {
"password": "orca"
}
},
"customizations": {
"vscode": {
"settings": {
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-vscode.cmake-tools",
"ms-vscode.cpptools-extension-pack"
]
}
},
"forwardPorts": [6080],
"portsAttributes": {
"6080": {
"label": "VNC web client (noVNC)",
"onAutoForward": "silent"
}
},
"otherPortsAttributes": {
"onAutoForward": "ignore"
},
"onCreateCommand": {
"Set postCreate executable flag": "chmod +x .devcontainer/postCreate.sh"
},
"postCreateCommand": "sudo .devcontainer/postCreate.sh"
}
-15
View File
@@ -1,15 +0,0 @@
#!/bin/bash
# Update and upgrade all system packages
apt update
apt upgrade -y
build_linux="./build_linux.sh -u"
echo "-----------------------------------------"
echo "Running ${build_linux}..."
echo "-----------------------------------------"
${build_linux}
echo "------------------------------"
echo "Installing missing packages..."
echo "------------------------------"
apt install -y libgl1-mesa-dev m4 autoconf libtool
-30
View File
@@ -1,30 +0,0 @@
Build
Build.bat
/build/
deps/build
MYMETA.json
MYMETA.yml
_build
blib
xs/buildtmp
*.o
*.log
MANIFEST.bak
xs/MANIFEST.bak
xs/assertlib*
.init_bundle.ini
.vs/*
local-lib
/src/TAGS
/.vscode/
build-linux/*
deps/build-linux/*
**/.DS_Store
install_*
build_*/
!build_linux.sh
SVG
scripts/Dockerfile
scripts/DockerBuild.sh
scripts/DockerRun.sh
scripts/.dockerignore
-2801
View File
File diff suppressed because it is too large Load Diff
-12
View File
@@ -1,12 +0,0 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# Shell scripts are run by Git Bash on Windows CI, which cannot read a script
# with CRLF line endings: it fails on the first line. Windows checkouts default
# to core.autocrlf=true, so keep these LF whatever the platform.
*.sh text eol=lf
# Batch files are read by cmd.exe, which tracks a byte offset into the file to
# resume after `call :label`. With LF endings that offset can land wrong and the
# label lookup fails, so keep these CRLF whatever the platform.
*.bat text eol=crlf
-3
View File
@@ -1,3 +0,0 @@
github: SoftFever
ko_fi: SoftFever
custom: https://paypal.me/softfever3d
-145
View File
@@ -1,145 +0,0 @@
name: 🐞 Bug Report
description: Something behaves incorrectly while Orca Slicer keeps running
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
**Thank you for using Orca Slicer and wanting to report a bug.**
Please note that this is not the place to make feature requests or ask for help.
For this, please use the [Feature request](https://github.com/OrcaSlicer/OrcaSlicer/issues/new?assignees=&labels=&projects=&template=feature_request.yml) issue type or you can discuss your idea on our [Discord server](https://discord.gg/P4VE9UY9gJ) with others.
If Orca Slicer closes on its own, freezes or stops responding, please use the [Crash report](https://github.com/OrcaSlicer/OrcaSlicer/issues/new?assignees=&labels=&projects=&template=crash_report.yml) form instead. It asks for the logs a crash needs.
Before filing, please check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
- type: checkboxes
attributes:
label: Is this issue reproducible in the latest nightly build?
description: >
Please verify this issue still happens in the latest nightly build first. It may already be fixed there:
[Nightly builds](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds).
options:
- label: I have checked the latest nightly build and the issue is still reproducible
required: true
- type: checkboxes
attributes:
label: Is there an existing issue for this problem?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: input
id: version
attributes:
label: OrcaSlicer Version
description: Which version of Orca Slicer are you running? You can see the full version in `Help` -> `About Orca Slicer`.
placeholder: e.g. 2.5.0
validations:
required: true
- type: input
id: working_version
attributes:
label: Regression compared to a previous version
description: Did it work in a previous version?
placeholder: e.g. 2.3.2
validations:
required: false
- type: dropdown
id: os_type
attributes:
label: "Operating System (OS)"
description: "What OSes are you experiencing issues on?"
multiple: true
options:
- Linux
- macOS
- Windows
validations:
required: true
- type: input
id: os_version
attributes:
label: "OS Version"
description: "What OS version does this relate to?"
placeholder: "i.e. OS: Windows 7/8/10/11 ..., Ubuntu 22.04/Fedora 36 ..., macOS 10.15/11.1/12.3 ..."
validations:
required: true
- type: textarea
id: system_info
attributes:
label: Additional system information
description: For the performance issue, please also show the CPU, Memory information; For the 3D Rendering issue, please also show the Display Card information.
placeholder: |
CPU: 11th gen Intel r core tm i7-1185g7/AMD Ryzen 7 6800h/...
Memory: 32/16 GB...
Display Card: NVIDIA Quadro P400/...
validations:
required: false
- type: input
id: printer
attributes:
label: Printer
description: Which printer was selected
placeholder: Voron 2.4/VzBot/Prusa MK4/Bambu Lab X1 series/Bambu Lab P1P/...
validations:
required: true
- type: textarea
id: reproduce_steps
attributes:
label: How to reproduce
description: Please describe the detailed steps to reproduce this issue
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
validations:
required: true
- type: textarea
id: actual_results
attributes:
label: Actual results
description: What happens after the above steps? Please, enclose a screenshot whenever possible (even when you think the description is clear).
validations:
required: true
- type: textarea
id: expected_results
attributes:
label: Expected results
description: What should happen after the above steps?
validations:
required: true
- type: textarea
id: file_uploads
attributes:
label: Project file & Debug log uploads
description: |
Attach the files with the **Paste, drop, or click to add files** control directly underneath this box. Zip anything that is not a `.log`, `.txt` or image, since GitHub rejects other file types, and keep each file under 25 MB.
* The **project file** used when the problem happened, zipped. Export it just before or after the problem occurs. Even if you did nothing and there is no object on the plate, export it, since we need the configuration it carries. `File` -> `Save project as...`
* The **log folder**, zipped. `Help` -> `Show Configuration Folder` opens it, or find it at:
* Windows: `%APPDATA%\OrcaSlicer\log`, usually `C:\Users\<you>\AppData\Roaming\OrcaSlicer\log`
* macOS: `$HOME/Library/Application Support/OrcaSlicer/log`
* Linux: `$HOME/.config/OrcaSlicer/log`
* Flatpak: `$HOME/.var/app/com.orcaslicer.OrcaSlicer/config/OrcaSlicer/log`
* If the zip comes out over 25 MB, attach the newest logs from that folder on their own instead.
placeholder: |
Zipped project file
Zipped log folder
validations:
required: true
- type: checkboxes
id: file_checklist
attributes:
label: Checklist of files to include
options:
- label: Log file
- label: Project file
- type: textarea
attributes:
label: Anything else?
description: |
Screenshots? References? Anything that will give us more context about the issue you are encountering!
validations:
required: false
-8
View File
@@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Community Support (Discord channel)
url: https://discord.gg/P4VE9UY9gJ
about: Please ask and answer support "how do I?"questions here.
- name: Discussion Forum
url: https://github.com/OrcaSlicer/OrcaSlicer/discussions
about: Please raise ideas and feature suggestions here.
-183
View File
@@ -1,183 +0,0 @@
name: 💥 Crash Report
description: Orca Slicer closes on its own, freezes or stops responding
labels: ["crash"]
body:
- type: markdown
attributes:
value: |
**Thank you for taking the time to report a crash.**
Use this form when Orca Slicer closes on its own, freezes, or stops responding.
If the application stays open and only produces a wrong result, please use the [Bug report](https://github.com/OrcaSlicer/OrcaSlicer/issues/new?assignees=&labels=&projects=&template=bug_report.yml) form instead.
A printer whose toolhead collides with the print is also a bug report rather than a crash, since the application itself did not stop.
Before filing, please check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
- type: checkboxes
attributes:
label: Is this crash reproducible in the latest nightly build?
description: >
Please verify this crash still happens in the latest nightly build first. It may already be fixed there:
[Nightly builds](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds).
options:
- label: I have checked the latest nightly build and the crash is still reproducible
required: true
- type: checkboxes
attributes:
label: Is there an existing issue for this crash?
description: Please search to see if an issue already exists for the crash you encountered.
options:
- label: I have searched the existing issues
required: true
- type: input
id: version
attributes:
label: OrcaSlicer Version
description: Which version of Orca Slicer are you running? You can see the full version in `Help` -> `About Orca Slicer`.
placeholder: e.g. 2.5.0
validations:
required: true
- type: input
id: working_version
attributes:
label: Regression compared to a previous version
description: Did it work in a previous version?
placeholder: e.g. 2.3.2
validations:
required: false
- type: dropdown
id: os_type
attributes:
label: "Operating System (OS)"
description: "What OSes are you seeing the crash on?"
multiple: true
options:
- Linux
- macOS
- Windows
validations:
required: true
- type: input
id: os_version
attributes:
label: "OS Version"
description: "What OS version does this relate to?"
placeholder: "i.e. OS: Windows 7/8/10/11 ..., Ubuntu 22.04/Fedora 36 ..., macOS 10.15/11.1/12.3 ..."
validations:
required: true
- type: input
id: printer
attributes:
label: Printer
description: Which printer was selected
placeholder: Voron 2.4/VzBot/Prusa MK4/Bambu Lab X1 series/Bambu Lab P1P/...
validations:
required: true
- type: dropdown
id: crash_moment
attributes:
label: When does the crash happen?
description: Pick the point where Orca Slicer stops working.
options:
- Not sure
- On startup, before the main window appears
- When opening or importing a project or model
- While changing printer, filament or process settings
- While slicing
- In the 3D view, Preview or Assembly view
- When exporting G-code or sending a print to the printer
- On the Device tab, or connecting to a printer (camera, sync, login)
- While using a specific tool, dialog or calibration
- After resuming from sleep or changing monitors
- When closing the application
- No clear pattern
validations:
required: true
- type: dropdown
id: crash_frequency
attributes:
label: How often does it happen?
options:
- Not sure
- Every time
- Often, but not every time
- Rarely
- It only happened once
validations:
required: true
- type: dropdown
id: fresh_config
attributes:
label: Does it still crash with a fresh configuration?
description: >
Close Orca Slicer and rename your configuration folder (`%APPDATA%\OrcaSlicer` on Windows,
`$HOME/Library/Application Support/OrcaSlicer` on macOS, `$HOME/.config/OrcaSlicer` on Linux),
then start it again. Renaming keeps your settings, so you can put the folder back afterwards.
options:
- I have not tried this
- Yes, it still crashes
- No, the crash goes away
validations:
required: true
- type: textarea
id: reproduce_steps
attributes:
label: How to reproduce
description: Please describe the detailed steps that lead to the crash.
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. Orca Slicer closes
validations:
required: true
- type: textarea
id: system_info
attributes:
label: Additional system information
description: >
Display card and driver version are worth adding for crashes on startup or in the 3D view.
CPU and memory are worth adding for crashes while slicing.
placeholder: |
CPU: 11th gen Intel r core tm i7-1185g7/AMD Ryzen 7 6800h/...
Memory: 32/16 GB...
Display Card: NVIDIA Quadro P400/...
validations:
required: false
- type: textarea
id: file_uploads
attributes:
label: Project file, logs and crash report uploads
description: |
A crash report without logs usually cannot be acted on. Attach the files with the **Paste, drop, or click to add files** control directly underneath this box. Zip anything that is not a `.log`, `.txt` or image, since GitHub rejects other file types, and keep each file under 25 MB.
* The **project file** used when the crash happened, zipped. Export it just before or after the crash, even if the plate is empty, since we need the configuration it carries. `File` -> `Save project as...`
* The whole **log folder**, zipped rather than single files picked out of it. `Help` -> `Show Configuration Folder` opens it, or find it at:
* Windows: `%APPDATA%\OrcaSlicer\log`, usually `C:\Users\<you>\AppData\Roaming\OrcaSlicer\log`
* macOS: `$HOME/Library/Application Support/OrcaSlicer/log`
* Linux: `$HOME/.config/OrcaSlicer/log`
* Flatpak: `$HOME/.var/app/com.orcaslicer.OrcaSlicer/config/OrcaSlicer/log`
* On Windows the crash itself is written to a separate `crash_*.log` in there, and that is the file we need most. If the zip comes out over 25 MB GitHub will refuse it, so attach the newest log and any `crash_*.log` on their own instead.
* The **operating system crash report**, on macOS and Linux, where Orca Slicer cannot write its own crash log. It is often the only record of where it died:
* macOS: Console.app -> Crash Reports, or `$HOME/Library/Logs/DiagnosticReports/`. The file starts with `OrcaSlicer` and ends in `.ips`. Zip it before attaching, GitHub does not accept `.ips` files.
* Linux: run `orca-slicer` from a terminal (Flatpak: `flatpak run com.orcaslicer.OrcaSlicer`) and paste everything it prints when it dies. On systemd systems `coredumpctl info orca-slicer` gives a backtrace.
placeholder: |
Zipped project file
Zipped log folder
Zipped macOS .ips crash report, or the terminal output on Linux
validations:
required: true
- type: checkboxes
id: file_checklist
attributes:
label: Checklist of files to include
options:
- label: Log folder
- label: Project file
- label: Operating system crash report (macOS and Linux)
- type: textarea
attributes:
label: Anything else?
description: |
Screenshots? References? Anything that will give us more context about the crash you are encountering!
validations:
required: false
@@ -1,66 +0,0 @@
name: 🚀 Feature Request / Enhancement
description: Suggest an improvement to make Orca Slicer even better!
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request!
If your idea is still at the formulation stage, or you're not sure it would
be useful to many users, you can raise it as a discussion topic under [Ideas](https://github.com/OrcaSlicer/OrcaSlicer/discussions/categories/ideas)
or you can raise it on the [Discord server](https://discord.gg/P4VE9UY9gJ).
- type: checkboxes
attributes:
label: Is there an existing issue for this feature request?
description: |
Please search to see if an issue already exists for a feature, or perhaps one similar.
You can then comment and react so that we know know much interest there is in the feature request.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Is your feature request related to a problem?
description: A clear and concise description of what the problem is.
placeholder: I'm always frustrated when [...]
validations:
required: true
- type: dropdown
attributes:
label: Which printers will be beneficial to this feature?
description: Select affected printer firmware type.
multiple: true
options:
- Klipper
- Marlin
- Others
- All
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
placeholder: It should do [...]
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
placeholder: |
1. [...]
2. [...]
3. [...]
validations:
required: false
- type: textarea
attributes:
label: Additional context
description: |
Add any other context, diagrams, illustations or screenshots about the feature request here.
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
@@ -1,11 +0,0 @@
name: 'Apt Install Deps'
description: 'Install the packages needed for building and deploying'
runs:
using: 'composite'
steps:
- name: Install dependencies from build_linux.sh
shell: bash
run: |
mkdir -p ${{ github.workspace }}/deps/build/destdir
sudo env "ORCA_UPDATER_SIG_KEY=$ORCA_UPDATER_SIG_KEY" ./build_linux.sh -ur
sudo chown $USER -R ./
-18
View File
@@ -1,18 +0,0 @@
version: 2
updates:
# Only care about major updates to github actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
ignore: # Only suggest major updates for built-in actions
- dependency-name: "github/*"
update-types: ["version-update:semver-patch", "version-update:semver-minor"]
- dependency-name: "actions/*"
update-types: ["version-update:semver-patch", "version-update:semver-minor"]
# Docker dependencies
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "monthly"
# no support for c/cpp
-27
View File
@@ -1,27 +0,0 @@
# Description
<!--
> Please provide a summary of the changes made in this PR. Include details such as:
> * What issue does this PR address or fix?
> * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be considered?
-->
# Screenshots/Recordings/Graphs
<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->
## Tests
<!--
> Please describe the tests that you have conducted to verify the changes made in this PR.
-->
<!--
> A guide for users on how to download the artifacts from this PR.
-->
[How to Download Pull Requests Artifacts for Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
-26
View File
@@ -1,26 +0,0 @@
name: Assign Issue
on:
schedule:
- cron: 0 0 * * *
issue_comment:
types: [created]
workflow_dispatch:
jobs:
assign:
permissions:
issues: write
runs-on: ubuntu-latest
steps:
- name: Assign the user or unassign stale assignments
# Note: v3.0.0 is broken (dist/index.mjs vs action.yml expects index.js)
# See: https://github.com/takanome-dev/assign-issue-action/issues/426
uses: takanome-dev/assign-issue-action@v2.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
maintainers: 'noisyfox,softfever'
days_until_unassign: 30
block_assignment: false
reminder_days: 7
max_assignments: 12
@@ -1,31 +0,0 @@
name: Auto-close duplicate issues
description: Auto-closes issues that are duplicates of existing issues
on:
schedule:
- cron: "0 9 * * *"
workflow_dispatch:
jobs:
auto-close-duplicates:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Auto-close duplicate issues
run: bun run scripts/auto-close-duplicates.ts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}
STATSIG_API_KEY: ${{ secrets.STATSIG_API_KEY }}
@@ -1,44 +0,0 @@
name: Backfill Duplicate Comments
description: Triggers duplicate detection for old issues that don't have duplicate comments
on:
workflow_dispatch:
inputs:
days_back:
description: 'How many days back to look for old issues'
required: false
default: '90'
type: string
dry_run:
description: 'Dry run mode (true to only log what would be done)'
required: false
default: 'true'
type: choice
options:
- 'true'
- 'false'
jobs:
backfill-duplicate-comments:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
issues: read
actions: write
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Backfill duplicate comments
run: bun run scripts/backfill-duplicate-comments.ts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DAYS_BACK: ${{ inputs.days_back }}
DRY_RUN: ${{ inputs.dry_run }}
-486
View File
@@ -1,486 +0,0 @@
name: Build all
on:
push:
branches:
- main
- release/*
- belt-printer
paths:
- 'deps/**'
- 'src/**'
- '**/CMakeLists.txt'
- 'version.inc'
- 'localization/**'
- 'resources/**'
- ".github/workflows/build_*.yml"
- ".github/workflows/unit_tests*.yml"
- 'build_win.bat'
- 'scripts/test_build_win.ps1'
- 'scripts/build_preset_cache.*'
- 'scripts/flatpak/**'
- 'scripts/msix/**'
- 'tests/**'
pull_request:
branches:
- main
- release/*
paths:
- 'deps/**'
- 'deps_src/**'
- 'src/**'
- '**/CMakeLists.txt'
- 'version.inc'
- ".github/workflows/build_*.yml"
- ".github/workflows/unit_tests*.yml"
- 'build_linux.sh'
- 'build_win.bat'
- 'scripts/test_build_win.ps1'
- 'build_release_macos.sh'
- 'scripts/build_preset_cache.*'
- 'scripts/flatpak/**'
- 'scripts/msix/**'
- 'tests/**'
schedule:
- cron: '0 17 * * *' # run once a day at 1 AM Singapore time (UTC+8)
workflow_dispatch: # allows for manual dispatch
inputs:
build-deps-only:
description: 'Only build dependencies (bypasses caching)'
type: boolean
default: false
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
# build_win.bat ships a test suite. Run it before the Windows builds.
check_build_script:
name: Windows build script tests
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v7
with:
lfs: 'false'
# Windows PowerShell rather than pwsh: the suite drives build_win.bat
# through cmd, and the two differ in how they quote native arguments.
- name: Run the build script test suite
shell: powershell
run: .\scripts\test_build_win.ps1
build_linux:
strategy:
fail-fast: false
# SELF_HOSTED skips arm64 (no arm self-hosted server). amd64's empty arch
# is load-bearing: it keeps the historical 'linux-clang' deps cache key and
# the unsuffixed asset names.
matrix:
include: ${{ fromJSON(vars.SELF_HOSTED
&& '[{"arch":"","os":"orca-lnx-server"}]'
|| '[{"arch":"","os":"ubuntu-24.04"},{"arch":"aarch64","os":"ubuntu-24.04-arm"}]') }}
# Don't run scheduled builds on forks:
if: ${{ !cancelled() && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }}
uses: ./.github/workflows/build_check_cache.yml
with:
os: ${{ matrix.os }}
arch: ${{ matrix.arch }}
build-deps-only: ${{ inputs.build-deps-only || false }}
secrets: inherit
build_windows:
name: Build Windows ${{ matrix.arch }}
strategy:
fail-fast: false
# SELF_HOSTED skips arm64 (the self-hosted Windows server is x64-only).
matrix:
include: ${{ fromJSON(vars.SELF_HOSTED
&& '[{"arch":"x64","os":"orca-win-server","compiler":"clang"}]'
|| '[{"arch":"x64","os":"windows-latest","compiler":"clang"},{"arch":"arm64","os":"windows-11-vs2026-arm","compiler":"clang"}]') }}
needs: check_build_script
# Don't run scheduled builds on forks:
if: ${{ !cancelled() && needs.check_build_script.result == 'success' && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }}
uses: ./.github/workflows/build_check_cache.yml
with:
os: ${{ matrix.os }}
arch: ${{ matrix.arch }}
compiler: ${{ matrix.compiler }}
build-deps-only: ${{ inputs.build-deps-only || false }}
force-build: ${{ github.event_name == 'schedule' }}
secrets: inherit
build_macos_arch:
strategy:
fail-fast: false
matrix:
arch:
- arm64
- x86_64
# Don't run scheduled builds on forks:
if: ${{ !cancelled() && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }}
uses: ./.github/workflows/build_check_cache.yml
with:
os: ${{ vars.SELF_HOSTED && 'orca-macos-arm64' || 'macos-14' }}
arch: ${{ matrix.arch }}
build-deps-only: ${{ inputs.build-deps-only || false }}
force-build: ${{ github.event_name == 'schedule' }}
secrets: inherit
build_macos_universal:
name: Build macOS Universal
needs: build_macos_arch
if: ${{ !cancelled() && needs.build_macos_arch.result == 'success' && !inputs.build-deps-only && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }}
uses: ./.github/workflows/build_orca.yml
with:
os: ${{ vars.SELF_HOSTED && 'orca-macos-arm64' || 'macos-14' }}
arch: universal
macos-combine-only: true
secrets: inherit
# One test job per built arch, on the runner that built it.
unit_tests_linux_x86_64:
name: Linux x86_64
needs: build_linux
if: ${{ !cancelled() && success() }}
uses: ./.github/workflows/unit_tests.yml
with:
os: ${{ vars.SELF_HOSTED && 'orca-lnx-server' || 'ubuntu-24.04' }}
artifact: ${{ github.sha }}-tests-linux-x86_64
unit_tests_linux_aarch64:
name: Linux aarch64
needs: build_linux
if: ${{ !cancelled() && success() && !vars.SELF_HOSTED }}
uses: ./.github/workflows/unit_tests.yml
with:
os: ubuntu-24.04-arm
artifact: ${{ github.sha }}-tests-linux-aarch64
unit_tests_windows_x64:
name: Windows x64
needs: build_windows
if: ${{ !cancelled() && success() }}
uses: ./.github/workflows/unit_tests.yml
with:
os: ${{ vars.SELF_HOSTED && 'orca-win-server' || 'windows-latest' }}
artifact: ${{ github.sha }}-tests-windows-x64
unit_tests_windows_arm64:
name: Windows arm64
needs: build_windows
if: ${{ !cancelled() && success() && !vars.SELF_HOSTED }}
uses: ./.github/workflows/unit_tests.yml
with:
os: windows-11-vs2026-arm
artifact: ${{ github.sha }}-tests-windows-arm64
test-dir: build-arm64/tests
unit_tests_macos_arm64:
name: macOS arm64
needs: build_macos_arch
if: ${{ !cancelled() && success() }}
uses: ./.github/workflows/unit_tests.yml
with:
os: ${{ vars.SELF_HOSTED && 'orca-macos-arm64' || 'macos-14' }}
artifact: ${{ github.sha }}-tests-macos-arm64
test-dir: build/arm64/tests
# Slice a two-colour cube through every shipped printer so all custom g-code
# (change_filament_gcode, machine start/end, etc.) is expanded - catches
# slicing regressions the static profile checks and unit tests can't see.
# Profile-only PRs are covered by check_profiles.yml's nightly binary; this
# covers src/engine PRs with the PR-built binary.
slice_check_linux:
name: Slice check (Linux ${{ vars.SELF_HOSTED && 'x86_64' || 'aarch64' }})
needs: build_linux
if: ${{ !cancelled() && success() }}
# Follows whichever Linux leg built the validator.
runs-on: ${{ vars.SELF_HOSTED && 'orca-lnx-server' || 'ubuntu-24.04-arm' }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Download profile validator
uses: actions/download-artifact@v8
with:
name: ${{ github.sha }}-profile-validator-linux-${{ vars.SELF_HOSTED && 'x86_64' || 'aarch64' }}
path: validator-bin
- name: Validate slice (expand custom g-code)
timeout-minutes: 60
run: |
chmod +x validator-bin/OrcaSlicer_profile_validator
./validator-bin/OrcaSlicer_profile_validator -p "${{ github.workspace }}/resources/profiles" -s -l 2
publish_test_results:
name: Publish Test Results
needs: [unit_tests_linux_x86_64, unit_tests_linux_aarch64, unit_tests_windows_x64, unit_tests_windows_arm64, unit_tests_macos_arm64, unit_tests_flatpak_x86_64, unit_tests_flatpak_aarch64]
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
steps:
- name: Download Test Results
uses: actions/download-artifact@v8
with:
pattern: test-results-*
path: test-results
# Best-effort: a read-only token (e.g. fork PRs) can't write the check, so
# don't let a publish failure fail the run. The test jobs gate correctness.
- name: Publish Test Results
continue-on-error: true
uses: EnricoMi/publish-unit-test-result-action@v2
with:
files: "test-results/**/*.xml"
- name: Delete Test Results
if: success()
uses: geekyeggo/delete-artifact@v6
with:
name: test-results-*
failOnError: false
flatpak:
name: "Flatpak"
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-50
options: --privileged
volumes:
- /usr/local/lib/android:/usr/local/lib/android
- /usr/share/dotnet:/usr/share/dotnet
- /opt/ghc:/opt/ghc1
- /usr/local/share/boost:/usr/local/share/boost1
- /opt/hostedtoolcache:/opt/hostedtoolcache1
strategy:
fail-fast: false
matrix:
variant:
- arch: x86_64
runner: ubuntu-24.04
- arch: aarch64
runner: ubuntu-24.04-arm
# Don't run scheduled builds on forks; skip entirely on self-hosted runners
if: ${{ !cancelled() && !vars.SELF_HOSTED && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }}
runs-on: ${{ matrix.variant.runner }}
env:
date:
ver:
ver_pure:
# Belt-printer nightlies share the main nightly release but carry a `_belt`
# suffix so they never overwrite the main assets.
nightly_suffix: ${{ github.ref == 'refs/heads/belt-printer' && '_belt' || '' }}
steps:
- name: "Remove unneeded stuff to free disk space"
run:
rm -rf /usr/local/lib/android/* /usr/share/dotnet/* /opt/ghc1/* "/usr/local/share/boost1/*" /opt/hostedtoolcache1/*
- uses: actions/checkout@v7
- name: Get the version and date
run: |
ver_pure=$(grep 'set(SoftFever_VERSION' version.inc | cut -d '"' -f2)
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
ver="PR-${{ github.event.number }}"
git_commit_hash="${{ github.event.pull_request.head.sha }}"
else
ver=V$ver_pure
git_commit_hash="${{ github.sha }}"
fi
echo "ver=$ver" >> $GITHUB_ENV
echo "ver_pure=$ver_pure" >> $GITHUB_ENV
echo "date=$(date +'%Y%m%d')" >> $GITHUB_ENV
echo "git_commit_hash=$git_commit_hash" >> $GITHUB_ENV
shell: bash
- name: Compute the flatpak-builder cache key
id: fp_cache_key
run: echo "key=flatpak-builder-${{ matrix.variant.arch }}-${{ hashFiles('deps/**', 'scripts/flatpak/com.orcaslicer.OrcaSlicer.yml', 'scripts/flatpak/make_deps_tar.sh') }}" >> "$GITHUB_OUTPUT"
shell: bash
# Manage flatpak-builder cache externally so PRs restore but never upload.
# The compiler cache under it is keyed per run below, so it is left out.
- name: Restore flatpak-builder cache
if: github.event_name == 'pull_request'
uses: actions/cache/restore@v6
with:
path: |
.flatpak-builder/*
!.flatpak-builder/ccache
key: ${{ steps.fp_cache_key.outputs.key }}
restore-keys: flatpak-builder-${{ matrix.variant.arch }}-
- name: Save/restore flatpak-builder cache
if: github.event_name != 'pull_request'
uses: actions/cache@v6
with:
path: |
.flatpak-builder/*
!.flatpak-builder/ccache
key: ${{ steps.fp_cache_key.outputs.key }}
restore-keys: flatpak-builder-${{ matrix.variant.arch }}-
# Compiler cache for the OrcaSlicer module, as in build_orca.yml. Pull
# requests only restore it; every other run (main, release branches, the
# nightly, a dispatch) saves it. orca_deps stays on the state cache above.
- name: Name the compiler cache leg
run: |
leg="Flatpak-${{ matrix.variant.arch }}"
echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV"
echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV"
shell: bash
- name: Restore compiler cache
id: ccache_restore
uses: actions/cache/restore@v6
with:
path: .flatpak-builder/ccache
key: ${{ env.CCACHE_ENTRY }}
restore-keys: ccache-${{ env.CCACHE_LEG }}-
- name: Disable debug info for faster CI builds
run: |
sed -i '/^build-options:/a\ no-debuginfo: true\n strip: true' \
scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
shell: bash
# flatpak-builder reuses a module from its cache when the definition and
# sources are unchanged, so a re-run of the same commit would skip the
# OrcaSlicer module and ship no test asset. A per-run value in that module's
# env keeps it rebuilding; orca_deps stays cached, and the compiler cache
# still serves the rebuild.
- name: Inject commit hash and flatpak-builder cache buster into Flatpak manifest
env:
flatpak_builder_cache_buster: ${{ github.run_id }}-${{ github.run_attempt }}
run: |
sed -i "/name: OrcaSlicer/{n;s|buildsystem: simple|buildsystem: simple\n build-options:\n env:\n flatpak_builder_cache_buster: \"$flatpak_builder_cache_buster\"\n git_commit_hash: \"$git_commit_hash\"|}" \
scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
shell: bash
# flatpak-builder's --ccache only wraps cc and gcc, and the manifest builds
# with clang, so CMake's launcher runs ccache instead; --ccache is still what
# mounts the cache directory into the sandbox. The settings go into that
# directory's own config file, which the sandbox reads too.
- name: Enable compiler cache
run: |
printf ' %s\n' \
'CMAKE_C_COMPILER_LAUNCHER: ccache' \
'CMAKE_CXX_COMPILER_LAUNCHER: ccache' > "$RUNNER_TEMP/ccache-env.yml"
sed -i "/^ git_commit_hash: /r $RUNNER_TEMP/ccache-env.yml" \
scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
grep -q '^ CMAKE_CXX_COMPILER_LAUNCHER: ccache$' scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
mkdir -p .flatpak-builder/ccache
export CCACHE_DIR=$PWD/.flatpak-builder/ccache
ccache --set-config=max_size=3G
# The compiler is reinstalled every run, so its mtime means nothing.
ccache --set-config=compiler_check=content
# Headers a fresh checkout has just written, the few files that use
# __DATE__ or __TIME__, and the precompiled header, whose macros ccache
# cannot see.
ccache --set-config=sloppiness=pch_defines,time_macros,include_file_mtime,include_file_ctime
# Hash the includes the compiler reports instead of preprocessing every
# miss before compiling it.
ccache --set-config=depend_mode=true
# The restored directory carries the previous run's counters.
ccache -z
shell: bash
- name: Check the manifest keeps orca_deps cacheable
run: ./scripts/flatpak/check_manifest_cacheable.sh
shell: bash
- name: Pack deps/ for the Flatpak manifest
run: ./scripts/flatpak/make_deps_tar.sh
shell: bash
- uses: flatpak/flatpak-github-actions/flatpak-builder@master
with:
bundle: OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
manifest-path: scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
# cache only turns on flatpak-builder --ccache; the caching itself is above.
cache: true
restore-cache: false
save-cache: false
arch: ${{ matrix.variant.arch }}
upload-artifact: false
# run-tests fires the module's build-only test-commands; keep-build-dirs
# retains the binaries for the packaging step below.
run-tests: true
keep-build-dirs: true
# The build has just touched everything it can use, so an object untouched
# for a week is dead, usually orphaned by a flag change.
- name: Compiler cache statistics
if: always()
run: |
export CCACHE_DIR=$PWD/.flatpak-builder/ccache
ccache --evict-older-than 7d
ccache -s -v || ccache -s
shell: bash
# Save the new entry first, then drop the older ones for this leg on this
# ref, so a failed save leaves the previous entry in place. A cancelled or
# failed build saves too, since what it compiled is still valid; a restore
# that did not finish does not, since the directory may be a truncated copy.
- name: Save compiler cache
id: ccache_save
if: ${{ always() && steps.ccache_restore.outcome == 'success' && github.event_name != 'pull_request' }}
uses: actions/cache/save@v6
with:
path: .flatpak-builder/ccache
key: ${{ env.CCACHE_ENTRY }}
- name: Drop older compiler cache entries
if: ${{ always() && steps.ccache_save.outcome == 'success' }}
# The container has no gh, so this is the list and delete over the REST API.
# Older means a lower run id, so two runs finishing close together keep
# the newer entry whichever of them cleans up last.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
api="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/caches"
curl -sSf -H "Authorization: Bearer $GH_TOKEN" \
"$api?ref=$GITHUB_REF&key=ccache-$CCACHE_LEG-&per_page=100" \
| jq -r --arg prefix "ccache-$CCACHE_LEG-" --argjson run "$GITHUB_RUN_ID" \
'.actions_caches[] | select((.key | ltrimstr($prefix) | split("-")[0] | tonumber?) < $run) | .id' \
| while read -r id; do
curl -sSf -X DELETE -H "Authorization: Bearer $GH_TOKEN" "$api/$id"
done
shell: bash
- name: Upload artifacts Flatpak
uses: actions/upload-artifact@v7
with:
name: OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
path: '/__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak'
- name: Deploy Flatpak to nightly release
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer')
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: /__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
asset_name: OrcaSlicer-Linux-flatpak_nightly${{ env.nightly_suffix }}_${{ matrix.variant.arch }}.flatpak
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
# The asset is /app (the exes link it at runtime) plus the build tree
# slimmed to what ctest needs.
- name: Package flatpak test asset
shell: bash
run: |
d=$(ls -d .flatpak-builder/build/OrcaSlicer-* | tail -1)
find "$d/build_flatpak" -mindepth 1 -maxdepth 1 ! -name tests -exec rm -rf {} +
# Strip debug info (the SDK builds with -g, only the app gets stripped);
# the bounds checks are compiled in, so a stripped exe still catches them.
find "$d/build_flatpak/tests" -type f -perm -u+x -exec strip --strip-unneeded {} + 2>/dev/null || true
# At runtime the tests read tests/ (TEST_DATA_DIR), scripts/, and under
# resources/ the shipped profiles (PROFILES_DIR), the printers/ maps, and
# the icon SVGs (NativeCommands icon names are checked against them).
find "$d" -mindepth 1 -maxdepth 1 -type d \
! -name tests ! -name build_flatpak ! -name scripts ! -name resources -exec rm -rf {} +
find "$d/resources" -mindepth 1 -maxdepth 1 ! -name profiles ! -name printers ! -name images -exec rm -rf {} +
# Only the SVGs are read; the png/ico/icns/gif assets are ~35MB of dead weight.
find "$d/resources/images" -mindepth 1 -maxdepth 1 ! -name '*.svg' -exec rm -rf {} + 2>/dev/null || true
tar -cf flatpak-test-asset.tar flatpak_app "$d"
- name: Upload flatpak test asset
uses: actions/upload-artifact@v7
with:
name: ${{ github.sha }}-flatpak-tests-${{ matrix.variant.arch }}
path: flatpak-test-asset.tar
retention-days: 1
# keep-build-dirs would otherwise land in the flatpak-builder cache saved post-job.
- name: Drop the kept build dirs before the flatpak-builder cache saves
if: always()
shell: bash
run: rm -rf .flatpak-builder/build
unit_tests_flatpak_x86_64:
name: Flatpak x86_64
needs: flatpak
if: ${{ !cancelled() && success() }}
uses: ./.github/workflows/unit_tests_flatpak.yml
with:
os: ubuntu-24.04
artifact: ${{ github.sha }}-flatpak-tests-x86_64
unit_tests_flatpak_aarch64:
name: Flatpak aarch64
needs: flatpak
if: ${{ !cancelled() && success() }}
uses: ./.github/workflows/unit_tests_flatpak.yml
with:
os: ubuntu-24.04-arm
artifact: ${{ github.sha }}-flatpak-tests-aarch64
-73
View File
@@ -1,73 +0,0 @@
name: Check Cache
on:
workflow_call:
inputs:
os:
required: true
type: string
arch:
required: false
type: string
compiler:
required: false
type: string
default: msvc
build-deps-only:
required: false
type: boolean
force-build:
required: false
type: boolean
jobs:
check_cache: # determines if there is a cache and outputs variables used in caching process
name: Check Cache
runs-on: ${{ inputs.os }}
outputs:
cache-key: ${{ steps.set_outputs.outputs.cache-key }}
cache-path: ${{ steps.set_outputs.outputs.cache-path }}
valid-cache: ${{ steps.cache_deps.outputs.cache-hit }}
steps:
- name: Checkout
uses: actions/checkout@v7
with:
lfs: 'false'
- name: set outputs
id: set_outputs
env:
# Anything that changes how the tree is built belongs in the key, or a job
# restores one it cannot use. Linux amd64 passes no arch deliberately, so
# 'linux-clang' keeps the cache it already has.
cache-os: ${{ runner.os == 'macOS' && format('macos-{0}', inputs.arch) || (runner.os == 'Windows' && format('windows-{0}-{1}', inputs.arch, inputs.compiler) || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }}
# The Windows ARM64 deps build in build-arm64, all others under build;
# build_deps.yml and build_orca.yml pass the Windows directory to build_win.bat.
dep-folder-name: ${{ runner.os == 'macOS' && format('/{0}', inputs.arch) || (runner.os == 'Windows' && inputs.arch == 'arm64') && '-arm64/OrcaSlicer_dep' || '/OrcaSlicer_dep' }}
output-cmd: ${{ runner.os == 'Windows' && '$env:GITHUB_OUTPUT' || '"$GITHUB_OUTPUT"'}}
run: |
echo cache-key=${{ env.cache-os }}-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }} >> ${{ env.output-cmd }}
echo cache-path=${{ github.workspace }}/deps/build${{ env.dep-folder-name }} >> ${{ env.output-cmd }}
- name: load cache
id: cache_deps
uses: actions/cache@v6
with:
path: ${{ steps.set_outputs.outputs.cache-path }}
key: ${{ steps.set_outputs.outputs.cache-key }}
lookup-only: true
build_deps: # call next step
name: Build Deps
needs: [check_cache]
uses: ./.github/workflows/build_deps.yml
with:
cache-key: ${{ needs.check_cache.outputs.cache-key }}
cache-path: ${{ needs.check_cache.outputs.cache-path }}
valid-cache: ${{ needs.check_cache.outputs.valid-cache == 'true' }}
os: ${{ inputs.os }}
arch: ${{ inputs.arch }}
compiler: ${{ inputs.compiler }}
build-deps-only: ${{ inputs.build-deps-only }}
force-build: ${{ inputs.force-build }}
secrets: inherit
-209
View File
@@ -1,209 +0,0 @@
on:
workflow_call:
inputs:
cache-key:
required: true
type: string
cache-path:
required: true
type: string
valid-cache:
required: true
type: boolean
os:
required: true
type: string
arch:
required: false
type: string
compiler:
required: false
type: string
default: msvc
build-deps-only:
required: false
type: boolean
force-build:
required: false
type: boolean
jobs:
build_deps:
name: Build Deps
if: ${{ !cancelled() && (inputs.build-deps-only || inputs.force-build || inputs.valid-cache != true) }}
runs-on: ${{ inputs.os }}
env:
date:
steps:
# Setup the environment
- name: Checkout
uses: actions/checkout@v7
with:
lfs: 'false'
- name: load cached deps
uses: actions/cache@v6
with:
path: ${{ inputs.cache-path }}
key: ${{ inputs.cache-key }}
- uses: lukka/get-cmake@latest
# The windows-11-arm runner needs CMake <= 3.31 (handled in the next step).
if: ${{ !(runner.os == 'Windows' && inputs.arch == 'arm64') }}
with:
cmakeVersion: "~4.3.0" # use most recent 4.3.x version
useLocalCache: true # <--= Use the local cache (default is 'false').
useCloudCache: true
- name: Install CMake 3.31.x (Windows ARM64)
# windows-11-arm ships CMake 4.x, which removed pre-3.5 policy
# compatibility AND has incomplete ASM_ARMASM linker modules
# (breaks Boost.Context on ARM64). Pin to the last 3.x release.
if: runner.os == 'Windows' && inputs.arch == 'arm64'
shell: pwsh
run: |
$ver = "3.31.6"
$url = "https://github.com/Kitware/CMake/releases/download/v$ver/cmake-$ver-windows-arm64.zip"
Invoke-WebRequest -Uri $url -OutFile "$env:RUNNER_TEMP\cmake.zip"
Expand-Archive -Path "$env:RUNNER_TEMP\cmake.zip" -DestinationPath "$env:RUNNER_TEMP\cmake" -Force
$cmakeBin = "$env:RUNNER_TEMP\cmake\cmake-$ver-windows-arm64\bin"
if (-not (Test-Path "$cmakeBin\cmake.exe")) { throw "cmake.exe not found at $cmakeBin" }
Add-Content -Path $env:GITHUB_PATH -Value $cmakeBin
- name: setup dev on Windows
if: runner.os == 'Windows'
uses: microsoft/setup-msbuild@v3
- name: Get the date on Ubuntu and macOS
if: runner.os != 'Windows'
run: echo "date=$(date +'%Y%m%d')" >> $GITHUB_ENV
shell: bash
- name: Get the date on Windows
if: runner.os == 'Windows'
run: echo "date=$(Get-Date -Format 'yyyyMMdd')" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
shell: pwsh
- name: Install MSYS2 (clangarm64) with GMP/MPFR and LLVM tools
if: runner.os == 'Windows' && inputs.arch == 'arm64'
uses: msys2/setup-msys2@v2
with:
msystem: CLANGARM64
update: true
install: >-
mingw-w64-clang-aarch64-gmp
mingw-w64-clang-aarch64-mpfr
mingw-w64-clang-aarch64-llvm
- name: Stage ARM64 GMP/MPFR (no prebuilt blobs exist for win-arm64)
# GMP/MPFR ship prebuilt x64/x86 blobs in-tree but none for ARM64.
# Pull them from MSYS2 clangarm64 and generate MSVC import libs via
# llvm-dlltool, then stage into deps/{GMP,MPFR}/.../win-arm64 where the
# MSVC branch of GMP.cmake/MPFR.cmake copies them into the dep prefix.
if: runner.os == 'Windows' && inputs.arch == 'arm64'
shell: msys2 {0}
run: |
set -euo pipefail
BIN=/clangarm64/bin
REPO=$(cygpath -u "$GITHUB_WORKSPACE")
make_import_lib() {
local dll="$1"; local lib="$2"; local def="/tmp/${dll%.dll}.def"
echo "EXPORTS" > "$def"
llvm-readobj --coff-exports "$BIN/$dll" | awk '/Name: /{print $2}' >> "$def"
llvm-dlltool -m arm64 -D "$dll" -d "$def" -l "$BIN/$lib"
}
make_import_lib libgmp-10.dll libgmp-10.lib
# MPFR 4.x ships as libmpfr-6.dll; rename to libmpfr-4 BEFORE generating
# the import lib so the baked-in runtime DLL name is correct.
MPFR_DLL=$(ls $BIN/libmpfr-*.dll | head -1 | xargs basename)
if [ "$MPFR_DLL" != "libmpfr-4.dll" ]; then cp "$BIN/$MPFR_DLL" "$BIN/libmpfr-4.dll"; fi
make_import_lib libmpfr-4.dll libmpfr-4.lib
mkdir -p $REPO/deps/GMP/gmp/lib/win-arm64 $REPO/deps/MPFR/mpfr/lib/win-arm64
cp $BIN/libgmp-10.dll $BIN/libgmp-10.lib $REPO/deps/GMP/gmp/lib/win-arm64/
cp $BIN/libmpfr-4.dll $BIN/libmpfr-4.lib $REPO/deps/MPFR/mpfr/lib/win-arm64/
cp /clangarm64/include/gmp.h $REPO/deps/GMP/gmp/include/
cp /clangarm64/include/mpfr.h $REPO/deps/MPFR/mpfr/include/ || true
# Build Dependencies
- name: Build on Windows
if: runner.os == 'Windows'
working-directory: ${{ github.workspace }}
run: |
if (-not "${{ vars.SELF_HOSTED }}") {
choco install strawberryperl
}
# cache-path is the install directory inside the deps build directory.
$deps = (Split-Path "${{ inputs.cache-path }}").Replace('\', '/')
# -l compiles with Visual Studio's clang-cl and -x builds with Ninja; --msvc --msbuild is cl under the Visual Studio generator.
$flags = if ("${{ inputs.compiler }}" -eq "clang") { "-l", "-x" } else { "--msvc", "--msbuild" }
.\build_win.bat -d --arch ${{ inputs.arch }} --deps-dir $deps @flags
shell: pwsh
- name: Build on Mac ${{ inputs.arch }}
if: runner.os == 'macOS'
working-directory: ${{ github.workspace }}
run: |
if [ -z "${{ vars.SELF_HOSTED }}" ]; then
brew install automake texinfo libtool pkgconf yasm nasm
fi
./build_release_macos.sh -dx ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a ${{ inputs.arch }} -t 10.15
(cd "${{ github.workspace }}/deps/build/${{ inputs.arch }}" && \
find . -mindepth 1 -maxdepth 1 ! -name 'OrcaSlicer_dep' -exec rm -rf {} +)
- name: Apt-Install Dependencies
if: runner.os == 'Linux' && !vars.SELF_HOSTED
uses: ./.github/actions/apt-install-deps
- name: Build on Ubuntu
if: runner.os == 'Linux'
working-directory: ${{ github.workspace }}
run: |
mkdir -p ${{ github.workspace }}/deps/build/destdir
./build_linux.sh -drlL
cd deps/build
tar -czvf OrcaSlicer_dep_ubuntu_$(date +"%Y%m%d").tar.gz destdir
# Upload Artifacts
# - name: Upload Mac ${{ inputs.arch }} artifacts
# if: runner.os == 'macOS'
# uses: actions/upload-artifact@v6
# with:
# name: OrcaSlicer_dep_mac_${{ env.date }}
# path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep*.tar.gz
# - name: Upload Windows artifacts
# if: runner.os == 'Windows'
# uses: actions/upload-artifact@v6
# with:
# name: OrcaSlicer_dep_win64_${{ env.date }}
# path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep*.zip
# - name: Upload Ubuntu artifacts
# if: runner.os == 'Linux' && !env.ACT
# env:
# ubuntu-ver: '2404'
# uses: actions/upload-artifact@v6
# with:
# name: OrcaSlicer_dep_ubuntu_${{ env.ubuntu-ver }}_${{ env.date }}
# path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep_ubuntu_*.tar.gz
build_orca:
name: Build OrcaSlicer
needs: [build_deps]
if: ${{ !cancelled() && !inputs.build-deps-only && (inputs.force-build || (inputs.valid-cache == true && needs.build_deps.result == 'skipped') || (inputs.valid-cache != true && success())) }}
uses: ./.github/workflows/build_orca.yml
with:
cache-key: ${{ inputs.cache-key }}
cache-path: ${{ inputs.cache-path }}
os: ${{ inputs.os }}
arch: ${{ inputs.arch }}
compiler: ${{ inputs.compiler }}
secrets: inherit
-788
View File
@@ -1,788 +0,0 @@
on:
workflow_call:
inputs:
cache-key:
required: false
type: string
cache-path:
required: false
type: string
os:
required: true
type: string
arch:
required: false
type: string
compiler:
required: false
type: string
default: msvc
macos-combine-only:
required: false
type: boolean
default: false
jobs:
build_orca:
name: Build OrcaSlicer
runs-on: ${{ inputs.os }}
env:
date:
ver:
ver_pure:
ubuntu-ver: '2404'
ubuntu-ver-str: '_Ubuntu2404'
ORCA_UPDATER_SIG_KEY: ${{ secrets.ORCA_UPDATER_SIG_KEY }}
# Branches whose builds are published to the nightly release. The
# belt-printer branch ships alongside main but its assets carry a `_belt`
# suffix (nightly_suffix) so they never overwrite the main nightly assets.
deploy_nightly: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer' }}
nightly_suffix: ${{ github.ref == 'refs/heads/belt-printer' && '_belt' || '' }}
steps:
- name: Checkout
uses: actions/checkout@v7
with:
lfs: 'false'
- name: load cached deps
if: ${{ !(runner.os == 'macOS' && inputs.macos-combine-only) }}
uses: actions/cache@v6
with:
path: ${{ inputs.cache-path }}
key: ${{ inputs.cache-key }}
fail-on-cache-miss: true
- uses: lukka/get-cmake@latest
# The windows-11-arm runner needs CMake <= 3.31 (handled in the next step).
if: ${{ !(runner.os == 'Windows' && inputs.arch == 'arm64') }}
with:
cmakeVersion: "~4.3.0" # use most recent 4.3.x version
useLocalCache: true # <--= Use the local cache (default is 'false').
useCloudCache: true
- name: Install CMake 3.31.x (Windows ARM64)
# windows-11-arm ships CMake 4.x, which removed pre-3.5 policy
# compatibility AND has incomplete ASM_ARMASM linker modules
# (breaks Boost.Context on ARM64). Pin to the last 3.x release.
if: runner.os == 'Windows' && inputs.arch == 'arm64'
shell: pwsh
run: |
$ver = "3.31.6"
$url = "https://github.com/Kitware/CMake/releases/download/v$ver/cmake-$ver-windows-arm64.zip"
Invoke-WebRequest -Uri $url -OutFile "$env:RUNNER_TEMP\cmake.zip"
Expand-Archive -Path "$env:RUNNER_TEMP\cmake.zip" -DestinationPath "$env:RUNNER_TEMP\cmake" -Force
$cmakeBin = "$env:RUNNER_TEMP\cmake\cmake-$ver-windows-arm64\bin"
if (-not (Test-Path "$cmakeBin\cmake.exe")) { throw "cmake.exe not found at $cmakeBin" }
Add-Content -Path $env:GITHUB_PATH -Value $cmakeBin
# Compiler cache. Pushes save it, so main keeps it warm; pull requests
# restore it and discard what they compiled. Objects are keyed on the
# preprocessed source, the compiler and the flags, so a leg only ever
# hits its own entries. A failed install costs the caching, not the build.
- name: Name the compiler cache leg
if: ${{ !inputs.macos-combine-only }}
shell: bash
run: |
leg="${{ runner.os }}-${{ inputs.arch || 'amd64' }}${{ runner.os == 'Windows' && format('-{0}', inputs.compiler) || '' }}"
# clang-cl refuses a precompiled header from another cl.exe build and ccache
# does not hash that build, so each one gets its own cache. The build number
# is read from cl.exe itself; the toolset directory keeps its name across patches.
if [ "${{ runner.os }}" = Windows ]; then
vswhere='/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe'
toolset=$(tr -d '\r\n' < "$("$vswhere" -latest -products '*' -find 'VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt' | tr -d '\r')")
cl=$("$vswhere" -latest -products '*' -find 'VC\Tools\MSVC\'"$toolset"'\**\cl.exe' | tr -d '\r' | head -1)
leg="$leg-vc$("$cl" 2>&1 | grep -o -E 'Version [0-9.]+' | cut -d' ' -f2)"
fi
echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV"
echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV"
# The action only installs and configures ccache. Restore and save go
# through actions/cache with one path string, since the cache service
# only matches entries saved under the identical path and the action
# spells it differently on Windows.
- name: Compiler cache
id: ccache
if: ${{ !inputs.macos-combine-only }}
continue-on-error: true
uses: hendrikmuhs/ccache-action@v1.2.24
with:
key: ${{ env.CCACHE_LEG }}
max-size: 3G
restore: false
save: false
# ccache -s runs as its own step; no summary table per job.
job-summary: ''
- name: Restore compiler cache
id: ccache_restore
if: ${{ steps.ccache.outcome == 'success' }}
uses: actions/cache/restore@v6
with:
path: ${{ github.workspace }}/.ccache
key: ${{ env.CCACHE_ENTRY }}
restore-keys: ccache-${{ env.CCACHE_LEG }}-
- name: Enable compiler cache
if: ${{ steps.ccache.outcome == 'success' }}
shell: bash
run: |
echo "CMAKE_C_COMPILER_LAUNCHER=ccache" >> "$GITHUB_ENV"
echo "CMAKE_CXX_COMPILER_LAUNCHER=ccache" >> "$GITHUB_ENV"
# Headers a fresh checkout has just written, the few files that
# use __DATE__ or __TIME__, and the precompiled header, whose
# macros ccache cannot see.
echo "CCACHE_SLOPPINESS=pch_defines,time_macros,include_file_mtime,include_file_ctime" >> "$GITHUB_ENV"
# Hash the includes the compiler reports instead of preprocessing
# every miss before compiling it.
echo "CCACHE_DEPEND=1" >> "$GITHUB_ENV"
# The restored directory carries the previous run's counters.
ccache -z
- name: Get the version and date on Ubuntu and macOS
if: runner.os != 'Windows'
run: |
ver_pure=$(grep 'set(SoftFever_VERSION' version.inc | cut -d '"' -f2)
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
ver="PR-${{ github.event.number }}"
git_commit_hash="${{ github.event.pull_request.head.sha }}"
else
ver=V$ver_pure
git_commit_hash=""
fi
echo "ver=$ver" >> $GITHUB_ENV
echo "ver_pure=$ver_pure" >> $GITHUB_ENV
echo "date=$(date +'%Y%m%d')" >> $GITHUB_ENV
echo "git_commit_hash=$git_commit_hash" >> $GITHUB_ENV
# Per-arch Linux AppImage naming: amd64 keeps the historical unsuffixed
# name (arch_suffix empty). Unused on macOS/Windows.
if [ '${{ inputs.arch }}' = 'aarch64' ]; then
echo "arch_suffix=_aarch64" >> $GITHUB_ENV
fi
shell: bash
- name: Get the version and date on Windows
if: runner.os == 'Windows'
run: |
$date = Get-Date -Format 'yyyyMMdd'
$ref = "${{ github.ref }}"
$eventName = "${{ github.event_name }}"
$prNumber = "${{ github.event.number }}"
if ($eventName -eq 'pull_request') {
$ver = "PR" + $prNumber
$git_commit_hash = "${{ github.event.pull_request.head.sha }}"
} else {
$versionContent = Get-Content version.inc -Raw
if ($versionContent -match 'set\(SoftFever_VERSION "(.*?)"\)') {
$ver = $matches[1]
}
$ver = "V$ver"
$git_commit_hash = ""
}
echo "ver=$ver" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
echo "date=$date" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
echo "git_commit_hash=$git_commit_hash" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
echo "date: ${{ env.date }} version: ${{ env.ver }}"
shell: pwsh
# Mac
- name: Install tools mac
if: runner.os == 'macOS' && !inputs.macos-combine-only
run: |
if [ -z "${{ vars.SELF_HOSTED }}" ]; then
brew install libtool
brew list
fi
mkdir -p ${{ github.workspace }}/deps/build/${{ inputs.arch }}
- name: Free disk space
if: runner.os == 'macOS' && !inputs.macos-combine-only && !vars.SELF_HOSTED
run: |
df -hI /dev/disk3s1s1
sudo find /Applications -maxdepth 1 -type d -name "Xcode_*.app" ! -name "Xcode_15.4.app" -exec rm -rf {} +
sudo rm -rf ~/Library/Developer/CoreSimulator/Caches/*
df -hI /dev/disk3s1s1
- name: Build slicer mac
if: runner.os == 'macOS' && !inputs.macos-combine-only
working-directory: ${{ github.workspace }}
# arm64 only: build the tests here; the unit_tests_macos job runs them.
env:
ORCA_TESTS_BUILD_ONLY: ${{ inputs.arch == 'arm64' && '1' || '' }}
run: |
./build_release_macos.sh -s -n -x ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a ${{ inputs.arch }} -t 10.15 ${{ inputs.arch == 'arm64' && '-T' || '' }}
- name: Pack unit tests mac
if: runner.os == 'macOS' && !inputs.macos-combine-only && inputs.arch == 'arm64'
working-directory: ${{ github.workspace }}
run: tar -cvf build_tests.tar build/arm64/tests
- name: Upload Test Artifact mac
if: runner.os == 'macOS' && !inputs.macos-combine-only && inputs.arch == 'arm64'
uses: actions/upload-artifact@v7
with:
name: ${{ github.sha }}-tests-macos-arm64
overwrite: true
path: build_tests.tar
retention-days: 5
if-no-files-found: error
- name: Build system preset cache (macOS)
if: runner.os == 'macOS' && !inputs.macos-combine-only
working-directory: ${{ github.workspace }}
shell: bash
# The bundle was already packed from resources/, so the caches have to be
# installed into it here; the source tree keeps its JSONs for later jobs.
run: ./scripts/build_preset_cache.sh -b build/${{ inputs.arch }} build/${{ inputs.arch }}/OrcaSlicer/OrcaSlicer.app/Contents/Resources/profiles
- name: Pack macOS app bundle ${{ inputs.arch }}
if: runner.os == 'macOS' && !inputs.macos-combine-only
working-directory: ${{ github.workspace }}
run: |
tar -czvf OrcaSlicer_Mac_bundle_${{ inputs.arch }}_${{ github.sha }}.tar.gz -C build/${{ inputs.arch }} OrcaSlicer
- name: Upload macOS app bundle ${{ inputs.arch }}
if: runner.os == 'macOS' && !inputs.macos-combine-only
uses: actions/upload-artifact@v7
with:
name: OrcaSlicer_Mac_bundle_${{ inputs.arch }}_${{ github.sha }}
path: ${{ github.workspace }}/OrcaSlicer_Mac_bundle_${{ inputs.arch }}_${{ github.sha }}.tar.gz
- name: Download macOS app bundles
if: runner.os == 'macOS' && inputs.macos-combine-only
uses: actions/download-artifact@v8
with:
pattern: OrcaSlicer_Mac_bundle_*_${{ github.sha }}
path: ${{ github.workspace }}/mac_bundles
- name: Extract macOS app bundles
if: runner.os == 'macOS' && inputs.macos-combine-only
working-directory: ${{ github.workspace }}
run: |
mkdir -p build/arm64 build/x86_64
arm_bundle=$(find "${{ github.workspace }}/mac_bundles/OrcaSlicer_Mac_bundle_arm64_${{ github.sha }}" -name '*.tar.gz' -print -quit)
x86_bundle=$(find "${{ github.workspace }}/mac_bundles/OrcaSlicer_Mac_bundle_x86_64_${{ github.sha }}" -name '*.tar.gz' -print -quit)
tar -xzvf "$arm_bundle" -C "${{ github.workspace }}/build/arm64"
tar -xzvf "$x86_bundle" -C "${{ github.workspace }}/build/x86_64"
- name: Build universal mac app bundle
if: runner.os == 'macOS' && inputs.macos-combine-only
working-directory: ${{ github.workspace }}
run: |
./build_release_macos.sh -u -x ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a universal -t 10.15
# Thanks to RaySajuuk, it's working now
- name: Sign app and notary
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'macOS' && inputs.macos-combine-only
working-directory: ${{ github.workspace }}
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
CERTIFICATE_ID: ${{ secrets.MACOS_CERTIFICATE_ID }}
run: |
# Load the `retry` helper (retries flaky commands such as `hdiutil create`).
source ${{ github.workspace }}/scripts/retry.sh
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode --output $CERTIFICATE_PATH
security create-keychain -p $KEYCHAIN_PASSWORD $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p $KEYCHAIN_PASSWORD $KEYCHAIN_PATH
# Set the temporary keychain as the default to prevent codesign from accessing the locked login keychain
security default-keychain -s "$KEYCHAIN_PATH"
security import $CERTIFICATE_PATH -P $P12_PASSWORD -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $P12_PASSWORD $KEYCHAIN_PATH
# codesign --deep is deprecated and cannot sign the bundled Python
# runtime (see relocate_python_runtime in build_release_macos.sh).
# Sign every nested Mach-O in the bundle explicitly -- notarization
# requires the hardened-runtime signature on each -- then seal the
# bundle itself last, and verify so coverage gaps fail at CI time
# rather than at notarization.
ENTITLEMENTS=${{ github.workspace }}/scripts/disable_validation.entitlements
sign() {
codesign --force --options runtime --timestamp --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE_ID" "$@"
}
sign_app() {
local app="$1"
local machos=()
# file is batched via xargs (per-file it costs minutes); universal
# binaries also print per-architecture lines, dropped by grep -v.
while IFS= read -r f; do machos+=("$f"); done < <(
find "$app" -type f -print0 |
xargs -0 file --no-pad --mime-type -- |
grep -v ' (for architecture ' |
grep ': application/x-mach-binary$' |
sed 's|: application/x-mach-binary$||'
)
if [ "${#machos[@]}" -eq 0 ]; then
echo "ERROR: no Mach-O files found in $app -- detection is broken" >&2
exit 1
fi
echo "signing ${#machos[@]} Mach-O files in $app"
sign "${machos[@]}"
sign --verbose "$app"
codesign --verify --deep --strict --verbose=2 "$app"
}
sign_app "${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer.app"
# Sign OrcaSlicer_profile_validator.app if it exists
if [ -f "${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator" ]; then
sign_app "${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app"
fi
# Create main OrcaSlicer DMG without the profile validator helper
mkdir -p ${{ github.workspace }}/build/universal/OrcaSlicer_dmg
rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/*
cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer.app ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/Applications
retry hdiutil create -volname "OrcaSlicer" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_dmg -ov -format UDZO OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
codesign --deep --force --verbose --options runtime --timestamp --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE_ID" OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
# Create separate OrcaSlicer_profile_validator DMG if the app exists
if [ -f "${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator" ]; then
mkdir -p ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg
rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/*
cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/Applications
retry hdiutil create -volname "OrcaSlicer Profile Validator" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg -ov -format UDZO OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
codesign --deep --force --verbose --options runtime --timestamp --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE_ID" OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
fi
# Notarize main DMG
xcrun notarytool submit "OrcaSlicer_Mac_universal_${{ env.ver }}.dmg" --apple-id "${{ secrets.APPLE_DEV_ACCOUNT }}" --team-id "${{ secrets.TEAM_ID }}" --password "${{ secrets.APP_PWD }}" --wait
xcrun stapler staple OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
# Notarize profile validator DMG if it exists
if [ -f "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" ]; then
xcrun notarytool submit "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" --apple-id "${{ secrets.APPLE_DEV_ACCOUNT }}" --team-id "${{ secrets.TEAM_ID }}" --password "${{ secrets.APP_PWD }}" --wait
xcrun stapler staple OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
fi
- name: Create DMG without notary
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/belt-printer' && runner.os == 'macOS' && inputs.macos-combine-only
working-directory: ${{ github.workspace }}
run: |
# Load the `retry` helper (retries flaky commands such as `hdiutil create`).
source ${{ github.workspace }}/scripts/retry.sh
mkdir -p ${{ github.workspace }}/build/universal/OrcaSlicer_dmg
rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/*
cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer.app ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/Applications
retry hdiutil create -volname "OrcaSlicer" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_dmg -ov -format UDZO OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
# Create separate OrcaSlicer_profile_validator DMG if the app exists
if [ -f "${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator" ]; then
mkdir -p ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg
rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/*
cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/Applications
retry hdiutil create -volname "OrcaSlicer Profile Validator" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg -ov -format UDZO OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
fi
# Delete the per-arch bundles only after signing/DMG creation succeeded, so a
# failed run keeps them available for a re-run instead of forcing a full rebuild.
- name: Delete intermediate per-arch artifacts
if: success() && runner.os == 'macOS' && inputs.macos-combine-only
uses: geekyeggo/delete-artifact@v6
with:
name: |
OrcaSlicer_Mac_bundle_arm64_${{ github.sha }}
OrcaSlicer_Mac_bundle_x86_64_${{ github.sha }}
- name: Upload artifacts mac
if: runner.os == 'macOS' && inputs.macos-combine-only
uses: actions/upload-artifact@v7
with:
name: OrcaSlicer_Mac_universal_${{ env.ver }}
path: ${{ github.workspace }}/OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
- name: Upload OrcaSlicer_profile_validator DMG mac
if: runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED
uses: actions/upload-artifact@v7
with:
name: OrcaSlicer_profile_validator_Mac_universal_DMG_${{ env.ver }}
path: ${{ github.workspace }}/OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
if-no-files-found: ignore
- name: Deploy Mac release
if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
asset_name: OrcaSlicer_Mac_universal_nightly${{ env.nightly_suffix }}.dmg
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
- name: Deploy Mac OrcaSlicer_profile_validator DMG release
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
asset_name: OrcaSlicer_profile_validator_Mac_universal_nightly.dmg
asset_content_type: application/octet-stream
max_releases: 1
# Windows
- name: Set Windows build variables
if: runner.os == 'Windows'
shell: pwsh
run: |
if ("${{ inputs.arch }}" -eq "arm64") {
"BUILD_DIR=build-arm64" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
"ARCH_SUFFIX=_arm64" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
} else {
"BUILD_DIR=build" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
"ARCH_SUFFIX=_x64" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
}
- name: setup MSVC
if: runner.os == 'Windows'
uses: microsoft/setup-msbuild@v3
- name: Install nsis
if: runner.os == 'Windows' && !vars.SELF_HOSTED
run: |
dir "C:/Program Files (x86)/Windows Kits/10/Include"
choco install nsis
- name: Build slicer Win
if: runner.os == 'Windows'
working-directory: ${{ github.workspace }}
# Orca: Removed Netfabb STL fixing service support in favor of CGAL.
# env:
# WindowsSdkDir: 'C:\Program Files (x86)\Windows Kits\10\'
# WindowsSDKVersion: '10.0.26100.0\'
# --tests builds the unit tests too; the unit_tests_windows_* jobs run them.
run: |
# cache-path is the install directory inside the deps build directory.
$deps = (Split-Path "${{ inputs.cache-path }}").Replace('\', '/')
# -l compiles with Visual Studio's clang-cl and -x builds with Ninja; --msvc --msbuild is cl under the Visual Studio generator.
$flags = if ("${{ inputs.compiler }}" -eq "clang") { "-l", "-x" } else { "--msvc", "--msbuild" }
.\build_win.bat -s --tests -i --arch ${{ inputs.arch }} --build-dir $env:BUILD_DIR --deps-dir $deps @flags
shell: pwsh
- name: Build system preset cache (Windows)
if: runner.os == 'Windows'
shell: cmd
# Shipped into both the already-installed tree (portable zip, MSIX) and
# the checkout cpack re-installs from when it builds the NSIS installer.
run: scripts\build_preset_cache.bat --prune-source "%BUILD_DIR%" "resources\profiles" "%BUILD_DIR%\OrcaSlicer\resources\profiles"
- name: Pack unit tests Win
if: runner.os == 'Windows'
working-directory: ${{ github.workspace }}
shell: pwsh
run: tar -cvf build_tests.tar ${{ env.BUILD_DIR }}/tests
- name: Upload Test Artifact Win
if: runner.os == 'Windows'
uses: actions/upload-artifact@v7
with:
name: ${{ github.sha }}-tests-windows-${{ inputs.arch }}
overwrite: true
path: build_tests.tar
retention-days: 5
if-no-files-found: error
# NSIS is x86-only; it runs (and the installer it emits runs) under ARM64's
# x86 emulation, packaging the native arm64 payload from build-arm64.
- name: Create installer Win
if: runner.os == 'Windows' && !vars.SELF_HOSTED
working-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}
run: |
cpack -G NSIS
- name: Pack app
if: runner.os == 'Windows'
working-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}
shell: cmd
run: '"C:/Program Files/7-Zip/7z.exe" a -tzip OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable.zip ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer'
- name: Pack PDB
if: runner.os == 'Windows' && inputs.arch != 'arm64' && !vars.SELF_HOSTED
working-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}/src/Release
shell: cmd
run: '"C:/Program Files/7-Zip/7z.exe" a -m0=lzma2 -mx9 Debug_PDB_${{ env.ver }}_for_developers_only.7z *.pdb'
- name: Upload artifacts Win zip
if: runner.os == 'Windows'
uses: actions/upload-artifact@v7
with:
name: OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable
path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer
- name: Upload artifacts Win installer
if: runner.os == 'Windows' && !vars.SELF_HOSTED
uses: actions/upload-artifact@v7
with:
name: OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}
path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer*.exe
- name: Upload artifacts Win PDB
if: runner.os == 'Windows' && inputs.arch != 'arm64' && !vars.SELF_HOSTED
uses: actions/upload-artifact@v7
with:
name: PDB
path: ${{ github.workspace }}/build/src/Release/Debug_PDB_${{ env.ver }}_for_developers_only.7z
- name: Upload OrcaSlicer_profile_validator Win
if: runner.os == 'Windows' && inputs.arch != 'arm64' && !vars.SELF_HOSTED
uses: actions/upload-artifact@v7
with:
name: OrcaSlicer_profile_validator_Windows_${{ env.ver }}
path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe
- name: Deploy Windows release portable
if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable.zip
asset_name: OrcaSlicer_Windows${{ env.ARCH_SUFFIX }}_nightly${{ env.nightly_suffix }}_portable.zip
asset_content_type: application/x-zip-compressed
max_releases: 1
- name: Deploy Windows release installer
if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe
asset_name: OrcaSlicer_Windows_Installer${{ env.ARCH_SUFFIX }}_nightly${{ env.nightly_suffix }}.exe
asset_content_type: application/x-msdownload
max_releases: 1
- name: Deploy Windows OrcaSlicer_profile_validator release
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && inputs.arch != 'arm64' && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe
asset_name: OrcaSlicer_profile_validator_Windows_nightly.exe
asset_content_type: application/x-msdownload
max_releases: 1
- name: Build MSIX Store package Win
if: runner.os == 'Windows' && !vars.SELF_HOSTED
working-directory: ${{ github.workspace }}
shell: pwsh
run: |
./scripts/msix/build_msix.ps1 `
-InstallDir "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer" `
-OutputPath "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_MSIX_${{ env.ver }}${{ env.ARCH_SUFFIX }}.msix" `
-Architecture "${{ inputs.arch }}" `
-IdentityName "${{ vars.ORCA_MSIX_IDENTITY_NAME || 'OrcaSlicer.OrcaSlicer' }}" `
-Publisher "${{ vars.ORCA_MSIX_PUBLISHER || 'CN=38F7EA55-C73B-4072-B3B2-C8E0EA15BB82' }}" `
-PublisherDisplayName "${{ vars.ORCA_MSIX_PUBLISHER_DISPLAY_NAME || 'OrcaSlicer' }}"
- name: Upload artifacts Win MSIX
if: runner.os == 'Windows' && !vars.SELF_HOSTED
uses: actions/upload-artifact@v7
with:
name: OrcaSlicer_Windows_MSIX_${{ env.ver }}${{ env.ARCH_SUFFIX }}
path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_MSIX_${{ env.ver }}${{ env.ARCH_SUFFIX }}.msix
# Ubuntu
- name: Apt-Install Dependencies
if: runner.os == 'Linux' && !vars.SELF_HOSTED
uses: ./.github/actions/apt-install-deps
# Tests must built at the same time as the slicer;
# if you untangle them feel free to separate them here too
- name: Build slicer and tests
if: runner.os == 'Linux'
shell: bash
run: |
# Build + tar the unit tests (-t) on both Linux legs so each arch
# (x86_64 + aarch64) gets tested by its own unit_tests_linux_* job.
./build_linux.sh -istrlL
./scripts/check_appimage_libs.sh ./build/package ./build/package/bin/orca-slicer
appimage=./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_${{ env.ver }}.AppImage
mv -n ./build/OrcaSlicer_Linux_V${{ env.ver_pure }}.AppImage "$appimage"
chmod +x "$appimage"
tar -cvf build_tests.tar build/tests
# Use tar because upload-artifacts won't always preserve directory structure
# and doesn't preserve file permissions
- name: Upload Test Artifact
if: runner.os == 'Linux'
uses: actions/upload-artifact@v7
with:
name: ${{ github.sha }}-tests-linux-${{ inputs.arch == 'aarch64' && 'aarch64' || 'x86_64' }}
overwrite: true
path: build_tests.tar
retention-days: 5
if-no-files-found: error
- name: Build system preset cache (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
# Both were packed from resources/ before the caches existed, so the
# AppImage is unpacked first and the caches shipped into it and into
# the package tree; the source tree keeps its JSONs for later steps.
appimage=$(find build -maxdepth 1 -name "OrcaSlicer_Linux_AppImage*.AppImage" | head -1)
chmod +x "$appimage"
"$appimage" --appimage-extract
./scripts/build_preset_cache.sh -b build build/package/resources/profiles squashfs-root/resources/profiles
appimagetool=$(find build -name "appimagetool.AppImage" | head -1)
ARCH=$(uname -m) "$appimagetool" --appimage-extract-and-run squashfs-root "$appimage"
rm -rf squashfs-root
# Ship the freshly-built validator so slice_check_linux (build_all.yml)
# can slice-sweep the shipped profiles with this PR's engine. Taken from
# the aarch64 leg so the sweep also exercises the arm build; x86_64 on
# SELF_HOSTED, which skips arm64.
- name: Upload profile validator (for slice check)
if: ${{ runner.os == 'Linux' && (vars.SELF_HOSTED && inputs.arch != 'aarch64' || !vars.SELF_HOSTED && inputs.arch == 'aarch64') }}
uses: actions/upload-artifact@v7
with:
name: ${{ github.sha }}-profile-validator-linux-${{ inputs.arch == 'aarch64' && 'aarch64' || 'x86_64' }}
overwrite: true
path: ./build/src/Release/OrcaSlicer_profile_validator
retention-days: 5
if-no-files-found: error
- name: Run external slicer regression tests
if: runner.os == 'Linux' && inputs.arch != 'aarch64'
timeout-minutes: 20
shell: bash
run: |
test_repo_dir="${{ runner.temp }}/orca-test-repo"
rm -rf "$test_repo_dir"
git clone --depth 1 https://github.com/OrcaSlicer/orca-test-repo.git "$test_repo_dir"
python3 "$test_repo_dir/run_test.py" "${{ github.workspace }}/build/package/bin/orca-slicer"
- name: Build orca_custom_preset_tests
if: github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64'
working-directory: ${{ github.workspace }}/build/src/Release
shell: bash
run: |
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -g 1
cd ${{ github.workspace }}/resources/profiles
zip -r orca_custom_preset_tests.zip user/
- name: Upload artifacts Ubuntu
if: ${{ ! env.ACT && runner.os == 'Linux' }}
uses: actions/upload-artifact@v7
with:
name: OrcaSlicer_Linux_ubuntu_${{ env.ubuntu-ver }}${{ env.arch_suffix }}_${{ env.ver }}
path: "./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_${{ env.ver }}.AppImage"
- name: Upload OrcaSlicer_profile_validator Ubuntu
if: ${{ ! env.ACT && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: actions/upload-artifact@v7
with:
name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
path: './build/src/Release/OrcaSlicer_profile_validator'
# generate_system_cache is what scripts/build_preset_cache.sh bakes the
# <vendor>.opc caches with; it was already built by the "Build system
# preset cache (Linux)" step above. The .opc format is 64-bit
# little-endian native, i.e. identical across every platform Orca ships,
# so only the Linux binary is published.
- name: Upload generate_system_cache Ubuntu
if: ${{ ! env.ACT && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: actions/upload-artifact@v7
with:
name: generate_system_cache_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
path: './build/src/dev-utils/Release/generate_system_cache'
- name: Deploy Ubuntu release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_${{ env.ver }}.AppImage
asset_name: OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_nightly${{ env.nightly_suffix }}.AppImage
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
- name: Deploy Ubuntu release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: rickstaa/action-create-tag@v1
with:
tag: "nightly-builds"
tag_exists_error: false
force_push_tag: true
message: "nightly-builds"
- name: Deploy Ubuntu OrcaSlicer_profile_validator release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ./build/src/Release/OrcaSlicer_profile_validator
asset_name: OrcaSlicer_profile_validator_Linux${{ env.ubuntu-ver-str }}_nightly
asset_content_type: application/octet-stream
max_releases: 1
- name: Deploy Ubuntu generate_system_cache release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ./build/src/dev-utils/Release/generate_system_cache
asset_name: generate_system_cache_Linux${{ env.ubuntu-ver-str }}_nightly
asset_content_type: application/octet-stream
max_releases: 1
- name: Deploy orca_custom_preset_tests
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/resources/profiles/orca_custom_preset_tests.zip
asset_name: orca_custom_preset_tests.zip
asset_content_type: application/octet-stream
max_releases: 1
# The build has just touched everything it can use, so an object
# untouched for a week is dead, usually orphaned by a flag change.
- name: Compiler cache statistics
if: ${{ always() && steps.ccache.outcome == 'success' }}
shell: bash
run: |
ccache --evict-older-than 7d
ccache -s -v || ccache -s
# Entries are immutable, so the new one is saved first and the older
# ones for this leg on this ref are dropped afterwards: a failed save
# leaves the previous entry in place. A cancelled or failed build saves
# too, since what it compiled is still valid; a restore that did not
# finish does not, since the directory may be a truncated copy.
- name: Save compiler cache
id: ccache_save
if: ${{ always() && steps.ccache_restore.outcome == 'success' && github.event_name != 'pull_request' }}
uses: actions/cache/save@v6
with:
path: ${{ github.workspace }}/.ccache
key: ${{ env.CCACHE_ENTRY }}
- name: Drop older compiler cache entries
if: ${{ always() && steps.ccache_save.outcome == 'success' }}
# A read-only token (fork PRs) cannot delete; that only costs storage.
# Older means a lower run id, so two runs finishing close together keep
# the newer entry whichever of them cleans up last.
continue-on-error: true
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
gh cache list --ref "$GITHUB_REF" --key "ccache-$CCACHE_LEG-" --limit 100 --json id,key \
| jq -r --arg prefix "ccache-$CCACHE_LEG-" --argjson run "$GITHUB_RUN_ID" \
'.[] | select((.key | ltrimstr($prefix) | split("-")[0] | tonumber?) < $run) | .id' \
| tr -d '\r' \
| while read -r id; do gh cache delete "$id"; done
-44
View File
@@ -1,44 +0,0 @@
name: Check locale
on:
pull_request:
branches:
- main
paths:
- 'localization/**'
- ".github/workflows/check_locale.yml"
jobs:
check_translation:
name: Check translation
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Install gettext
run: |
sudo apt-get update
sudo apt-get install -y gettext
# don't call ./run_gentext.sh as most translators never sync with main branch...
- name: Check translation format
run: |
echo $PWD
pot_file="./localization/i18n/OrcaSlicer.pot"
for dir in ./localization/i18n/*/
do
dir=${dir%*/} # remove the trailing "/"
lang=${dir##*/} # extract the language identifier
if [ -f "$dir/OrcaSlicer_${lang}.po" ]; then
mkdir -p ./resources/i18n/${lang}/
msgfmt --check-format -o ./resources/i18n/${lang}/OrcaSlicer.mo $dir/OrcaSlicer_${lang}.po
# Check the exit status of the msgfmt command
if [ $? -ne 0 ]; then
echo "Error encountered with msgfmt command for language ${lang}."
exit 1 # Exit the script with an error status
fi
fi
done
-271
View File
@@ -1,271 +0,0 @@
name: Check profiles
on:
pull_request:
# release/* is included because pr-merge-bot.yml lets delegates merge into
# it, and it gates on this workflow's result. Without it a delegated merge
# into a release branch would run no profile validation at all.
branches:
- main
- release/*
paths:
- 'resources/profiles/**'
# orca_profile_tool.py also validates resources/printers/bambu_filament_ids.json, and
# both it and its tests live in scripts/, so a PR touching only those must still run
# this workflow.
- 'resources/printers/**'
- 'scripts/**'
- ".github/workflows/check_profiles.yml"
workflow_dispatch:
inputs:
logLevel:
description: 'Log level'
required: true
default: 'warning'
permissions:
contents: read
jobs:
check_profiles:
# This job name is the check-run name pr-merge-bot.yml requires before a
# delegated merge. Renaming it silently disables that gate.
name: Check profiles
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v7
# Deliberately not continue-on-error, unlike every check below: if the tool itself is
# broken, nothing it then reports about the profiles is worth reading.
- name: Run the profile tool's own unit tests
run: python3 -m unittest discover -s scripts/tests -t scripts
# What the validator below cannot see. It loads the tree the way the slicer does, so
# it never notices a profile no <vendor>.json indexes, a preset name two files claim,
# an id that is not the mint of its own triple, or a file that normalize and
# update-index would still rewrite.
# The step id is the handle the PR comment and the failure gate below use; renaming it
# silently disables them.
- name: Check profiles (orca_profile_tool.py)
id: profile_tool
continue-on-error: true
run: |
set +e
python3 ./scripts/orca_profile_tool.py check 2>&1 | tee ${{ runner.temp }}/profile_tool.log
exit ${PIPESTATUS[0]}
# download
- name: Download
working-directory: ${{ github.workspace }}
run: |
curl -L -o OrcaSlicer_profile_validator https://github.com/OrcaSlicer/OrcaSlicer/releases/download/nightly-builds/OrcaSlicer_profile_validator_Linux_Ubuntu2404_nightly
chmod +x ./OrcaSlicer_profile_validator
# Validate all system profiles.
- name: validate system profiles
id: validate_system
continue-on-error: true
run: |
set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log
exit ${PIPESTATUS[0]}
# Slice a two-colour cube through every printer so all custom g-code (incl. change_filament_gcode)
# is expanded - catches undefined-placeholder / invalid-flow bugs the static checks above cannot see.
- name: validate slice (expand custom g-code)
id: validate_slice
continue-on-error: true
run: |
set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -s -l 2 2>&1 | tee ${{ runner.temp }}/validate_slice.log
exit ${PIPESTATUS[0]}
# All vendors' filament_id collisions were fixed, so the duplicate-filament-subtype
# check runs tree-wide.
- name: validate filament subtype check
id: validate_filament_subtypes
continue-on-error: true
run: |
set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 -f 2>&1 | tee ${{ runner.temp }}/validate_filament_subtypes.log
exit ${PIPESTATUS[0]}
- name: validate custom presets
id: validate_custom
continue-on-error: true
working-directory: ${{ github.workspace }}
run: |
fixtures_dir="${{ runner.temp }}/profile-fixtures"
output_dir="${{ runner.temp }}/custom-preset-validation"
combined_log="${{ runner.temp }}/validate_custom.log"
summary="${output_dir}/summary.md"
release_url="https://github.com/OrcaSlicer/OrcaSlicer-profile-validator/releases/download/fixture-archive"
rm -rf "${fixtures_dir}" "${output_dir}"
mkdir -p "${fixtures_dir}" "${output_dir}"
curl -fsSL -o "${fixtures_dir}/manifest.json" "${release_url}/manifest.json"
MANIFEST_PATH="${fixtures_dir}/manifest.json" python3 <<'PY' > "${fixtures_dir}/fixtures.tsv"
import json
import os
with open(os.environ["MANIFEST_PATH"], encoding="utf-8") as fh:
manifest = json.load(fh)
if isinstance(manifest, dict):
entries = manifest.get("fixtures", [])
else:
entries = manifest
for entry in entries:
version = entry.get("version", "")
asset = entry.get("asset", "")
sha256 = entry.get("asset_sha256", "")
if not version or not asset:
continue
print(f"{version}\t{asset}\t{sha256}")
PY
if [ ! -s "${fixtures_dir}/fixtures.tsv" ]; then
echo "No custom preset fixtures found in ${release_url}/manifest.json" | tee "${combined_log}"
exit 1
fi
{
echo "## Custom Preset Fixture Validation"
echo ""
echo "| Version | Status | Log |"
echo "| --- | --- | --- |"
} > "${summary}"
status=0
failed_logs=()
while IFS=$'\t' read -r version asset expected_sha256; do
fixture_zip="${fixtures_dir}/${asset}"
asset_url_name="$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "${asset}")"
profile_tree="${output_dir}/profiles-${version}"
log_path="${output_dir}/${version}.log"
curl -fsSL -o "${fixture_zip}" "${release_url}/${asset_url_name}"
if [ -n "${expected_sha256}" ] && [ "${expected_sha256}" != "<sha256>" ]; then
echo "${expected_sha256} ${fixture_zip}" | sha256sum -c -
fi
rm -rf "${profile_tree}"
mkdir -p "${profile_tree}"
cp -a "${{ github.workspace }}/resources/profiles/." "${profile_tree}/"
rm -rf "${profile_tree}/user"
unzip -q "${fixture_zip}" -d "${profile_tree}"
set +e
./OrcaSlicer_profile_validator -p "${profile_tree}" -l 2 > "${log_path}" 2>&1
result=$?
set -e
if [ "${result}" -eq 0 ]; then
echo "| ${version} | PASS | ${version}.log |" >> "${summary}"
else
echo "| ${version} | FAIL | ${version}.log |" >> "${summary}"
failed_logs+=("${log_path}")
status=1
fi
done < "${fixtures_dir}/fixtures.tsv"
{
cat "${summary}"
if [ "${#failed_logs[@]}" -gt 0 ]; then
echo ""
echo "## Failed Fixture Logs"
for log_path in "${failed_logs[@]}"; do
echo ""
echo "### $(basename "${log_path}" .log)"
echo '```'
head -c 12000 "${log_path}" || echo "No output captured"
echo '```'
done
fi
} | tee "${combined_log}"
exit "${status}"
- name: Prepare PR number for comment workflow
if: ${{ always() && github.event_name == 'pull_request' }}
run: |
mkdir -p ${{ runner.temp }}/profile-check-results
echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt
- name: Prepare comment artifact
if: ${{ always() && github.event_name == 'pull_request' && (steps.profile_tool.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
run: |
{
# Marker matched by check_profiles_comment.yml to delete prior comments.
echo "<!-- profile-validation-comment -->"
echo "## :x: Profile Validation Errors"
echo ""
if [ "${{ steps.profile_tool.outcome }}" = "failure" ]; then
echo "### Profile Check Failed (orca_profile_tool.py)"
echo ""
echo '```'
head -c 30000 ${{ runner.temp }}/profile_tool.log || echo "No output captured"
echo '```'
echo ""
fi
if [ "${{ steps.validate_system.outcome }}" = "failure" ]; then
echo "### System Profile Validation Failed"
echo ""
echo '```'
head -c 30000 ${{ runner.temp }}/validate_system.log || echo "No output captured"
echo '```'
echo ""
fi
if [ "${{ steps.validate_slice.outcome }}" = "failure" ]; then
echo "### Slice Validation Failed (custom g-code expansion)"
echo ""
echo '```'
head -c 30000 ${{ runner.temp }}/validate_slice.log || echo "No output captured"
echo '```'
echo ""
fi
if [ "${{ steps.validate_filament_subtypes.outcome }}" = "failure" ]; then
echo "### Filament Subtype Validation Failed"
echo ""
echo '```'
head -c 30000 ${{ runner.temp }}/validate_filament_subtypes.log || echo "No output captured"
echo '```'
echo ""
fi
if [ "${{ steps.validate_custom.outcome }}" = "failure" ]; then
echo "### Custom Preset Validation Failed"
echo ""
echo '```'
head -c 30000 ${{ runner.temp }}/validate_custom.log || echo "No output captured"
echo '```'
echo ""
fi
echo "---"
echo '*Fix the errors above and push a new commit. To reproduce this run locally: `scripts/check_profile.sh`, or `scripts\check_profile.bat` on Windows.*'
} > ${{ runner.temp }}/profile-check-results/pr_comment.md
- name: Upload comment artifact
if: ${{ always() && github.event_name == 'pull_request' }}
uses: actions/upload-artifact@v7
with:
name: profile-check-results
path: ${{ runner.temp }}/profile-check-results/
retention-days: 1
- name: Fail if any check failed
if: ${{ always() && (steps.profile_tool.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
run: |
echo "One or more profile checks failed; see the step logs above."
echo 'Reproduce the whole run locally with scripts/check_profile.sh (scripts\check_profile.bat on Windows).'
exit 1
@@ -1,66 +0,0 @@
name: Post profile check comment
# NOTE: The workflow name in the 'workflows' filter below must match the 'name'
# field in check_profiles.yml exactly. If that name changes, update it here too.
on:
workflow_run:
workflows: ["Check profiles"]
types:
- completed
permissions:
pull-requests: write
# Needed to delete outdated bot comments via the issues/comments endpoint.
issues: write
# Serialize handlers per source branch so parallel runs don't race delete-and-post.
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: false
jobs:
post_comment:
name: Post PR comment
runs-on: ubuntu-24.04
if: ${{ github.event.workflow_run.event == 'pull_request' && (github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure') }}
steps:
- name: Download artifact
id: download
uses: actions/download-artifact@v8
continue-on-error: true
with:
name: profile-check-results
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Update PR comment
if: ${{ steps.download.outcome == 'success' }}
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
if [ ! -f pr_number.txt ]; then
echo "No pr_number.txt in artifact, skipping."
exit 0
fi
PR_NUMBER=$(cat pr_number.txt)
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "Invalid PR number: $PR_NUMBER"
exit 1
fi
# Delete prior comments matching the marker (from check_profiles.yml) or the legacy heading.
OLD_IDS=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" \
--jq '.[] | select(.user.login == "github-actions[bot]") | select((.body | startswith("<!-- profile-validation-comment -->")) or (.body | startswith("## :x: Profile Validation Errors"))) | .id')
for comment_id in $OLD_IDS; do
echo "Deleting outdated profile-validation comment ${comment_id}"
gh api -X DELETE "repos/${GH_REPO}/issues/comments/${comment_id}" || true
done
# Post a new comment only when validation failed (pr_comment.md present).
if [ -f pr_comment.md ]; then
gh pr comment "$PR_NUMBER" --body-file pr_comment.md
else
echo "Validation succeeded; cleaned up prior comments without posting."
fi
-81
View File
@@ -1,81 +0,0 @@
name: Orca Issue Dedupe
description: Automatically dedupe GitHub issues using AI
on:
issues:
types: [opened]
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number to process for duplicate detection'
required: true
type: string
jobs:
dedupe-issues:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Run Claude Code slash command
uses: anthropics/claude-code-base-action@beta
with:
prompt: "/dedupe ${{ github.repository }}/issues/${{ github.event.issue.number || inputs.issue_number }}"
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
model: "claude-sonnet-4-5-20250929"
claude_env: |
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Log duplicate comment event to Statsig
if: always()
env:
STATSIG_API_KEY: ${{ secrets.STATSIG_API_KEY }}
run: |
ISSUE_NUMBER=${{ github.event.issue.number || inputs.issue_number }}
REPO=${{ github.repository }}
if [ -z "$STATSIG_API_KEY" ]; then
echo "STATSIG_API_KEY not found, skipping Statsig logging"
exit 0
fi
# Prepare the event payload
EVENT_PAYLOAD=$(jq -n \
--arg issue_number "$ISSUE_NUMBER" \
--arg repo "$REPO" \
--arg triggered_by "${{ github.event_name }}" \
'{
events: [{
eventName: "github_duplicate_comment_added",
value: 1,
metadata: {
repository: $repo,
issue_number: ($issue_number | tonumber),
triggered_by: $triggered_by,
workflow_run_id: "${{ github.run_id }}"
},
time: (now | floor | tostring)
}]
}')
# Send to Statsig API
echo "Logging duplicate comment event to Statsig for issue #${ISSUE_NUMBER}"
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST https://events.statsigapi.net/v1/log_event \
-H "Content-Type: application/json" \
-H "STATSIG-API-KEY: ${STATSIG_API_KEY}" \
-d "$EVENT_PAYLOAD")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | head -n-1)
if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 202 ]; then
echo "Successfully logged duplicate comment event for issue #${ISSUE_NUMBER}"
else
echo "Failed to log duplicate comment event for issue #${ISSUE_NUMBER}. HTTP ${HTTP_CODE}: ${BODY}"
fi
-85
View File
@@ -1,85 +0,0 @@
name: Generate Doxygen Documentation
on:
schedule:
- cron: '0 0 * * 1' # Every Monday at midnight UTC
workflow_dispatch: # Manual trigger
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs:
build-and-deploy:
name: Build and Deploy Docs
runs-on: ubuntu-latest
timeout-minutes: 60
# Only run on main branch of the main repository
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main'
permissions:
contents: read
steps:
# Doxygen with call graphs over all of src/ outgrows the runner's RAM;
# replace the runner's swapfile with an 8 GB one.
- name: Grow swap space
run: |
set -euo pipefail
sudo swapoff -a
sudo rm -f /swapfile
sudo fallocate -l 8G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
free -h
- name: Checkout repository
uses: actions/checkout@v7
- name: Install Doxygen and Graphviz
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y doxygen graphviz
- name: Generate documentation
run: |
set -euo pipefail
# Override DOT_NUM_THREADS to avoid parallel dot race condition bug
sed -i 's/^DOT_NUM_THREADS.*/DOT_NUM_THREADS = 1/' .doxygen
doxygen .doxygen
# Verify documentation was generated
if [ ! -f "internal_docs/index.html" ]; then
echo "Error: Documentation generation failed - index.html not found"
exit 1
fi
- name: Install Rclone
run: |
set -euo pipefail
sudo -v
curl -fsSL https://rclone.org/install.sh | sudo bash
- name: optimize
run: |
set -euo pipefail
rm -f internal_docs/Nodes.xml internal_docs/Tokens.xml
find internal_docs -name "*.map" -type f -delete || true
find internal_docs -name "*.md5" -type f -delete || true
- name: upload
# We configure rclone dynamically using environment variables
run: |
set -euo pipefail
# Remove existing config if it exists to avoid conflicts
rclone config delete cloudflare 2>/dev/null || true
rclone config create cloudflare s3 \
provider Cloudflare \
access_key_id ${{ secrets.R2_ACCESS_KEY_ID }} \
secret_access_key ${{ secrets.R2_SECRET_ACCESS_KEY }} \
endpoint ${{ secrets.R2_ENDPOINT }}
rclone sync internal_docs/ cloudflare:orcaslicer-internals \
--progress \
--transfers 512 \
--checkers 512
echo "Documentation upload completed successfully"
-219
View File
@@ -1,219 +0,0 @@
# Nightly parity checks from OrcaSlicer/orca-test-repo, kept out of the
# per-build "Run external slicer regression tests" step because they take far
# longer than that step's budget:
# effect - the CLI override sweep's full effect stage: every landed option
# re-sliced on its own to see whether it changes the G-code
# harness - the GUI-vs-CLI parity harness (metrics only, never fails)
# Both test the latest successful build_all.yml Linux AppImage from main, with
# sources checked out at the commit that build was made from. Nothing here
# gates a build or a PR.
name: Parity Nightly
on:
schedule:
# build_all.yml starts at 17:00 UTC and has finished by ~20:00
- cron: "0 21 * * *"
workflow_dispatch:
inputs:
test_repo_ref:
description: "orca-test-repo ref to run"
required: false
default: "main"
build_branch:
description: "branch whose latest successful build_all artifact to test"
required: false
default: "main"
fixtures:
description: "harness fixture ids, space-separated (empty = all)"
required: false
default: ""
cli_presets:
description: "harness lane C presets: flat = flatten inherits first, raw = leaf profile as-is"
required: false
default: "flat"
permissions:
contents: read
actions: read
jobs:
build:
name: Find the build to test
# Don't run scheduled checks on forks
if: github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer'
runs-on: ubuntu-24.04
outputs:
run_id: ${{ steps.find.outputs.run_id }}
head_sha: ${{ steps.find.outputs.head_sha }}
steps:
- id: find
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
gh run list --workflow build_all.yml \
--branch "${{ inputs.build_branch || 'main' }}" \
--status success --limit 1 --json databaseId,headSha \
--jq '"run_id=\(.[0].databaseId)\nhead_sha=\(.[0].headSha)"' \
>> "$GITHUB_OUTPUT"
cat "$GITHUB_OUTPUT"
effect:
name: Override sweep effect stage (shard ${{ matrix.shard }})
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
# orca-test-repo's parity/effect_routing.json holds a 2-way split,
# ~12.5 min a shard on this runner
shard: [0, 1]
steps:
- &checkout-suite
name: Check out the test suite
uses: actions/checkout@v7
with:
repository: OrcaSlicer/orca-test-repo
ref: ${{ inputs.test_repo_ref || 'main' }}
path: orca-test-repo
# The AppImage ships only packed preset caches, so profiles and the CLI
# option surface come from the sources the build was made from
- &checkout-slicer
name: Check out OrcaSlicer at the build's commit
uses: actions/checkout@v7
with:
ref: ${{ needs.build.outputs.head_sha }}
path: slicer
lfs: 'false'
- &extract-appimage
name: Download and extract the Linux AppImage
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
gh run download "${{ needs.build.outputs.run_id }}" --dir appimage \
--pattern "OrcaSlicer_Linux_ubuntu_2404*"
appimage=$(find appimage -name "*.AppImage" ! -name "*aarch64*" | head -1)
[ -n "$appimage" ] || { echo "no x86_64 AppImage in run ${{ needs.build.outputs.run_id }}"; exit 1; }
chmod +x "$appimage"
"$appimage" --appimage-extract > /dev/null
# The bare binary cannot find the AppImage's bundled libraries; AppRun
# sets them up and execs it, so exit codes and signals pass through
[ -x squashfs-root/AppRun ] || { echo "no AppRun in the AppImage"; exit 1; }
echo "ORCA_BIN=$PWD/squashfs-root/AppRun" >> "$GITHUB_ENV"
echo "ORCA_SOURCE=$PWD/slicer" >> "$GITHUB_ENV"
- name: Install the AppImage's host runtime dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install suite dependencies
run: pip install -r orca-test-repo/requirements.txt
- name: Run the override sweep with the full effect stage
id: run
continue-on-error: true
working-directory: orca-test-repo
run: |
set -o pipefail
# -rA keeps the per-stage summaries, which pytest otherwise swallows
# for passing tests
python -m pytest test_cli_overrides.py -c pytest.ini -v -rA \
--effect-full --effect-shard ${{ matrix.shard }}/2 \
--orca-bin "$ORCA_BIN" --orca-source "$ORCA_SOURCE" \
2>&1 | tee ../sweep.log
- name: Publish job summary
if: always()
run: |
{
echo "## Override sweep effect stage, shard ${{ matrix.shard }}/2"
echo "Build ${{ needs.build.outputs.head_sha }} (run ${{ needs.build.outputs.run_id }})"
echo '```'
grep -E "\[override sweep" sweep.log || echo "no stage summaries, see the log"
grep -E "^=+ .*(passed|failed)" sweep.log | tail -1 || true
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload the override report
if: always()
uses: actions/upload-artifact@v7
with:
name: override-report-shard${{ matrix.shard }}
path: |
orca-test-repo/.pytest_cache/override_report.json
sweep.log
if-no-files-found: warn
retention-days: 30
# The sweep step continues on error so the summary and report still get
# published; this puts the failure back on the job
- name: Fail the job if the sweep failed
if: steps.run.outcome == 'failure'
run: |
echo "the override sweep failed, see the job summary and the uploaded report" >&2
exit 1
harness:
name: GUI-vs-CLI parity harness
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 180
steps:
- *checkout-suite
- *checkout-slicer
- *extract-appimage
- name: Install display tooling and the AppImage's host runtime
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
xvfb xdotool imagemagick openbox mesa-utils \
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
- name: Run the parity harness
run: |
set -euo pipefail
fixtures=()
for f in ${{ inputs.fixtures || '' }}; do
fixtures+=(--fixture "$f")
done
# 2 GUI displays: ~1.5 cores peak / ~1.9 GB on this 4-vCPU runner,
# and each fixture is fully isolated, so results match a serial run
python3 orca-test-repo/parity/run_parity.py \
--slicer-root "$ORCA_SOURCE" --bin "$ORCA_BIN" \
--cli-presets "${{ inputs.cli_presets || 'flat' }}" \
--gui-workers 2 --out "$PWD/parity-out" "${fixtures[@]}"
- name: Publish job summary
if: always()
run: |
if [ -f parity-out/report.md ]; then
cat parity-out/report.md >> "$GITHUB_STEP_SUMMARY"
else
echo "the harness produced no report, see the log" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Drop per-lane datadirs before upload
if: always()
run: rm -rf parity-out/*/seed parity-out/*/datadir-* || true
- name: Upload the scorecard and evidence
if: always()
uses: actions/upload-artifact@v7
with:
name: parity-scorecard
path: parity-out/
if-no-files-found: warn
retention-days: 30
-253
View File
@@ -1,253 +0,0 @@
name: Post-merge profiles
# Push-triggered counterpart to check_profiles.yml (which only gates PRs). When a
# profile change lands on main or a release branch, rebuild the affected vendors'
# binary preset caches (<vendor>.opc) and publish each as a versioned ZIP asset on
# a per-Orca-version release of the profiles repo. From there OrcaCloud's OTA
# Manager lists the asset, a maintainer attaches a changelog and hits Publish, and
# only then does it become a live OTA update - this workflow does none of that
# last part (no changelog, no R2, no webhook).
#
# Asset contract expected by OrcaCloud's release scanner:
# ^(\d+\.\d+\.\d+)_([^_]+)_(\d+(?:\.\d+){3})_(\d{12})\.zip$
# <orca_ver>_<vendor>_<profile_version>_<UTC yyyymmddHHMM>.zip (zip root: <vendor>.opc)
#
# Setup (App + secrets): docs/ota/post-merge-profiles-setup.md
on:
push:
branches:
- main
- release/*
paths:
- 'resources/profiles/**'
- '.github/workflows/post_merge_profiles.yml'
workflow_dispatch:
permissions:
contents: read
# One run per branch; let a run finish rather than cancel it, since it publishes.
concurrency:
group: post-merge-profiles-${{ github.ref }}
cancel-in-progress: false
env:
# generate_system_cache is published to this repo's own nightly-builds release
# by build_orca.yml's Linux leg. The job guard pins github.repository to
# OrcaSlicer/OrcaSlicer, so this resolves there.
TOOL_REPO: ${{ github.repository }}
TOOL_ASSET: generate_system_cache_Linux_Ubuntu2404_nightly
# Where per-vendor ZIP assets are published; OrcaCloud's OTA reads this repo.
PROFILES_OWNER: OrcaSlicer
PROFILES_REPO: orcaslicer-profiles
jobs:
publish_profile_caches:
name: Publish profile caches
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }}
# FOLDER_MERGERS is an environment-scoped variable, shared with the PR
# merge bot. Keep this environment free of protection rules so this
# push-triggered job does not wait for a reviewer.
environment: merge-delegation
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
# Enough history to reach github.event.before for the changed-vendor
# diff on a normal push; deeper pushes fall back to HEAD^..HEAD in the
# step below. fetch-depth: 0 would clone all of OrcaSlicer's history.
fetch-depth: 50
- name: Resolve changed vendors
id: vendors
shell: bash
env:
FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }}
run: |
set -euo pipefail
base='${{ github.event.before }}'
head='${{ github.sha }}'
# Zero SHA (branch created / force push) or manual dispatch: fall back
# to this commit's own diff.
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then
base="$head^"
fi
echo "Diffing $base..$head"
mapfile -t candidates < <(
git diff --name-only "$base" "$head" -- resources/profiles \
| sed -nE 's#^resources/profiles/([^/]+)/.*#\1#p; s#^resources/profiles/([^/]+)\.json$#\1#p' \
| sort -u
)
vendors=()
for v in "${candidates[@]:-}"; do
[ -n "$v" ] || continue
json="resources/profiles/$v.json"
# A vendor has a manifest plus either a preset directory or a version
# field; this drops non-vendor files such as blacklist.json.
if [ -f "$json" ] && { [ -d "resources/profiles/$v" ] || jq -e '.version' "$json" >/dev/null 2>&1; }; then
vendors+=("$v")
fi
done
if [ "${#vendors[@]}" -eq 0 ]; then
echo "No changed vendor profiles in this push; nothing to publish."
echo "vendors=" >> "$GITHUB_OUTPUT"
exit 0
fi
# A vendor is eligible only when both the profile directory and its
# sibling bundle JSON are covered by at least one FOLDER_MERGERS
# grant. The account part is intentionally ignored here: this is a
# post-merge safety check, not an authorization check for a command.
grants=()
while IFS= read -r raw_line; do
line="${raw_line#"${raw_line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
[ -n "$line" ] || continue
[[ "$line" == \#* ]] && continue
[[ "$line" == *:* ]] || continue
grant="${line#*:}"
grant="${grant#"${grant%%[![:space:]]*}"}"
grant="${grant%"${grant##*[![:space:]]}"}"
while [[ "$grant" == */ ]]; do grant="${grant%/}"; done
grants+=("$grant")
done <<< "${FOLDER_MERGERS:-}"
is_granted() {
local path="$1"
local grant
for grant in "${grants[@]:-}"; do
if [[ "$path" == "$grant" || "$path" == "$grant/"* ]]; then
return 0
fi
done
return 1
}
unauthorized=()
for v in "${vendors[@]}"; do
if ! is_granted "resources/profiles/$v" || ! is_granted "resources/profiles/$v.json"; then
unauthorized+=("$v")
fi
done
if [ "${#unauthorized[@]}" -ne 0 ]; then
echo "Changed vendor profiles are not covered by FOLDER_MERGERS: ${unauthorized[*]}"
echo "No profile caches will be published for this push."
echo "vendors=" >> "$GITHUB_OUTPUT"
exit 0
fi
printf 'Changed vendors: %s\n' "${vendors[*]}"
echo "vendors=${vendors[*]}" >> "$GITHUB_OUTPUT"
- name: Resolve Orca version
id: orca
if: steps.vendors.outputs.vendors != ''
shell: bash
run: |
set -euo pipefail
raw="$(sed -nE 's/^set\(SoftFever_VERSION "([^"]+)".*/\1/p' version.inc | head -1)"
[ -n "$raw" ] || { echo "::error::could not read SoftFever_VERSION from version.inc"; exit 1; }
orca_ver="${raw%%-*}"
if ! [[ "$orca_ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Orca version '$orca_ver' (from '$raw') is not X.Y.Z"; exit 1
fi
# release_tag is what the desktop client sends as orca_version and what
# OrcaCloud keys R2 on; orca_ver (X.Y.Z) is the asset-name prefix.
echo "release_tag=$raw" >> "$GITHUB_OUTPUT"
echo "orca_ver=$orca_ver" >> "$GITHUB_OUTPUT"
echo "Orca version: release_tag=$raw asset_prefix=$orca_ver"
- name: Validate profile versions
id: pver
if: steps.vendors.outputs.vendors != ''
shell: bash
run: |
set -euo pipefail
: > "$RUNNER_TEMP/pver.tsv"
for v in ${{ steps.vendors.outputs.vendors }}; do
pv="$(jq -r '.version // empty' "resources/profiles/$v.json")"
if ! [[ "$pv" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::vendor $v version '${pv:-<none>}' must be 4 numeric parts (A.B.C.D) for the OTA asset name; fix resources/profiles/$v.json"
exit 1
fi
printf '%s\t%s\n' "$v" "$pv" >> "$RUNNER_TEMP/pver.tsv"
echo "$v -> $pv"
done
- name: Download generate_system_cache
if: steps.vendors.outputs.vendors != ''
shell: bash
env:
# gh (with the default token) rather than an unauthenticated curl: keeps
# working if TOOL_REPO is ever private and avoids anonymous rate limits.
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
gh release download nightly-builds --repo "$TOOL_REPO" \
--pattern "$TOOL_ASSET" --output generate_system_cache --clobber
chmod +x generate_system_cache
- name: Build caches and package assets
id: pkg
if: steps.vendors.outputs.vendors != ''
shell: bash
run: |
set -euo pipefail
# One timestamp for the whole run so a multi-vendor merge groups together.
ts="$(date -u +%Y%m%d%H%M)"
orca_ver='${{ steps.orca.outputs.orca_ver }}'
out="$RUNNER_TEMP/assets"
mkdir -p "$out"
for v in ${{ steps.vendors.outputs.vendors }}; do
./generate_system_cache -p "$GITHUB_WORKSPACE/resources/profiles" -v "$v" -l 2
opc="resources/profiles/$v.opc"
[ -f "$opc" ] || { echo "::error::$opc was not generated"; exit 1; }
pv="$(awk -F'\t' -v v="$v" '$1==v{print $2}' "$RUNNER_TEMP/pver.tsv")"
name="${orca_ver}_${v}_${pv}_${ts}.zip"
( cd resources/profiles && zip -q -j "$out/$name" "$v.opc" )
echo "packaged $name"
done
echo "dir=$out" >> "$GITHUB_OUTPUT"
- name: Mint profiles-repo token
id: token
if: steps.vendors.outputs.vendors != ''
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.PROFILES_APP_ID }}
private-key: ${{ secrets.PROFILES_APP_PRIVATE_KEY }}
owner: ${{ env.PROFILES_OWNER }}
repositories: ${{ env.PROFILES_REPO }}
- name: Publish assets to profiles release
if: steps.vendors.outputs.vendors != ''
shell: bash
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
RELEASE_TAG: ${{ steps.orca.outputs.release_tag }}
ASSET_DIR: ${{ steps.pkg.outputs.dir }}
run: |
set -euo pipefail
repo="$PROFILES_OWNER/$PROFILES_REPO"
if ! gh release view "$RELEASE_TAG" --repo "$repo" >/dev/null 2>&1; then
echo "Creating release $RELEASE_TAG on $repo"
gh release create "$RELEASE_TAG" --repo "$repo" \
--title "$RELEASE_TAG" --notes "Profile cache assets for Orca $RELEASE_TAG." \
--latest=false
fi
# Asset names are timestamp-unique; a clash means a bug, so don't --clobber.
gh release upload "$RELEASE_TAG" --repo "$repo" "$ASSET_DIR"/*.zip
{
echo "### Published to \`$repo\` release \`$RELEASE_TAG\`"
for f in "$ASSET_DIR"/*.zip; do echo "- \`$(basename "$f")\`"; done
} >> "$GITHUB_STEP_SUMMARY"
-377
View File
@@ -1,377 +0,0 @@
name: PR Label Bot
on:
pull_request_target:
types:
- opened
- reopened
issue_comment:
types:
- created
permissions:
contents: read
pull-requests: write
issues: write
jobs:
request-label:
if: github.event_name == 'pull_request_target' && github.event.pull_request.author_association != 'COLLABORATOR' && github.event.pull_request.author_association != 'OWNER' && github.event.pull_request.author_association != 'MEMBER'
permissions:
contents: read
pull-requests: write
issues: write
runs-on: ubuntu-latest
steps:
- name: Ask PR author for label
uses: actions/github-script@v9
with:
script: |
function isPermissionDenied(error) {
return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || '');
}
const allowedLabels = [
// kind of change
'crash',
'bug-fix',
'enhancement',
'QoL',
'optimization',
// area
'UI/UX',
'profile',
'Localization',
// infrastructure
'build',
'test',
'dependencies',
'documentation'
];
const pr = context.payload.pull_request;
const labelsList = `${allowedLabels
.slice(0, -1)
.map((label) => `\`${label}\``)
.join(', ')} and \`${allowedLabels[allowedLabels.length - 1]}\`.`;
const examplesText = [
'```',
'/bot add-label bug-fix',
'```',
'```',
'/bot add-label bug-fix, UI/UX',
'```',
'```',
'/bot remove-label bug-fix, UI/UX',
'```'
].join('\n');
try {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body:
`Hi @${pr.user.login}, you can manage the labels for this PR by \`/bot add-label\` and \`/bot remove-label\`\n\n` +
`Allowed labels are:\n${labelsList}\n\n` +
`Examples:\n${examplesText}`
});
} catch (error) {
if (isPermissionDenied(error)) {
core.warning(
'Skipping PR comment because token cannot write. Enable Actions write permissions, ' +
'or run with a token that has issues:write and pull_requests:write.'
);
return;
}
throw error;
}
localization-pr:
if: github.event_name == 'pull_request_target'
permissions:
contents: read
pull-requests: write
issues: write
runs-on: ubuntu-latest
steps:
- name: Auto-label and remind about the localization glossary
uses: actions/github-script@v9
with:
script: |
function isPermissionDenied(error) {
return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || '');
}
const pr = context.payload.pull_request;
// List changed files once (mirrors the `localization/**` paths filter in check_locale.yml)
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
per_page: 100
});
const touchesLocalization = files.some((file) => file.filename.startsWith('localization/'));
const onlyPoFiles = files.length > 0 && files.every((file) => file.filename.endsWith('.po'));
// If the PR changes only .po files, automatically apply the Localization label
if (onlyPoFiles) {
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels: ['Localization']
});
core.info('Applied Localization label (PR changes only .po files).');
} catch (error) {
if (isPermissionDenied(error)) {
core.warning('Cannot add Localization label because token cannot write.');
} else {
throw error;
}
}
}
if (!touchesLocalization) {
core.info('No localization changes detected; skipping glossary reminder.');
return;
}
// Avoid posting the reminder twice (e.g. on reopen)
const marker = '<!-- localization-glossary-reminder -->';
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
per_page: 100
});
if (comments.some((comment) => (comment.body || '').includes(marker))) {
core.info('Glossary reminder already present; skipping.');
return;
}
try {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body:
`${marker}\n` +
`Hi @${pr.user.login}, this PR changes translations (\`localization/**\`).\n\n` +
`Please make sure recurring terms follow the [Localization glossary](https://www.orcaslicer.com/wiki/localization_glossary), ` +
`so the same English term is always rendered the same way within a language and terms that must stay in English ` +
`(brand/product names, acronyms, file formats, G-code, macros/variables) are not translated.`
});
} catch (error) {
if (isPermissionDenied(error)) {
core.warning('Skipping glossary reminder because token cannot write comments.');
return;
}
throw error;
}
apply-label:
if: github.event_name == 'issue_comment'
permissions:
contents: read
pull-requests: write
issues: write
runs-on: ubuntu-latest
steps:
- name: Apply label command from PR author
uses: actions/github-script@v9
with:
script: |
function isPermissionDenied(error) {
return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || '');
}
const allowedLabels = [
// kind of change
'crash',
'bug-fix',
'enhancement',
'QoL',
'optimization',
// area
'UI/UX',
'profile',
'Localization',
// infrastructure
'build',
'test',
'dependencies',
'documentation'
];
const issue = context.payload.issue;
if (!issue.pull_request) {
core.info('Ignoring comment that is not on a pull request.');
return;
}
const body = (context.payload.comment.body || '').trim();
const commandLine = body
.split('\n')
.map((line) => line.trim())
.find((line) => /^\/bot\s+(add-label|remove-label)(?:\s*:\s*|\s+)/i.test(line));
if (!commandLine) {
core.info('No /bot add-label or /bot remove-label command found.');
return;
}
const commandMatch = commandLine.match(/^\/bot\s+(add-label|remove-label)(?:\s*:\s*|\s+)(.+)\s*$/i);
if (!commandMatch) {
core.info('Label command format is invalid.');
return;
}
const action = commandMatch[1].toLowerCase() === 'add-label' ? 'add' : 'remove';
let labelsExpr = (commandMatch[2] || '').trim();
if (!labelsExpr) {
core.info('Label command is missing label name.');
return;
}
if (labelsExpr.startsWith('[') && labelsExpr.endsWith(']')) {
labelsExpr = labelsExpr.slice(1, -1).trim();
}
const requestedRawLabels = labelsExpr
.split(',')
.map((part) => part.trim())
.filter(Boolean);
if (!requestedRawLabels.length) {
core.info('No labels were provided in the command.');
return;
}
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: issue.number
});
const commenter = context.payload.comment.user.login;
if (commenter !== pr.user.login) {
core.info('Ignoring command because commenter is not the PR author.');
return;
}
const labelsByLower = new Map(
allowedLabels.map((label) => [label.toLowerCase(), label])
);
const resolvedLabels = [];
const invalidLabels = [];
for (const rawLabel of requestedRawLabels) {
const resolved = labelsByLower.get(rawLabel.toLowerCase());
if (resolved) {
resolvedLabels.push(resolved);
} else {
invalidLabels.push(rawLabel);
}
}
const uniqueRequestedLabels = [...new Set(resolvedLabels)];
if (action === 'add' && uniqueRequestedLabels.length) {
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: uniqueRequestedLabels
});
} catch (error) {
if (isPermissionDenied(error)) {
core.warning(
'Cannot add labels because token cannot write. Enable Actions write permissions, ' +
'or run with a token that has issues:write and pull_requests:write.'
);
return;
}
throw error;
}
core.info(`Added labels: ${uniqueRequestedLabels.join(', ')}`);
}
if (action === 'remove' && uniqueRequestedLabels.length) {
let removedCount = 0;
try {
for (const label of uniqueRequestedLabels) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
name: label
});
removedCount += 1;
} catch (error) {
if (isPermissionDenied(error)) {
core.warning(
'Cannot remove labels because token cannot write. Enable Actions write permissions, ' +
'or run with a token that has issues:write and pull_requests:write.'
);
return;
}
if (error.status === 404) {
core.info(`Label is not currently applied: ${label}`);
continue;
}
throw error;
}
}
core.info(`Removed labels count: ${removedCount}`);
} catch (error) {
throw error;
}
}
if (!uniqueRequestedLabels.length) {
core.info('No valid labels were provided in the command.');
}
if (invalidLabels.length) {
const allowedText = allowedLabels.map((label) => `\`${label}\``).join(', ');
const invalidText = invalidLabels.map((label) => `\`${label}\``).join(', ');
const validText = uniqueRequestedLabels.length
? `\n\nProcessed valid label(s): ${uniqueRequestedLabels.map((label) => `\`${label}\``).join(', ')}`
: '';
try {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body:
`@${commenter} invalid label(s): ${invalidText}.${validText}\n\n` +
`Allowed labels: ${allowedText}\n\n` +
`Use:\n` +
`- \`/bot add-label label\`\n` +
`- \`/bot remove-label label\`\n` +
`- \`/bot add-label label1, label2\``
});
} catch (error) {
if (isPermissionDenied(error)) {
core.warning('Cannot post invalid-label feedback because token cannot write comments.');
return;
}
throw error;
}
}
const processedLabelsText = uniqueRequestedLabels.length ? uniqueRequestedLabels.join(', ') : '(none)';
core.info(`Processed label command: ${action} ${processedLabelsText}`);
-911
View File
@@ -1,911 +0,0 @@
name: PR Merge Bot
# Merges a pull request on request from a delegated vendor profile maintainer.
# The merge is performed by this workflow's GITHUB_TOKEN, so a delegate needs no
# repository access.
#
# Commands, posted as a comment on the PR:
# /bot merge squash-merge the PR
# /bot merge --dry-run report the verdict without merging
#
# Merges only when the commenter holds a grant covering every changed path, the
# PR targets main or release/*, and CI is green on the head commit. Otherwise it
# comments naming the files that fell outside the grant.
#
# When a PR touching resources/profiles/** is opened, two labels are applied
# independently of the merge command:
# profile every changed path is inside resources/profiles/
# orca profile partner the PR author holds a grant covering every changed
# path, plus a one-time comment explaining /bot merge
# Neither label changes what the merge command checks.
#
# Grants come from the FOLDER_MERGERS variable in the `merge-delegation`
# environment: one per line, `account: path`, `#` comments and blank lines
# allowed. Paths may contain spaces. A vendor takes two grants, the folder and
# its sibling bundle JSON:
#
# # Acme profiles
# vendor-maintainer: resources/profiles/Acme/
# vendor-maintainer: resources/profiles/Acme.json
#
# Edit the grant list (environment scope, so admin only):
# gh variable set FOLDER_MERGERS --env merge-delegation --body "$(cat folder-mergers.txt)"
# gh variable get FOLDER_MERGERS --env merge-delegation
#
# Stop all merging without touching this file:
# gh variable set MERGE_BOT_DRY_RUN --body true
on:
issue_comment:
types:
- created
# Labels profile PRs on open, without waiting for a /bot merge command.
pull_request_target:
types:
- opened
paths:
- 'resources/profiles/**'
# One merge attempt per PR at a time, so two quick comments cannot race.
# Labels run under their own group, so a queued label run is not replaced by
# a merge run for the same PR.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.issue.number || github.event.pull_request.number }}
cancel-in-progress: false
jobs:
merge:
# Skips the job unless a PR comment mentions the command.
if: >-
github.repository == 'OrcaSlicer/OrcaSlicer'
&& github.event_name == 'issue_comment'
&& github.event.issue.pull_request != null
&& contains(github.event.comment.body, '/bot merge')
permissions:
contents: write # pulls.merge
pull-requests: write # pulls.merge
issues: write # feedback comment + reactions
actions: write # re-dispatch build_all.yml after the merge
runs-on: ubuntu-latest
timeout-minutes: 10
# Supplies FOLDER_MERGERS. Must carry no protection rules, or every
# delegated merge and partner label run would wait for a human reviewer.
environment: merge-delegation
steps:
- name: Merge PR on behalf of a folder delegate
uses: actions/github-script@v9
env:
# Read as env vars, never interpolated into the script body.
FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }}
MERGE_BOT_DRY_RUN: ${{ vars.MERGE_BOT_DRY_RUN }}
with:
script: |
function isPermissionDenied(error) {
return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || '');
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const MARKER = '<!-- pr-merge-bot -->';
// No grant may reach outside this root.
const DELEGATABLE_ROOT = 'resources/profiles/';
const ALLOWED_BASE_BRANCH = /^(?:main|release\/.+)$/;
const MERGE_METHOD = 'squash';
const REQUIRED_CHECK = 'Check profiles'; // job name in check_profiles.yml
const LISTFILES_CAP = 3000;
const MAX_REPORTED_FILES = 12;
const MERGEABLE_ATTEMPTS = 5;
const MERGEABLE_DELAY_MS = 2000;
const OK_CONCLUSIONS = new Set(['success', 'neutral', 'skipped']);
const REGULAR_FILE_MODES = new Set(['100644', '100755']);
// Paths refused whatever the grants say. Checked before grants, so
// delegating a new root means removing it from this list too.
const DENIED_PATTERNS = [
/^\.github\//,
/(^|\/)\.git(attributes|modules|ignore|config)$/,
/^(?:src|deps|deps_src|tests|tools|cmake|sandboxes|scripts|docs?|localization|bbl)\//,
/(^|\/)cmakelists\.txt$/,
/\.cmake$/,
/^build_[^/]*\.(?:sh|bat)$/,
/^version\.inc$/,
// Executables, including those inside the delegatable root.
/\.(?:sh|bash|bat|cmd|ps1|py|js|mjs|cjs|ts|rb|pl|php)$/
];
function parseGrants(raw) {
// GitHub login: 1-39 chars, alphanumerics with single interior hyphens.
const loginPattern = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
const grantsByLogin = new Map();
const problems = [];
(raw || '').split(/\r?\n/).forEach((rawLine, index) => {
const line = rawLine.trim();
if (!line || line.startsWith('#')) {
return;
}
// Splits on the first colon only, so paths may contain ':' and spaces.
const separator = line.indexOf(':');
if (separator === -1) {
problems.push(`line ${index + 1}: expected \`account: path\``);
return;
}
const login = line.slice(0, separator).trim().replace(/^@/, '');
const path = line.slice(separator + 1).trim().replace(/\/+$/, '');
if (!loginPattern.test(login)) {
problems.push(`line ${index + 1}: \`${login}\` is not a valid GitHub account name`);
return;
}
if (/[\\*?\u0000-\u001f\u007f]/.test(path) || path.split('/').includes('..') || path.includes('//')) {
problems.push(`line ${index + 1}: invalid path (no globs, \`..\`, \`//\`, backslashes or control characters)`);
return;
}
// Rejects anything outside the root, and the bare root itself.
if (!path.startsWith(DELEGATABLE_ROOT) || path.length <= DELEGATABLE_ROOT.length) {
problems.push(`line ${index + 1}: \`${path}\` is not inside \`${DELEGATABLE_ROOT}\``);
return;
}
const key = login.toLowerCase();
grantsByLogin.set(key, (grantsByLogin.get(key) || []).concat(path));
});
return { grantsByLogin, problems };
}
function isDenied(path) {
if (/[\\\u0000-\u001f\u007f]/.test(path) || path.startsWith('/') || path.split('/').includes('..')) {
return true;
}
const normalized = path.normalize('NFKC').toLowerCase();
return DENIED_PATTERNS.some((pattern) => pattern.test(normalized));
}
// Byte-exact match on directory boundaries, so a grant of
// `.../Acme` covers neither `.../Acme Labs/x.json` nor `.../Acme.json`.
function isGranted(path, grants) {
return grants.some((grant) => path === grant || path.startsWith(`${grant}/`));
}
// Both endpoints of a rename; both must satisfy the grant.
function pathsFor(file) {
return [file.filename, file.previous_filename].filter(Boolean);
}
function formatList(items) {
const unique = [...new Set(items)];
const shown = unique.slice(0, MAX_REPORTED_FILES).map((item) => `- \`${item}\``);
if (unique.length > MAX_REPORTED_FILES) {
shown.push(`- …and ${unique.length - MAX_REPORTED_FILES} more`);
}
return shown.join('\n');
}
const { owner, repo } = context.repo;
const issue = context.payload.issue;
const comment = context.payload.comment;
if (!issue.pull_request) {
core.info('Ignoring comment that is not on a pull request.');
return;
}
// Ignores a comment whose sender is not its author.
if (context.payload.action !== 'created' || context.payload.sender.login !== comment.user.login) {
core.warning('Ignoring comment whose sender does not match its author.');
return;
}
if (comment.user.type !== 'User') {
core.info('Ignoring bot-authored command.');
return;
}
const commandLine = (comment.body || '')
.split('\n')
.map((line) => line.trim())
.find((line) => /^\/bot\s+merge\b/i.test(line));
if (!commandLine) {
core.info('No /bot merge command found.');
return;
}
const commenter = comment.user.login;
const { grantsByLogin, problems } = parseGrants(process.env.FOLDER_MERGERS);
const grants = grantsByLogin.get(commenter.toLowerCase()) || [];
for (const problem of problems) {
core.warning(`FOLDER_MERGERS ${problem}`);
}
// Says nothing to accounts with no grant, so it cannot be used to spam.
if (!grants.length) {
core.info(`Ignoring /bot merge from @${commenter}: not listed in FOLDER_MERGERS.`);
return;
}
// Warns instead of failing when the token cannot post feedback.
async function bestEffort(call, warning) {
try {
await call();
} catch (error) {
if (isPermissionDenied(error)) {
core.warning(warning);
return;
}
throw error;
}
}
const react = (content) => bestEffort(
() => github.rest.reactions.createForIssueComment({ owner, repo, comment_id: comment.id, content }),
`Cannot add the "${content}" reaction because the token cannot write.`);
const say = (body) => bestEffort(
() => github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: `${MARKER}\n${body}` }),
'Cannot post a comment because the token cannot write comments.');
// Declines the command: warns in the log, reacts, explains on the PR.
async function refuse(reason) {
const configNote = problems.length
? `\n\n\`FOLDER_MERGERS\` also has problems a maintainer needs to fix:\n${problems.map((problem) => `- ${problem}`).join('\n')}`
: '';
const grantsNote = `\n\n<details><summary>Your current grants</summary>\n\n${formatList(grants)}\n\n</details>`;
core.warning(`Refused /bot merge from @${commenter}: ${reason}`);
await react('-1');
await say(`@${commenter} I can't merge this PR: ${reason}${configNote}${grantsNote}`);
}
await react('eyes');
const args = (commandLine.match(/^\/bot\s+merge\s*(.*)$/i)[1] || '').trim().split(/\s+/).filter(Boolean);
const unknownArgs = args.filter((arg) => arg.toLowerCase() !== '--dry-run');
const dryRun = String(process.env.MERGE_BOT_DRY_RUN || '').toLowerCase() === 'true'
|| unknownArgs.length !== args.length;
if (unknownArgs.length) {
return refuse(
`I don't understand ${unknownArgs.map((arg) => `\`${arg}\``).join(', ')}. ` +
'Usage: `/bot merge` or `/bot merge --dry-run`.'
);
}
// Refuses everything while the grant list is malformed.
if (problems.length) {
return refuse(
'the `FOLDER_MERGERS` grant list has malformed lines, so I refuse every merge until it is fixed.'
);
}
let { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: issue.number
});
if (pr.merged) {
return refuse('it is already merged.');
}
if (pr.state !== 'open') {
return refuse(`its state is \`${pr.state}\`, not \`open\`.`);
}
if (pr.draft) {
return refuse('it is still a draft. Mark it ready for review first.');
}
if (!ALLOWED_BASE_BRANCH.test(pr.base.ref)) {
return refuse(`it targets \`${pr.base.ref}\`. Delegated merges are only allowed into \`main\` and \`release/*\`.`);
}
// ---- folder scope ----
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: pr.number,
per_page: 100
});
if (!files.length) {
return refuse('it changes no files, so there is nothing to verify or merge.');
}
// Refuses when the file list is truncated or disagrees with the PR.
if (files.length >= LISTFILES_CAP || files.length !== pr.changed_files) {
return refuse(
`it reports ${pr.changed_files} changed files but the API listed ${files.length}, ` +
'so the file list is truncated and I cannot verify the folder scope. A maintainer must merge this one.'
);
}
const deniedFiles = [];
const outsideFiles = [];
for (const file of files) {
for (const path of pathsFor(file)) {
if (isDenied(path)) {
deniedFiles.push(path);
} else if (!isGranted(path, grants)) {
outsideFiles.push(path);
}
}
}
if (deniedFiles.length) {
core.error(`@${commenter} attempted a delegated merge touching protected paths: ${deniedFiles.join(', ')}`);
return refuse(
'it touches paths that are never delegatable, whatever the grants say ' +
`(CI, build, scripts or executable files):\n\n${formatList(deniedFiles)}\n\nA maintainer should look at this before it goes any further.`
);
}
if (outsideFiles.length) {
return refuse(
`${outsideFiles.length} changed path(s) fall outside your grants:\n\n${formatList(outsideFiles)}\n\n` +
'A vendor needs both grants: `resources/profiles/<Vendor>/` **and** `resources/profiles/<Vendor>.json`.'
);
}
// ---- file modes: rejects symlinks and submodules ----
// Fetches the delegatable subtree only; listFiles does not report modes.
const headSha = pr.head.sha;
const { data: tree } = await github.rest.git.getTree({
owner,
repo,
tree_sha: `${headSha}:${DELEGATABLE_ROOT.replace(/\/$/, '')}`,
recursive: 'true'
});
if (tree.truncated) {
return refuse('the git tree is too large to verify file modes. A maintainer must merge this one.');
}
// Entry paths are subtree-relative.
const modesByPath = new Map(tree.tree.map((entry) => [`${DELEGATABLE_ROOT}${entry.path}`, entry.mode]));
const irregularFiles = files
.filter((file) => file.status !== 'removed')
.map((file) => [file.filename, modesByPath.get(file.filename)])
.filter(([, mode]) => !REGULAR_FILE_MODES.has(mode))
.map(([path, mode]) => `${path} (mode ${mode || 'missing'})`);
if (irregularFiles.length) {
core.error(`@${commenter} attempted a delegated merge with non-regular files: ${irregularFiles.join(', ')}`);
return refuse(
`it adds symlinks, submodules or files I cannot verify:\n\n${formatList(irregularFiles)}\n\nA maintainer should look at this before it goes any further.`
);
}
// ---- mergeability: waits for GitHub to compute it ----
for (let attempt = 0; pr.mergeable === null && attempt < MERGEABLE_ATTEMPTS; attempt += 1) {
core.info(`Mergeability not computed yet; retrying in ${MERGEABLE_DELAY_MS}ms.`);
await sleep(MERGEABLE_DELAY_MS);
({ data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: pr.number
}));
}
if (pr.mergeable === null) {
return refuse('GitHub is still working out whether it can be merged. Try `/bot merge` again in a minute.');
}
if (!pr.mergeable) {
return refuse(`it is not mergeable (\`${pr.mergeable_state}\`) - most likely a conflict with \`${pr.base.ref}\`.`);
}
// ---- CI on the head commit ----
const checkRuns = await github.paginate(github.rest.checks.listForRef, {
owner,
repo,
ref: headSha,
filter: 'latest',
per_page: 100
});
const pendingChecks = checkRuns.filter((run) => run.status !== 'completed');
const failedChecks = checkRuns.filter((run) => run.status === 'completed' && !OK_CONCLUSIONS.has(run.conclusion));
if (pendingChecks.length) {
return refuse(
`${pendingChecks.length} check(s) are still running on \`${headSha.slice(0, 7)}\`:\n\n` +
`${formatList(pendingChecks.map((run) => run.name))}\n\nRe-run \`/bot merge\` once they finish.`
);
}
if (failedChecks.length) {
return refuse(
`${failedChecks.length} check(s) are not green on \`${headSha.slice(0, 7)}\`:\n\n` +
formatList(failedChecks.map((run) => `${run.name} (${run.conclusion})`))
);
}
const { data: combined } = await github.rest.repos.getCombinedStatusForRef({
owner,
repo,
ref: headSha
});
// total_count 0 only means there are no legacy statuses.
if (combined.total_count > 0 && combined.state !== 'success') {
return refuse(
`the combined commit status on \`${headSha.slice(0, 7)}\` is \`${combined.state}\`:\n\n` +
formatList(combined.statuses.filter((status) => status.state !== 'success')
.map((status) => `${status.context} (${status.state})`))
);
}
// Requires the check to have actually run, not merely to have not failed.
const requiredCheck = checkRuns.find((run) =>
run.name === REQUIRED_CHECK &&
run.app && run.app.slug === 'github-actions' &&
run.status === 'completed' && OK_CONCLUSIONS.has(run.conclusion));
if (!requiredCheck) {
return refuse(
`the \`${REQUIRED_CHECK}\` check has not succeeded on \`${headSha.slice(0, 7)}\`. ` +
'If it never ran, a maintainer needs to approve the workflow run first.'
);
}
const scopeSummary = `${files.length} file(s), all within:\n${formatList(grants)}`;
if (dryRun) {
core.info('Dry run: every gate passed, not merging.');
await react('+1');
await say(
`@${commenter} **dry run** - this PR passes every gate and I *would* squash-merge it ` +
`at \`${headSha.slice(0, 7)}\`.\n\nVerified scope: ${scopeSummary}`
);
return;
}
// ---- re-validate, then merge ----
// An unchanged head SHA means the verified file list still holds.
const { data: fresh } = await github.rest.pulls.get({
owner,
repo,
pull_number: pr.number
});
if (fresh.head.sha !== headSha || fresh.base.ref !== pr.base.ref || fresh.state !== 'open' || fresh.merged || fresh.draft) {
return refuse('it changed while I was checking it. Nothing was merged - re-run `/bot merge`.');
}
let merged;
try {
// Pinned to the verified head: a moved head fails with 409.
({ data: merged } = await github.rest.pulls.merge({
owner,
repo,
pull_number: pr.number,
sha: headSha,
merge_method: MERGE_METHOD,
commit_title: `${pr.title} (#${pr.number})`,
commit_message:
`Merged by /bot merge on behalf of @${commenter} (id ${comment.user.id}).\n` +
`Grants: ${grants.join(', ')}\nHead: ${headSha}\n`
}));
} catch (error) {
const hint = {
403: 'the workflow token cannot write to the repository.',
405: 'GitHub refused the merge - branch protection, a required review or check, a newly added CODEOWNERS file, or squash merging being disabled.',
409: `the head commit moved after I verified it (was \`${headSha.slice(0, 7)}\`).`,
422: 'GitHub rejected the merge as invalid.'
}[error.status];
if (!hint) {
throw error;
}
await refuse(`${hint}\n\n> ${error.message}\n\nNothing was merged.`);
core.setFailed(`Delegated merge failed: ${error.status} ${error.message}`);
return;
}
core.info(`Merged #${pr.number} as ${merged.sha}.`);
await react('rocket');
await say(
`@${commenter} squash-merged into \`${pr.base.ref}\` as ${merged.sha}.\n\nVerified scope: ${scopeSummary}`
);
// ---- re-kick the build ----
// A GITHUB_TOKEN merge fires no push event, so build_all.yml would
// otherwise never see these files.
try {
await github.rest.actions.createWorkflowDispatch({
owner,
repo,
workflow_id: 'build_all.yml',
ref: pr.base.ref
});
core.info(`Dispatched build_all.yml on ${pr.base.ref}.`);
} catch (error) {
core.warning(`Merged successfully, but dispatching build_all.yml failed: ${error.message}`);
}
label-profile:
# Independent of the merge rules: any PR that changes only files inside
# resources/profiles/ is labeled `profile`.
if: >-
github.repository == 'OrcaSlicer/OrcaSlicer'
&& github.event_name == 'pull_request_target'
permissions:
contents: read
pull-requests: read
issues: write
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Label profile-only PRs
uses: actions/github-script@v9
with:
script: |
function isPermissionDenied(error) {
return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || '');
}
const PROFILE_ROOT = 'resources/profiles/';
const LABEL = 'profile';
const LISTFILES_CAP = 3000;
const ATTEMPTS = 3;
function profileOnlyProblem(pr, files) {
if (!files.length) {
return 'PR changes no files; not labeling.';
}
// A truncated list, or a count that disagrees with the PR, cannot
// prove "only profile files".
if (files.length >= LISTFILES_CAP || files.length !== pr.changed_files) {
return `PR reports ${pr.changed_files} changed files but the API listed ${files.length}; not labeling.`;
}
// Both endpoints of a rename count, so a move out of the profile
// root is not mistaken for a profile-only change.
const paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean));
const outside = paths.filter((path) => !path.startsWith(PROFILE_ROOT));
if (outside.length) {
return `${outside.length} changed path(s) fall outside ${PROFILE_ROOT}; not labeling.`;
}
return null;
}
const { owner, repo } = context.repo;
const number = context.payload.pull_request.number;
// The event payload is frozen at `opened`; listFiles is not. Read
// fresh PR metadata and retry if either side of the diff changes.
for (let attempt = 0; attempt < ATTEMPTS; attempt += 1) {
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: number });
if (pr.state !== 'open') {
core.info(`PR is ${pr.state}; not labeling.`);
return;
}
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: pr.number,
per_page: 100
});
const problem = profileOnlyProblem(pr, files);
const { data: after } = await github.rest.pulls.get({ owner, repo, pull_number: number });
if (
after.state !== 'open' ||
after.head.sha !== pr.head.sha ||
after.base.ref !== pr.base.ref ||
after.base.sha !== pr.base.sha
) {
core.info('PR changed while listing files; retrying.');
continue;
}
if (problem) {
core.info(problem);
return;
}
try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pr.number,
labels: [LABEL]
});
core.info(`Applied the "${LABEL}" label.`);
} catch (error) {
if (isPermissionDenied(error)) {
core.warning(`Cannot add the "${LABEL}" label because the token cannot write.`);
return;
}
throw error;
}
return;
}
core.warning('PR kept changing during verification; not labeling.');
label-profile-partner:
# Labels a profile PR whose author holds a grant covering every changed
# path, and explains the /bot merge command to them once.
if: >-
github.repository == 'OrcaSlicer/OrcaSlicer'
&& github.event_name == 'pull_request_target'
permissions:
contents: read # delegatable subtree, for file modes
pull-requests: read
issues: write # label + comment
runs-on: ubuntu-latest
timeout-minutes: 10
# Supplies FOLDER_MERGERS. Must carry no protection rules, or every
# qualifying PR open would wait for a human reviewer.
environment: merge-delegation
steps:
- name: Label profile PRs from delegated maintainers
uses: actions/github-script@v9
env:
# Read as an env var, never interpolated into the script body.
FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }}
with:
script: |
function isPermissionDenied(error) {
return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || '');
}
// Never prints the grant list: this job posts public comments and
// its logs are public too.
async function bestEffort(call, warning) {
try {
await call();
} catch (error) {
if (isPermissionDenied(error)) {
core.warning(warning);
return;
}
throw error;
}
}
const MARKER = '<!-- profile-partner-bot -->';
const LABEL = 'orca profile partner';
const ATTEMPTS = 3;
// ---- scope rules, mirrored from the merge job above ----
// Change both together: these decide whether a delegate could merge.
const DELEGATABLE_ROOT = 'resources/profiles/';
const ALLOWED_BASE_BRANCH = /^(?:main|release\/.+)$/;
const LISTFILES_CAP = 3000;
const REGULAR_FILE_MODES = new Set(['100644', '100755']);
const DENIED_PATTERNS = [
/^\.github\//,
/(^|\/)\.git(attributes|modules|ignore|config)$/,
/^(?:src|deps|deps_src|tests|tools|cmake|sandboxes|scripts|docs?|localization|bbl)\//,
/(^|\/)cmakelists\.txt$/,
/\.cmake$/,
/^build_[^/]*\.(?:sh|bat)$/,
/^version\.inc$/,
// Executables, including those inside the delegatable root.
/\.(?:sh|bash|bat|cmd|ps1|py|js|mjs|cjs|ts|rb|pl|php)$/
];
function parseGrants(raw) {
// GitHub login: 1-39 chars, alphanumerics with single interior hyphens.
const loginPattern = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
const grantsByLogin = new Map();
const problems = [];
(raw || '').split(/\r?\n/).forEach((rawLine, index) => {
const line = rawLine.trim();
if (!line || line.startsWith('#')) {
return;
}
// Splits on the first colon only, so paths may contain ':' and spaces.
const separator = line.indexOf(':');
if (separator === -1) {
problems.push(`line ${index + 1}: expected \`account: path\``);
return;
}
const login = line.slice(0, separator).trim().replace(/^@/, '');
const path = line.slice(separator + 1).trim().replace(/\/+$/, '');
if (!loginPattern.test(login)) {
problems.push(`line ${index + 1}: \`${login}\` is not a valid GitHub account name`);
return;
}
if (/[\\*?\u0000-\u001f\u007f]/.test(path) || path.split('/').includes('..') || path.includes('//')) {
problems.push(`line ${index + 1}: invalid path (no globs, \`..\`, \`//\`, backslashes or control characters)`);
return;
}
// Rejects anything outside the root, and the bare root itself.
if (!path.startsWith(DELEGATABLE_ROOT) || path.length <= DELEGATABLE_ROOT.length) {
problems.push(`line ${index + 1}: \`${path}\` is not inside \`${DELEGATABLE_ROOT}\``);
return;
}
const key = login.toLowerCase();
grantsByLogin.set(key, (grantsByLogin.get(key) || []).concat(path));
});
return { grantsByLogin, problems };
}
function isDenied(path) {
if (/[\\\u0000-\u001f\u007f]/.test(path) || path.startsWith('/') || path.split('/').includes('..')) {
return true;
}
const normalized = path.normalize('NFKC').toLowerCase();
return DENIED_PATTERNS.some((pattern) => pattern.test(normalized));
}
// Byte-exact match on directory boundaries, so a grant of
// `.../Acme` covers neither `.../Acme Labs/x.json` nor `.../Acme.json`.
function isGranted(path, grants) {
return grants.some((grant) => path === grant || path.startsWith(`${grant}/`));
}
// Both endpoints of a rename; both must satisfy the grant.
function pathsFor(file) {
return [file.filename, file.previous_filename].filter(Boolean);
}
// ---- end mirrored rules ----
function scopeProblem(pr, files, grants) {
if (!files.length) {
return 'PR changes no files; not labeling.';
}
if (files.length >= LISTFILES_CAP || files.length !== pr.changed_files) {
return `PR reports ${pr.changed_files} changed files but the API listed ${files.length}; not labeling.`;
}
let outsideCount = 0;
for (const file of files) {
for (const path of pathsFor(file)) {
if (isDenied(path) || !isGranted(path, grants)) {
outsideCount += 1;
}
}
}
if (outsideCount) {
return `PR has ${outsideCount} path(s) outside @${author}'s grants; not labeling.`;
}
return null;
}
// ---- file modes: rejects symlinks and submodules ----
function modeProblem(files, tree) {
if (tree.truncated) {
return 'The profile tree is too large to verify file modes; not labeling.';
}
const modesByPath = new Map(tree.tree.map((entry) => [`${DELEGATABLE_ROOT}${entry.path}`, entry.mode]));
const hasIrregularFile = files.some((file) =>
file.status !== 'removed' && !REGULAR_FILE_MODES.has(modesByPath.get(file.filename)));
if (hasIrregularFile) {
return 'PR adds symlinks, submodules or files whose modes cannot be verified; not labeling.';
}
return null;
}
const { owner, repo } = context.repo;
const number = context.payload.pull_request.number;
const author = context.payload.pull_request.user.login;
const { grantsByLogin, problems } = parseGrants(process.env.FOLDER_MERGERS);
// Only the count: the malformed lines may name grant holders.
if (problems.length) {
core.warning(`FOLDER_MERGERS has ${problems.length} malformed line(s); not labeling.`);
return;
}
const grants = grantsByLogin.get(author.toLowerCase()) || [];
// Says nothing to accounts with no grant, so it cannot be used to spam.
if (!grants.length) {
core.info(`Ignoring PR from @${author}: not listed in FOLDER_MERGERS.`);
return;
}
// Read current PR metadata for the file list and head tree. Retry
// if either side of the diff changes during verification.
for (let attempt = 0; attempt < ATTEMPTS; attempt += 1) {
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: number });
if (pr.state !== 'open') {
core.info(`PR is ${pr.state}; not labeling.`);
return;
}
if (!ALLOWED_BASE_BRANCH.test(pr.base.ref)) {
core.info(`PR targets "${pr.base.ref}", not main or release/*; not labeling.`);
return;
}
// Checked before listing files, so a PR too large to list is
// rejected in one call.
if (pr.changed_files >= LISTFILES_CAP) {
core.info(`PR changes ${pr.changed_files} files, more than the API can list; not labeling.`);
return;
}
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: pr.number,
per_page: 100
});
const scopeIssue = scopeProblem(pr, files, grants);
let modeIssue = null;
if (!scopeIssue) {
const { data: tree } = await github.rest.git.getTree({
owner,
repo,
tree_sha: `${pr.head.sha}:${DELEGATABLE_ROOT.replace(/\/$/, '')}`,
recursive: 'true'
});
modeIssue = modeProblem(files, tree);
}
const { data: after } = await github.rest.pulls.get({ owner, repo, pull_number: number });
if (
after.state !== 'open' ||
after.head.sha !== pr.head.sha ||
after.base.ref !== pr.base.ref ||
after.base.sha !== pr.base.sha
) {
core.info('PR changed while verifying; retrying.');
continue;
}
const problem = scopeIssue || modeIssue;
if (problem) {
core.info(problem);
return;
}
// ---- label + one-time comment ----
await bestEffort(
() => github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: [LABEL] }),
`Cannot add the "${LABEL}" label because the token cannot write.`);
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: pr.number,
per_page: 100
});
if (comments.some((comment) => (comment.body || '').includes(MARKER))) {
core.info('Partner notice already present; skipping comment.');
return;
}
await bestEffort(
() => github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
body:
`${MARKER}\n` +
`Hi @${author}, this profile PR is covered by your delegated merge grant.\n\n` +
`Once it is ready for review and CI is green, you can merge it yourself:\n\n` +
`- \`/bot merge\` - squash-merge into \`main\` or \`release/*\`\n` +
`- \`/bot merge --dry-run\` - report the verdict without merging\n\n` +
`The bot re-checks the scope, the file modes and the \`Check profiles\` check at merge time.`
}),
'Cannot post the partner notice because the token cannot write comments.');
core.info(`Applied the "${LABEL}" label and posted the /bot merge notice.`);
return;
}
core.warning('PR kept changing during verification; not labeling.');
-131
View File
@@ -1,131 +0,0 @@
name: Publish to draft release
# Manually pulls the platform binaries produced by a "Build all" run and uploads
# them to an existing DRAFT release. Decoupled from the build so you can test a
# build first, then publish exactly that run's artifacts once you're happy.
#
# Trigger: Actions tab -> "Publish to draft release" -> Run workflow.
# Locked to a single person: the first step aborts unless the actor matches the
# `RELEASE_PUBLISHER` repo variable (set to "SoftFever"). To allow someone else,
# change that variable: gh variable set RELEASE_PUBLISHER --body "<login>".
on:
workflow_dispatch:
inputs:
run_id:
description: 'Run ID of the "Build all" workflow to pull binaries from (the number in the Actions run URL)'
required: true
type: string
tag:
description: 'Tag of the draft release to upload to (e.g. v2.4.0-beta)'
required: true
type: string
permissions:
contents: write # upload release assets
actions: read # download artifacts from another run
jobs:
publish:
name: Publish binaries to draft release
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN_ID: ${{ inputs.run_id }}
TAG: ${{ inputs.tag }}
steps:
- name: Restrict to the release publisher
env:
PUBLISHER: ${{ vars.RELEASE_PUBLISHER }}
run: |
if [ -z "$PUBLISHER" ]; then
echo "::error::RELEASE_PUBLISHER repo variable is not set."
exit 1
fi
if [ "${{ github.actor }}" != "$PUBLISHER" ]; then
echo "::error::Only @$PUBLISHER may run this workflow (you are @${{ github.actor }})."
exit 1
fi
echo "Authorized: @${{ github.actor }}"
- name: Verify target is a draft release
run: |
if ! gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json isDraft >/dev/null 2>&1; then
echo "::error::Release '$TAG' not found. Create the draft (with this tag) first."
exit 1
fi
is_draft=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json isDraft --jq '.isDraft')
if [ "$is_draft" != "true" ]; then
echo "::error::Release '$TAG' is published, not a draft. Aborting to avoid touching a live release."
exit 1
fi
- name: Verify source build run
run: |
info=$(gh run view "$RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,conclusion,headBranch)
name=$(echo "$info" | jq -r '.workflowName')
conclusion=$(echo "$info" | jq -r '.conclusion')
branch=$(echo "$info" | jq -r '.headBranch')
echo "Source run #$RUN_ID: '$name' on '$branch' (conclusion: $conclusion)"
if [ "$conclusion" != "success" ]; then
echo "::warning::Source run did not conclude 'success' ($conclusion). Some assets may be missing."
fi
- name: Download release artifacts from build run
run: |
# Windows_V* (not Windows_*) keeps the MSIX Store artifact out: it goes to Partner Center, not GitHub releases.
gh run download "$RUN_ID" --repo "$GITHUB_REPOSITORY" --dir artifacts \
-p 'OrcaSlicer_Windows_V*' \
-p 'OrcaSlicer_Mac_universal_*' \
-p 'OrcaSlicer_Linux_ubuntu_*' \
-p 'OrcaSlicer-Linux-flatpak_*' \
-p 'PDB'
echo "Downloaded artifact folders:"
ls -1 artifacts
- name: Assemble release assets
run: |
set -euo pipefail
mkdir -p upload
# gh run download auto-extracts each artifact into a folder, so the inner
# binaries are already unzipped. Copy the inner binary for each platform.
# -type f is required (some artifact *folders* are named "*.flatpak").
# Windows installers (x64 + arm64): the .exe inside each installer
# artifact, NOT the orca-slicer.exe in the portable app folder. CPack
# now bakes the arch into the filename (…_x64.exe / …_arm64.exe), so
# copy them straight through.
find artifacts -type f -name '*.exe' -path '*OrcaSlicer_Windows_V*' ! -path '*_portable*' -exec cp -v {} upload/ \;
# macOS universal DMG (profile-validator DMG isn't downloaded).
find artifacts -type f -name '*.dmg' -path '*OrcaSlicer_Mac_universal_*' -exec cp -v {} upload/ \;
# Linux AppImage.
find artifacts -type f -name '*.AppImage' -exec cp -v {} upload/ \;
# Flatpak bundles (x86_64 + aarch64).
find artifacts -type f -name '*.flatpak' -exec cp -v {} upload/ \;
# Windows debug symbols (PDB archive, for developers).
find artifacts -type f -name 'Debug_PDB_*.7z' -exec cp -v {} upload/ \;
# Portable Windows builds (x64 + arm64) are unzipped folder artifacts;
# re-zip each to its released filename (these stay .zip on the release).
mapfile -t portable_dirs < <(find artifacts -maxdepth 1 -type d -name 'OrcaSlicer_Windows_*_portable')
if [ ${#portable_dirs[@]} -eq 0 ]; then
echo "::warning::Windows portable artifact not found."
fi
for portable_dir in "${portable_dirs[@]}"; do
( cd "$portable_dir" && zip -qr "$GITHUB_WORKSPACE/upload/$(basename "$portable_dir").zip" . )
echo "Zipped portable -> $(basename "$portable_dir").zip"
done
echo "Assets to upload:"
ls -lh upload
if [ -z "$(ls -A upload)" ]; then
echo "::error::No assets assembled. Check the run_id and that its artifacts haven't expired."
exit 1
fi
- name: Upload assets to draft release
run: |
gh release upload "$TAG" upload/* --repo "$GITHUB_REPOSITORY" --clobber
echo "Uploaded to draft release: $TAG"
gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name'
-44
View File
@@ -1,44 +0,0 @@
on:
push:
paths:
- '**.sh'
- 'scripts/linux.d/*'
pull_request:
paths:
- '**.sh'
- 'scripts/linux.d/*'
schedule:
- cron: '55 7 * * *' # run once a day near midnight US Pacific time
workflow_dispatch: # allows for manual dispatch
name: "Shellcheck"
permissions: {}
jobs:
shellcheck:
name: Shellcheck
runs-on: ubuntu-latest
steps:
- name: Cache shellcheck download
id: cache-shellcheck-v0_11
uses: actions/cache@v6
with:
path: ~/shellcheck
key: ${{ runner.os }}-shellcheck-v0_11
- name: Download shellcheck
if: steps.cache-shellcheck-v0_11.outputs.cache-hit != 'true'
shell: bash
env:
INPUT_VERSION: "v0.11.0"
run: |
curl -L#o ~/sc.tar.xz "https://github.com/koalaman/shellcheck/releases/download/${INPUT_VERSION}/shellcheck-${INPUT_VERSION}.linux.x86_64.tar.xz"
tar -xvf ~/sc.tar.xz -C ~
mv ~/shellcheck-"${INPUT_VERSION}"/shellcheck ~/shellcheck
- uses: actions/checkout@v7
with:
fetch-depth: 1
- name: Shellcheck scripts
run: 'find . -not -name \*.md \( -path ./scripts/linux.d/\* -o -name \*.sh \) -print0 | xargs -0 ~/shellcheck'
-79
View File
@@ -1,79 +0,0 @@
name: Unit Tests
# Download a platform's test artifact, run ctest, and upload the JUnit
# results for aggregation. Called once per arch from build_all.yml.
on:
workflow_call:
inputs:
os:
required: true
type: string
artifact:
description: Test artifact uploaded by the build leg
required: true
type: string
test-dir:
description: Built tests dir; defaults to build/tests, override for arch-separated builds
required: false
type: string
default: build/tests
jobs:
unit_tests:
# Static "Unit Tests"; the per-arch label is the caller's job name, so the
# graph shows e.g. "Windows x64 / Unit Tests".
name: Unit Tests
runs-on: ${{ inputs.os }}
steps:
- name: Checkout
uses: actions/checkout@v7
with:
# Tests reach outside tests/ at runtime: tests/data (TEST_DATA_DIR) and
# resources/profiles (PROFILES_DIR) by baked-in absolute path, plus
# resources/info (nozzle data) via resources_dir() during a real slice.
# Check out all of resources/ so no test hits a missing-file path.
sparse-checkout: |
.github
scripts
tests
resources
- name: Apt-Install Dependencies
if: runner.os == 'Linux' && !vars.SELF_HOSTED
uses: ./.github/actions/apt-install-deps
- name: Restore Test Artifact
uses: actions/download-artifact@v8
with:
name: ${{ inputs.artifact }}
- uses: lukka/get-cmake@latest
with:
cmakeVersion: "~4.3.0" # use most recent 4.3.x version
useLocalCache: true
useCloudCache: true
- name: Unpackage and Run Unit Tests
timeout-minutes: 20
shell: bash
run: |
tar -xvf build_tests.tar
# Every platform builds with a multi-config generator (build_linux.sh uses Ninja
# Multi-Config), so ctest needs the config: without it, plain add_test() tests
# lose their labels and report "Not Run".
scripts/run_unit_tests.sh "${{ inputs.test-dir }}" Release
- name: Upload Test Logs
if: ${{ failure() }}
uses: actions/upload-artifact@v7
with:
name: unit-test-logs-${{ inputs.artifact }}
path: ${{ inputs.test-dir }}/**/*.log
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-${{ inputs.artifact }}
path: ctest_results.xml
retention-days: 5
if-no-files-found: warn
- name: Delete Test Artifact
if: success()
uses: geekyeggo/delete-artifact@v6
with:
name: ${{ inputs.artifact }}
-67
View File
@@ -1,67 +0,0 @@
name: Flatpak Unit Tests
# Run the flatpak build's test asset inside the sandbox, once per arch. The
# GNOME SDK's _GLIBCXX_ASSERTIONS gives a bounds-checked STL that catches
# out-of-bounds reads no other test leg does.
on:
workflow_call:
inputs:
os:
required: true
type: string
artifact:
description: Test asset uploaded by the flatpak build leg
required: true
type: string
jobs:
unit_tests_flatpak:
name: Flatpak Unit Tests
runs-on: ${{ inputs.os }}
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-50
options: --privileged
steps:
- name: Restore test asset
uses: actions/download-artifact@v8
with:
name: ${{ inputs.artifact }}
- name: Run unit tests (bounds-checked sandbox)
timeout-minutes: 20
shell: bash
run: |
tar -xf flatpak-test-asset.tar
# Recreate the stable module symlink so /run/build/OrcaSlicer resolves.
d=$(ls -d .flatpak-builder/build/OrcaSlicer-* | tail -1)
ln -sfn "$(basename "$d")" .flatpak-builder/build/OrcaSlicer
# The runtime + SDK + the llvm extension the app metadata references,
# which `flatpak build` mounts; best-effort, the image may have them.
flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install --user -y --noninteractive flathub \
org.gnome.Platform//50 org.gnome.Sdk//50 org.freedesktop.Sdk.Extension.llvm21//25.08 || true
# `flatpak build` uses bwrap (no rofiles-fuse, which this container
# rejects); bind-mount the build tree so the baked TEST_DATA_DIR resolves.
flatpak build --die-with-parent --share=network \
--bind-mount=/run/build="$PWD/.flatpak-builder/build" \
flatpak_app \
bash -c 'cd /run/build/OrcaSlicer && scripts/run_unit_tests.sh build_flatpak/tests'
- name: Collect test results
if: always()
shell: bash
run: |
d=$(ls -d .flatpak-builder/build/OrcaSlicer-* 2>/dev/null | tail -1 || true)
[ -n "$d" ] && [ -f "$d/ctest_results.xml" ] && cp "$d/ctest_results.xml" ctest_results.xml || true
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-${{ inputs.artifact }}
path: ctest_results.xml
retention-days: 5
if-no-files-found: warn
- name: Delete Test Asset
if: success()
uses: geekyeggo/delete-artifact@v6
with:
name: ${{ inputs.artifact }}
failOnError: false
-38
View File
@@ -1,38 +0,0 @@
name: Update Translation Catalog
on:
# schedule:
# - cron: 0 0 * * 1
workflow_dispatch:
jobs:
update_translation:
name: Update translation
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Install gettext
run: |
sudo apt-get update
sudo apt-get install -y gettext
- name: Update translation catalog
run: |
./scripts/run_gettext.sh --full
git add localization/i18n/*
- name: Commit translation catalog updates
uses: qoomon/actions--create-commit@v1
id: commit
with:
message: Update translation catalog
skip-empty: true
- name: Push changes
run: git push
-14
View File
@@ -1,14 +0,0 @@
name: Publish to WinGet
on:
release:
types: [ released ]
jobs:
publish:
runs-on: windows-latest
steps:
- uses: vedantmgoyal9/winget-releaser@main
with:
identifier: SoftFever.OrcaSlicer
version: ${{ github.event.release.tag_name }}
token: ${{ secrets.WINGET_TOKEN }}
installers-regex: '\.exe$'
+5 -36
View File
@@ -1,11 +1,7 @@
Build
Build.bat
/build*/
/out/
CMakeLists.txt.user
CMakeUserPresets.json
**/CMakeLists.txt.autosave
deps/build*
/build/
deps/build
MYMETA.json
MYMETA.yml
_build
@@ -21,36 +17,9 @@ xs/assertlib*
local-lib
/src/TAGS
/.vscode/
/.vscode
build-linux/*
deps/build*/*
deps/build-linux/*
**/.DS_Store
install_*
build_*/
install_dir
install_release
SVG
src/OrcaSlicer-doc/
.idea/
/.cache/
*.mo
**/process_full/
**/machine_full/
**/filament_full/
/deps/DL_CACHE/
/deps/DL_CACHE
**/.flatpak-builder/
*.no-debug.yml
resources/profiles/user/default
*.code-workspace
deps_src/build/
test.js
/.cache/
.clangd
internal_docs/
*.flatpak
/flatpak-repo/
# Python bytecode
__pycache__/
*.pyc
*.opc
/.test/
docs/superpowers/
-98
View File
@@ -1,98 +0,0 @@
# CLAUDE.md
OrcaSlicer — open-source C++17 3D slicer. wxWidgets GUI, CMake build system.
## Build Commands
```bash
# macOS
cmake --build build/arm64 --config RelWithDebInfo --target all --
# Linux
cmake --build build --config RelWithDebInfo --target all --
# Windows (replace %build_type% with Debug/Release/RelWithDebInfo)
cmake --build . --config %build_type% --target ALL_BUILD -- -m
```
## Testing
Catch2 framework. Tests in `tests/`; see [tests/AGENTS.md](tests/AGENTS.md) for where a new test belongs and the conventions to follow.
```bash
cd build && ctest -C Release --output-on-failure # all tests
ctest --test-dir ./tests/libslic3r -C Release # individual suite
ctest --test-dir ./tests/fff_print -C Release
```
## Documentation
- Docs live in `docs/`; the high-level design of a subsystem goes in `docs/HLSD/<subsystem>.md`.
- Describe the design as it stands — what the subsystem does, why it exists, and the constraints that shape it. Not the route that got there: no phases, task lists, status markers, or "before/after this PR" framing.
- Planning and investigation output (brainstorms, superpowers design and plan docs) stays in `docs/superpowers/`, which is gitignored. Never commit it.
- Write a doc only when the design is not evident from the code, and when a change invalidates an existing one, update it in the same PR.
## Code Style
- C++17, selective C++20. PascalCase classes, snake_case functions/variables
- `#pragma once` for headers. Smart pointers and RAII preferred
- Parallelization via TBB — be mindful of shared state
- Always use `SetSizerAndFit(sizer)` instead of `SetSizer(sizer)` on top level window. Unless `SetSizer` must be called before the full layout is built, call `sizer->SetSizeHints(window)` afterwards in this case.
## Key Entry Points
- App startup: `src/OrcaSlicer.cpp`
- Slicing pipeline: `src/libslic3r/Print.cpp`
- All print/printer/material settings: `src/libslic3r/PrintConfig.cpp`
- GUI: `src/slic3r/GUI/`
- Core algorithms: `src/libslic3r/` (GCode/, Fill/, Support/, Geometry/, Format/, Arachne/)
- Printer profiles: `resources/profiles/[manufacturer].json`
## Critical Constraints
- **Backward compatibility required** for .3mf project files and printer profiles
- **Cross-platform** — all changes must work on Windows, macOS, and Linux
- Profile/format changes need version migration handling
- Dependencies built separately in `deps/build/`, then linked to main app
## Code review focus areas
- Changes must not cause regressions in existing functionality, defaults, profiles, or project compatibility.
- Features gated by options must not affect existing behavior when those options are disabled.
- Changes should follow the existing code style and architecture. Architectural changes should be justified in code comments and the PR description.
- Add helper functions or utilities only when existing code cannot reasonably be reused. Avoid duplication.
- Keep code concise and clear. Manually simplify AI generated bloated codes before review.
- Include targeted tests or documented verification for behavior changes, especially in slicing logic, profiles, formats, and GUI defaults.
- For profile changes (`resources/profiles/<Vendor>/**`), check that `version` in the sibling `resources/profiles/<Vendor>.json` was bumped.
- For translation changes (`localization/i18n/**/*.po`), check that recurring terms match the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) for that language.
## Localization & translations
Catalogs live in `localization/i18n/<lang>/OrcaSlicer_<lang>.po`; the template is `OrcaSlicer.pot`.
See the [Localization guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_guide.md) for the human-facing version of these principles.
### Terminology
- Use the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) as the source of truth for recurring terms, so the same English term is always rendered the same way within a language, and terms that must stay in English (brand/product names, acronyms, materials, file formats, G-code tokens, macros/variables/identifiers) are not translated.
- If a term's established translation changes, update both the affected `.po` files and the glossary (`localization_glossary.tsv`, then regenerate) so they stay in sync.
- Translate the *meaning*, not the words. Check what the string actually controls before translating it — English reuses one word for different things. `Flow ratio` (multiplier), `Flow Rate` (throughput) and `Flow Dynamics` (pressure compensation) are three different terms; `extruder` may mean the toolhead, the feeder motor, or the nozzle depending on the string.
- Reuse one template per recurring message shape (`Failed to connect to …`, `Are you sure you want to …?`), even where the English wording varies.
### Editing rules
- Only edit `msgstr`**never** change `msgid`, and never "fix" wrong English in the translation alone. Report the source string instead.
- Preserve exactly: placeholders (`%s`, `%d`, `%1%`, `%zu`, `%%`), every `\n` (count *and* position, including leading/trailing), leading/trailing spaces, HTML tags, `℃`, and the file's encoding and line endings.
- **Never reorder positional arguments** in a `c-format` string. If the msgid is `%d` then `%s`, that order must hold — swapping them breaks at runtime.
- `msgctxt` separates homonyms — always read it. `Back`/`Camera View` is the rear view of the 3D navigator, while `Back`/`Navigation` is the go-back button; `Top` exists in the *Alignment*, *Layers* and *Camera View* senses.
- When a string needs disambiguating, add context in the source (`_L_CONTEXT`/`_u8L_CONTEXT`), don't work around it in the translation.
- A literal `%` inside a string xgettext flagged `possible-c-format` will fail `msgfmt`. Fix it with a `// xgettext:no-c-format, no-boost-format` comment above the string in the source — do not mangle the translation or use `%%` in text that is never passed through printf.
- Plural entries: read `nplurals` from the catalog's `Plural-Forms` header (it is **not** always 2 — ja/ko/zh/th/vi use 1, ru/cs/pl/lt use 3, uk uses 4). Each form must be genuinely inflected for its quantity; repeating one sentence across all forms is a bug in Slavic/Baltic languages, though it is correct for Turkish and Hungarian.
- An entry whose `msgstr` equals its `msgid` is untranslated even though it is not empty; a plural entry with any empty form is likewise incomplete.
- Mark machine-produced translations with an `# AI Translated` translator comment. Don't add it to a human translation you didn't actually rewrite.
- Don't reflow or re-wrap unrelated entries — keep the diff limited to the strings you changed.
### Verifying
- `scripts/run_gettext.bat --full` (Windows) regenerates the template, merges every catalog and compiles the `.mo` files. It must exit 0.
- Or check a single catalog with `msgfmt --check-format -o <out>.mo localization/i18n/<lang>/OrcaSlicer_<lang>.po`.
- Fuzzy entries are not shown to users. If you correct one, clear its `fuzzy` flag, otherwise the fix never ships.
BIN
View File
Binary file not shown.
+103
View File
@@ -0,0 +1,103 @@
{
"build_systems":
[
{
"name": "List",
//"file_regex": " at ([^-\\s]*) line ([0-9]*)",
// "file_regex": " at (D\\:\\/src\\/Slic3r\\/.*?) line ([0-9]*)",
"shell_cmd": "ls -l"
},
{
"name": "Run",
"working_dir": "$project_path",
"file_regex": " at (.*?) line ([0-9]*)",
// "shell_cmd": "chdir & perl slic3r.pl --DataDir \"C:\\Users\\Public\\Documents\\Prusa3D\\Slic3r settings MK2\" --gui \"..\\Slic3r-tests\\gap fill torture 20 -rt.stl\""
"shell_cmd": "chdir & perl slic3r.pl"
},
{
"name": "full",
"file_regex": "^(..[^:]*):([0-9]+):?([0-9]+)?:? (.*)$",
"shell_cmd": "chdir & perl Build.pl"
},
{
"name": "xs",
"working_dir": "$project_path/build",
// for Visual Studio:
"file_regex": "^(..[^:]*)\\(([0-9]+)\\)(.*)$",
// For GCC:
// "file_regex": "^(..[^:]*):([0-9]+):?([0-9]+)?:? (.*)$",
"shell_cmd": "chdir & ninja -j 6 -v",
"env": {
// "PATH": "C:\\Program Files (x86)\\MSBuild\\12.0\\bin\\amd64;C:\\Program Files (x86)\\Microsoft Visual Studio 12.0\\VC\\BIN\\amd64;C:\\Program Files (x86)\\Microsoft Visual Studio 12.0\\Common7\\IDE;C:\\Program Files (x86)\\Microsoft Visual Studio 12.0\\Common7\\Tools;%PATH%;c:\\wperl64d\\site\\bin;c:\\wperl64d\\bin",
// "PERL_CPANM_HOME": "c:\\wperl64d\\cpanm",
// "WXDIR": "D:\\src-perl\\wxWidgets-3.0.3-beta1",
// "BOOST_DIR": "D:\\src-perl\\boost_1_61_0",
// "BOOST_INCLUDEDIR": "D:\\src-perl\\boost_1_61_0",
// "BOOST_LIBRARYDIR": "D:\\src-perl\\boost_1_61_0\\stage\\x64\\lib",
// "SLIC3R_STATIC": "1"
}
},
{
"name": "xs & run",
"working_dir": "$project_path/build",
"file_regex": "^(..[^:]*):([0-9]+):?([0-9]+)?:? (.*)$",
"shell_cmd": "chdir & ninja -j 6 & cd .. & perl slic3r.pl --gui \"..\\Slic3r-tests\\star3-big2.stl\""
},
{
"name": "Slic3r - clean",
"working_dir": "$project_path/build",
"file_regex": "^(..[^:]*)(?::|\\()([0-9]+)(?::|\\))(?:([0-9]+):)?\\s*(.*)",
"shell_cmd": ["chdir & ninja clean"]
},
{
"name": "run tests",
"working_dir": "$project_path/build",
// for Visual Studio:
"file_regex": "^(..[^:]*)\\(([0-9]+)\\)(.*)$",
"shell_cmd": "chdir & ctest --verbose"
},
{
"name": "Clean & Configure",
"working_dir": "$project_path",
// for Visual Studio:
"file_regex": "^(..[^:]*)(?::|\\()([0-9]+)(?::|\\))(?:([0-9]+):)?\\s*(.*)",
"shell_cmd": "chdir & rmdir /S /Q build & mkdir build & cd build & cmake -G Ninja .. -DCMAKE_COLOR_MAKEFILE=OFF -DCMAKE_RULE_PROGRESS=OFF -DCMAKE_RULE_MESSAGES=OFF -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo"
},
{
"name": "Configure",
"working_dir": "$project_path/build",
// for Visual Studio:
"file_regex": "^(..[^:]*)(?::|\\()([0-9]+)(?::|\\))(?:([0-9]+):)?\\s*(.*)",
"shell_cmd": "cmake -G Ninja .. -DCMAKE_COLOR_MAKEFILE=OFF -DCMAKE_RULE_PROGRESS=OFF -DCMAKE_RULE_MESSAGES=OFF -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo"
}
],
"folders":
[
{
"path": ".",
// "folder_exclude_patterns": [".svn", "._d", ".metadata", ".settings"],
"file_exclude_patterns": ["XS.c", "*.pch", "*.ilk", "*.js" ]
}
],
"settings":
{
"sublimegdb_workingdir": "${folder:${project_path:run}}",
// NOTE: You MUST provide --interpreter=mi for the plugin to work
// "sublimegdb_commandline": "D:\\Qt\\Tools\\mingw492_32\\bin\\gdb.exe --interpreter=mi -ex 'target localhost:2345'",
// "sublimegdb_commandline": "D:\\Qt\\Tools\\mingw492_32\\bin\\gdb.exe --interpreter=mi perl --args perl slic3r.pl",
// "sublimegdb_commandline": "D:\\Qt\\Tools\\mingw492_32\\bin\\gdb.exe --interpreter=mi perl --args slic3r.pl ",
// "sublimegdb_commandline": "D:\\Qt\\Tools\\mingw492_32\\bin\\gdb.exe --interpreter=mi -e C:\\Strawberry\\perl\\bin\\perl.exe -s C:\\Strawberry\\perl\\site\\lib\\auto\\Slic3r\\XS\\XS.xs.dll --args perl slic3r.pl -j 1 --gui D:\\src\\Slic3r-tests\\star3-big.stl",
"sublimegdb_commandline": "D:\\Qt\\Tools\\mingw492_32\\bin\\gdb.exe --interpreter=mi perl.exe --args perl slic3r.pl -j 1 --gui", // D:\\src\\Slic3r-tests\\star3-big.stl",
// "sublimegdb_commandline": "D:\\Qt\\Tools\\mingw492_32\\bin\\gdb.exe --interpreter=mi -x slic3r.gdb",
// "arguments": "slic3r -j 1 --gui ../Slic3r-tests/star3-big.stl",
// "arguments": "../slic3r.pl -j 1 --gui",
// "sublimegdb_exec_cmd": "-exec-continue",
// Add "pending breakpoints" for symbols that are dynamically loaded from
// external shared libraries
"debug_ext" : true,
"run_after_init": false,
"close_views": false
}
}
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/perl
print "This script is currently used for installing Perl dependenices for running\n";
print "the libslic3r unit / integration tests through Perl prove.\n";
print "If you don't plan to run the unit / integration tests, you don't need to\n";
print "install these dependencies to build and run BambuSlicer.\n";
use strict;
use warnings;
use Config;
use File::Spec;
my %prereqs = qw(
Devel::CheckLib 0
ExtUtils::MakeMaker 6.80
ExtUtils::ParseXS 3.22
ExtUtils::XSpp 0
ExtUtils::XSpp::Cmd 0
ExtUtils::CppGuess 0
ExtUtils::Typemaps 0
ExtUtils::Typemaps::Basic 0
File::Basename 0
File::Spec 0
Getopt::Long 0
Module::Build::WithXSpp 0.14
Moo 1.003001
POSIX 0
Scalar::Util 0
Test::More 0
IO::Scalar 0
Time::HiRes 0
);
my %recommends = qw(
Class::XSAccessor 0
Test::Harness 0
);
my $sudo = grep { $_ eq '--sudo' } @ARGV;
my $nolocal = grep { $_ eq '--nolocal' } @ARGV;
my @missing_prereqs = ();
if ($ENV{SLIC3R_NO_AUTO}) {
foreach my $module (sort keys %prereqs) {
my $version = $prereqs{$module};
next if eval "use $module $version; 1";
push @missing_prereqs, $module if exists $prereqs{$module};
print "Missing prerequisite $module $version\n";
}
foreach my $module (sort keys %recommends) {
my $version = $recommends{$module};
next if eval "use $module $version; 1";
print "Missing optional $module $version\n";
}
} else {
my @try = (
$ENV{CPANM} // (),
File::Spec->catfile($Config{sitebin}, 'cpanm'),
File::Spec->catfile($Config{installscript}, 'cpanm'),
);
my $cpanm;
foreach my $path (@try) {
if (-e $path) { # don't use -x because it fails on Windows
$cpanm = $path;
last;
}
}
if (!$cpanm) {
if ($^O =~ /^(?:darwin|linux)$/ && system(qw(which cpanm)) == 0) {
$cpanm = 'cpanm';
}
}
die <<'EOF'
cpanm was not found. Please install it before running this script.
There are several ways to install cpanm, try one of these:
apt-get install cpanminus
curl -L http://cpanmin.us | perl - --sudo App::cpanminus
cpan App::cpanminus
If it is installed in a non-standard location you can do:
CPANM=/path/to/cpanm perl Build.PL
EOF
if !$cpanm;
my @cpanm_args = ();
push @cpanm_args, "--sudo" if $sudo;
# install local::lib without --local-lib otherwise it's not usable afterwards
if (!eval "use local::lib qw(local-lib); 1") {
my $res = system $cpanm, @cpanm_args, 'local::lib';
warn "Warning: local::lib is required. You might need to run the `cpanm --sudo local::lib` command in order to install it.\n"
if $res != 0;
}
push @cpanm_args, ('--local-lib', 'local-lib') if ! $nolocal;
# make sure our cpanm is updated (old ones don't support the ~ syntax)
system $cpanm, @cpanm_args, 'App::cpanminus';
my %modules = (%prereqs, %recommends);
foreach my $module (sort keys %modules) {
my $version = $modules{$module};
my @cmd = ($cpanm, @cpanm_args);
# temporary workaround for upstream bug in test
push @cmd, '--notest'
if $module =~ /^(?:OpenGL|Test::Harness)$/;
push @cmd, "$module~$version";
my $res = system @cmd;
if ($res != 0) {
if (exists $prereqs{$module}) {
push @missing_prereqs, $module;
} else {
printf "Don't worry, this module is optional.\n";
}
}
}
}
print "\n";
print "In the next step, you need to build the BambuStudio C++ library.\n";
print "1) Create a build directory and change to it\n";
print "2) run cmake .. -DCMAKE_BUILD_TYPE=Release\n";
print "3) run make\n";
print "4) to execute the automatic tests, run ctest --verbose\n";
__END__
-1
View File
@@ -1 +0,0 @@
@AGENTS.md
+264 -899
View File
File diff suppressed because it is too large Load Diff
View File
+76 -255
View File
@@ -1,259 +1,80 @@
<div align="center">
<picture>
<img alt="OrcaSlicer logo" src="resources/images/OrcaSlicer.png" width="15%" height="15%">
</picture>
<a href="https://trendshift.io/repositories/15552" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15552" alt="OrcaSlicer%2FOrcaSlicer | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![GitHub Repo stars](https://img.shields.io/github/stars/OrcaSlicer/OrcaSlicer)](https://github.com/OrcaSlicer/OrcaSlicer/stargazers) [![Build all](https://github.com/OrcaSlicer/OrcaSlicer/actions/workflows/build_all.yml/badge.svg?branch=main)](https://github.com/OrcaSlicer/OrcaSlicer/actions/workflows/build_all.yml)
OrcaSlicer: an open source Next-Gen Slicing Software for Precision 3D Prints.
Optimize your prints with ultra-fast slicing, intelligent support generation, and seamless printer compatibility—engineered for perfection.
# Official links and community
#### Official Website:
<a href="https://www.orcaslicer.com/" style="font-size:2em;">OrcaSlicer.com</a>
#### Github Repository:
<a href="https://github.com/OrcaSlicer/OrcaSlicer"><img src="https://img.shields.io/badge/OrcaSlicer-181717?style=flat&logo=github&logoColor=white" width="200" alt="GitHub Logo"/> </a>
#### Follow us:
<a href="https://twitter.com/real_OrcaSlicer"><img src="https://img.shields.io/badge/real__OrcaSlicer-000000?style=flat&logo=x&logoColor=white" width="200" alt="X Logo"/> </a>
<a href="https://www.youtube.com/@OfficialOrcaSlicer"><img src="https://img.shields.io/badge/OfficialOrcaSlicer-FF0000?style=flat&logo=youtube&logoColor=white" width="200" alt="YouTube Logo"/> </a>
#### Join our Discord community:
<a href="https://discord.gg/P4VE9UY9gJ"><img src="https://img.shields.io/badge/-Discord-5865F2?style=flat&logo=discord&logoColor=fff" width="200" alt="discord logo"/> </a>
<table border="2" style="border-color: #ffa500; background-color:rgb(232, 220, 180); color: #856404;">
<tr>
<td>
<strong>⚠️ CAUTION:</strong><br>
Several clickbait and malicious websites, such as <b>orca-slicer[.]com</b> and <b>orcaslicer[.]net</b>, are pretending to be the official OrcaSlicer site. These sites may redirect you to dangerous downloads or contain misleading information.<br>
<b>Our only official website is <a href="https://www.orcaslicer.com/">www.orcaslicer.com</a>.</b><br><br>
If you come across any of these in search results, please <b>report them</b> as unsafe or phishing to help keep the community secure with:<br>
- <a href="https://safebrowsing.google.com/safebrowsing/report_phish/">Google Safe Browsing</a><br>
- <a href="https://www.microsoft.com/en-us/wdsi/support/report-unsafe-site">Microsoft Security Intelligence</a><br>
- <a href="https://ipthreat.net/tools/reportphishing">IPThreat</a>
</td>
</tr>
</table>
</div>
# Main features
- **[Advanced Calibration Tools](https://www.orcaslicer.com/wiki/calibration_guide)**
Comprehensive suite: temperature towers, flow rate, retraction & more for optimal performance.
- **[Precise Wall](https://www.orcaslicer.com/wiki/quality_settings_precision#precise-wall) and [Seam Control](https://www.orcaslicer.com/wiki/quality_settings_seam)**
Adjust outer wall spacing and apply scarf seams to enhance print accuracy.
- **[Sandwich Mode](https://www.orcaslicer.com/wiki/quality_settings_wall_and_surfaces#innerouterinner) and [Polyholes](https://www.orcaslicer.com/wiki/quality_settings_precision#polyholes) Support**
Use varied infill [patterns](https://www.orcaslicer.com/wiki/strength_settings_patterns) and accurate hole shapes for improved clarity.
- **[Overhang](https://www.orcaslicer.com/wiki/quality_settings_overhangs) and [Support Optimization](https://www.orcaslicer.com/wiki#support-settings)**
Modify geometry for printable overhangs with precise support placement.
- **[Granular Controls and Customization](https://www.orcaslicer.com/wiki#process-settings)**
Fine-tune print speed, layer height, pressure, and temperature with precision.
- **Network Printer Support**
Seamless integration with Klipper, PrusaLink, and OctoPrint for remote control.
- **[Mouse Ear Brims](https://www.orcaslicer.com/wiki/others_settings_brim) & [Adaptive Bed Mesh](https://www.orcaslicer.com/wiki/printer_basic_information_adaptive_bed_mesh)**
Automatic brims and adaptive mesh calibration ensure consistent adhesion.
- **User-Friendly Interface**
Intuitive drag-and-drop design with pre-made profiles for popular printers.
- **[Open-Source](https://github.com/OrcaSlicer/OrcaSlicer) & [Community Driven](https://discord.gg/P4VE9UY9gJ)**
Regular updates fueled by continuous community contributions.
- **Wide Printer Compatibility**
Supports a broad range of printers: Bambu Lab, Prusa, Creality, Voron, and more.
- Additional features can be found in the [change notes](https://github.com/OrcaSlicer/OrcaSlicer/releases/).
# Wiki
The [wiki](https://www.orcaslicer.com/wiki) aims to provide a detailed explanation of the slicer settings, including how to maximize their use and how to calibrate and set up your printer.
- **[Access the wiki here](https://www.orcaslicer.com/wiki)**
- **[Contribute to the wiki](https://www.orcaslicer.com/wiki/how_to_wiki)**
# Download
## Stable Release
📥 **[Download the Latest Stable Release](https://github.com/OrcaSlicer/OrcaSlicer/releases/latest)**
Visit our GitHub Releases page for the latest stable version of OrcaSlicer, recommended for most users.
## Nightly Builds
🌙 **[Download the Latest Nightly Build](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds)**
Explore the latest developments in OrcaSlicer with our nightly builds. Feedback on these versions is highly appreciated.
### Belt Printer Builds
The [nightly release](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds) ships **two parallel builds**: the standard build and a belt-printer build. Both are attached to the same release — tell them apart by the filename suffix:
- **Standard** — no suffix (e.g. `OrcaSlicer_Windows_Installer_x64_nightly.exe`)
- **Belt** — `_belt` suffix (e.g. `OrcaSlicer_Windows_Installer_x64_nightly_belt.exe`)
The `_belt` builds add **experimental support for belt / conveyor (infinite-Z) printers**, where the model is sliced against a tilted belt surface instead of a flat horizontal bed. They include ready-to-use belt printer profiles, the full belt slicing pipeline (mesh rotation and G-code transforms), belt-aware support generation, and a tilted-bed preview.
> ⚠️ Belt printer support is under active development and is **not yet merged into `main`** — it currently ships only in these parallel `_belt` builds, produced from the [`belt-printer`](https://github.com/OrcaSlicer/OrcaSlicer/tree/belt-printer) branch. See tracking PR [#14394](https://github.com/OrcaSlicer/OrcaSlicer/pull/14394) and the original documentation in [#12998](https://github.com/OrcaSlicer/OrcaSlicer/pull/12998).
# How to install
## Windows
Download the **Windows Installer exe** for your preferred version from the [releases page](https://github.com/OrcaSlicer/OrcaSlicer/releases). Both `x64` and `arm64` installers are published — pick the one matching your CPU.
- *For convenience there is also a portable build available.*
<details>
<summary>Troubleshooting</summary>
- *If you have troubles to run the build, you might need to install following runtimes:*
- [MicrosoftEdgeWebView2RuntimeInstallerX64](https://github.com/OrcaSlicer/OrcaSlicer/releases/download/v1.0.10-sf2/MicrosoftEdgeWebView2RuntimeInstallerX64.exe)
- [Details of this runtime](https://aka.ms/webview2)
- [Alternative Download Link Hosted by Microsoft](https://go.microsoft.com/fwlink/p/?LinkId=2124703)
- [vcredist2019_x64](https://github.com/OrcaSlicer/OrcaSlicer/releases/download/v1.0.10-sf2/vcredist2019_x64.exe)
- [Alternative Download Link Hosted by Microsoft](https://aka.ms/vs/17/release/vc_redist.x64.exe)
- This file may already be available on your computer if you've installed visual studio. Check the following location: `%VCINSTALLDIR%Redist\MSVC\v142`
</details>
### Microsoft Store
Install from the [Microsoft Store](https://apps.microsoft.com/detail/9mv6gl23xm59) when you prefer a Store-signed package (helps on Windows 11 Smart App Control).
### Windows Package Manager
```shell
winget install --id=SoftFever.OrcaSlicer -e
```
## Mac
1. Download the universal DMG, which runs on both Apple Silicon and Intel Macs.
2. Drag OrcaSlicer.app to Application folder.
3. *If you want to run a build from a PR, you also need to follow the instructions below:*
<details>
<summary>Quarantine</summary>
- Option 1 (You only need to do this once. After that the app can be opened normally.):
- Step 1: Hold _cmd_ and right click the app, from the context menu choose **Open**.
- Step 2: A warning window will pop up, click _Open_
- Option 2:
Execute this command in terminal:
```shell
xattr -dr com.apple.quarantine /Applications/OrcaSlicer.app
```
- Option 3:
- Step 1: open the app, a warning window will pop up
![mac_cant_open](./SoftFever_doc/mac_cant_open.png)
- Step 2: in `System Settings` -> `Privacy & Security`, click `Open Anyway`:
![mac_security_setting](./SoftFever_doc/mac_security_setting.png)
</details>
### Homebrew Cask
```shell
brew install --cask orcaslicer
```
The [Homebrew cask](https://formulae.brew.sh/cask/orcaslicer) installs the official macOS DMG from [GitHub Releases](https://github.com/OrcaSlicer/OrcaSlicer/releases).
## Linux
### Flathub (Recommended)
OrcaSlicer is available through FlatHub:
<a href='https://flathub.org/apps/com.orcaslicer.OrcaSlicer'><img width='240' alt='Download on Flathub' src='https://dl.flathub.org/assets/badges/flathub-badge-en.png'/></a>
Install from the command line:
```shell
flatpak install flathub com.orcaslicer.OrcaSlicer
flatpak run com.orcaslicer.OrcaSlicer
```
It can also be installed through graphical software managers (KDE Discover, GNOME Software, etc.) when Flathub is enabled. Search for **OrcaSlicer** in your software center.
### AppImage
AppImages are published for both **x86_64** and **aarch64** (ARM64). Pick the file matching your CPU — the ARM64 build has `aarch64` in its name (e.g. `OrcaSlicer_Linux_AppImage_Ubuntu2404_aarch64_*.AppImage`).
1. Download App image from the [releases page](https://github.com/OrcaSlicer/OrcaSlicer/releases).
2. Double click the downloaded file to run it.
3. If you run into trouble executing it, try this command in the terminal:
`chmod +x /path_to_appimage/OrcaSlicer_Linux.AppImage`
# How to Compile
All updated build instructions for Windows, macOS, and Linux are now available on the official [OrcaSlicer Wiki - How to build](https://www.orcaslicer.com/wiki/how_to_build) page.
Please refer to the wiki to ensure you're following the latest and most accurate steps for your platform.
# Klipper Note
If you're running Klipper, it's recommended to add the following configuration to your `printer.cfg` file.
```gcode
# Enable object exclusion
[exclude_object]
# Enable arcs support
[gcode_arcs]
resolution: 0.1
```
# Supports
**OrcaSlicer** is an open-source project, and we're deeply grateful to all our sponsors and backers.
Their generous support helps fund filaments and other essential 3D printing materials for the project.
Thank you! :)
## Sponsors
<table>
<tr>
<td>
<a href="https://qidi3d.com/" style="display:inline-block; border-radius:8px; background:#fff;">
<img src="SoftFever_doc\sponsor_logos\QIDI.png" alt="QIDI" width="100" height="100">
</a>
</td>
<td>
<a href="https://bigtree-tech.com/" style="display:inline-block; border-radius:8px; background:#222;">
<img src="SoftFever_doc\sponsor_logos\BigTreeTech.png" alt="BIGTREE TECH" width="100" height="100">
</a>
</td>
</tr>
</table>
## Backers:
**Ko-fi supporters** ☕: [Backers list](https://github.com/user-attachments/files/16147016/Supporters_638561417699952499.csv)
## Support the project
<a href="https://github.com/sponsors/SoftFever"><img src="https://img.shields.io/badge/GitHub%20Sponsors-30363D?style=flat&logo=GitHub-Sponsors&logoColor=EA4AAA" height="50"></a>
<a href="https://ko-fi.com/G2G5IP3CP"><img src="https://img.shields.io/badge/Support_me_on_Ko--fi-FF5E5B?style=flat&logo=ko-fi&logoColor=white" height="50"></a>
<a href="https://paypal.me/softfever3d"><img src="https://img.shields.io/badge/PayPal-003087?style=flat&logo=paypal&logoColor=fff" height="50"></a>
## Some Background
Open-source slicing has always been built on a tradition of collaboration and attribution. [Slic3r](https://github.com/Slic3r/Slic3r), created by Alessandro Ranellucci and the RepRap community, laid the foundation. [PrusaSlicer](https://github.com/prusa3d/PrusaSlicer) by Prusa Research built on Slic3r and acknowledged that heritage. [Bambu Studio](https://github.com/bambulab/BambuStudio) in turn forked from PrusaSlicer, and [SuperSlicer](https://github.com/supermerill/SuperSlicer) by @supermerill extended PrusaSlicer with community-driven enhancements. Each project carried the work of its predecessors forward, crediting those who came before.
OrcaSlicer began in that same spirit, drawing from BambuStudio, PrusaSlicer, and ideas inspired by CuraSlicer and SuperSlicer. But it has since grown far beyond its origins. Through relentless innovation — introducing advanced calibration tools, precise wall and seam control, tree supports, adaptive slicing, and hundreds of other features — OrcaSlicer has become the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry.
The OrcaSlicer logo was designed by community member [Justin Levine](https://github.com/jal-co).
A modified version of Bambu Studio.
It has following changes:
- Support third-party machines:
- Voron 2.4
- Voron Trident
- Voron 0.1
- Prusa MK3S
- Export to to .gcode file.
Video: https://youtu.be/zCc7mVwu2xQ
![image](https://user-images.githubusercontent.com/103989404/179447873-b7b2a200-7a00-409d-b9d5-b0522f5f2ec8.png)
![image](https://user-images.githubusercontent.com/103989404/179447890-a124bd43-dab2-46b7-b0da-ef6c67783d9c.png)
![image](https://user-images.githubusercontent.com/103989404/179396355-fc07135a-fd08-430c-aa8d-dc1060ae94d1.png)
![image](https://user-images.githubusercontent.com/103989404/179447933-7752235a-a6eb-468c-a304-6eb0875c4c95.png)
![image](https://user-images.githubusercontent.com/103989404/179396363-d9868dfc-f8d5-4227-ad19-a439dfc31ce5.png)
![image](https://user-images.githubusercontent.com/103989404/179398283-f71ea1bd-18a8-4526-9678-a3c63bc36964.png)
Image credits:
1. Voron 2.4: vorondesign.com
2. Voron bed texture: VoronUsers/bryansj
3. Voron 0.1: myself
4. Prusa MK3S: Prusa3d
=========================SPLIT=========================
![image](https://user-images.githubusercontent.com/106916061/179006347-497d24c0-9bd6-45b7-8c49-d5cc8ecfe5d7.png)
# BambuStudio
Bambu Studio is a cutting-edge, feature-rich slicing software.
It contains project-based workflows, systematically optimized slicing algorithms, and an easy-to-use graphic interface, bringing users an incredibly smooth printing experience.
Prebuilt Windows, macOS 64-bit releases are available through the [github releases page](https://github.com/bambulab/BambuStudio/releases/).
Linux version currently is not supported.
Bambu Studio is based on [PrusaSlicer](https://github.com/prusa3d/PrusaSlicer) by Prusa Research, which is from [Slic3r](https://github.com/Slic3r/Slic3r) by Alessandro Ranellucci and the RepRap community.
See the [wiki](https://github.com/bambulab/BambuStudio/wiki) and the [documentation directory](https://github.com/bambulab/BambuStudio/tree/master/doc) for more informations.
# What are Bambu Studio's main features?
Key features are:
- Basic slicing features & GCode viewer
- Multiple plates management
- Remote control & monitoring
- Auto-arrange objects
- Auto-orient objects
- Hybrid/Tree/Normal support types, Customized support
- multi-material printing and rich painting tools
- multi-platform (Win/Mac/Linux) support
- Global/Object/Part level slicing parameters
Other major features are:
- Advanced cooling logic controlling fan speed and dynamic print speed
- Auto brim according to mechanical analysis
- Support arc path(G2/G3)
- Support STEP format
- Assembly & explosion view
- Flushing transition-filament into infill/object during filament change
# How to compile
Following platforms are currently supported to compile:
- Windows 64-bit, [Compile Guide](https://github.com/bambulab/BambuStudio/wiki/Windows-Compile-Guide)
- Mac 64-bit, [Compile Guide](https://github.com/bambulab/BambuStudio/wiki/Mac-Compile-Guide)
# Report issue
You can add an issue to the [github tracker](https://github.com/bambulab/BambuStudio/issues) if **it isn't already present.**
# License
Bambu Studio is licensed under the GNU Affero General Public License, version 3. Bambu Studio is based on PrusaSlicer by PrusaResearch.
PrusaSlicer is licensed under the GNU Affero General Public License, version 3. PrusaSlicer is owned by Prusa Research. PrusaSlicer is originally based on Slic3r by Alessandro Ranellucci.
Slic3r is licensed under the GNU Affero General Public License, version 3. Slic3r was created by Alessandro Ranellucci with the help of many other contributors.
The GNU Affero General Public License, version 3 ensures that if you use any part of this software in any way (even behind a web server), your software must be released under the same license.
The BambuNetworking and BambuTunnel plugins are based on non-free libraries. They are optional to the Bambu Studio and provides extended functionalities for users.
- **OrcaSlicer** is licensed under the GNU Affero General Public License, version 3.
- The **GNU Affero General Public License**, version 3 ensures that if you use any part of this software in any way (even behind a web server), your software must be released under the same license.
- OrcaSlicer includes a **pressure advance calibration pattern test** adapted from Andrew Ellis' generator, which is licensed under GNU General Public License, version 3. Ellis' generator is itself adapted from a generator developed by Sineos for Marlin, which is licensed under GNU General Public License, version 3.
- The **Bambu networking plugin** is based on non-free libraries from BambuLab. It is optional to the OrcaSlicer and provides extended functionalities for Bambulab printer users.
+1
View File
@@ -0,0 +1 @@
Init Version
-47
View File
@@ -1,47 +0,0 @@
# Security Policy
At OrcaSlicer, we are committed to maintaining the security of our ecosystem. Our policy is to ensure that we do not introduce vulnerabilities and that any security issues are addressed promptly and responsibly. We appreciate your help in improving the security of OrcaSlicer and thank you for your responsible disclosure.
Reporting Security Bugs
## To report a security bug, please follow these guidelines:
* Email Security Bugs:
Send an email to the lead maintainer at softfeverever@gmail.com.
Include the word "SECURITY" in the subject line of your email.
* Response Times:
The lead maintainer will acknowledge receipt of your email within one week (7 days).
A detailed response will follow within 48 hours, outlining the next steps for handling your report.
After the initial reply, the security team will keep you informed about the progress toward a fix and any announcements.
* Information and Collaboration:
We may request additional information or guidance as we work on addressing the issue.
* Handling the Report:
OrcaSlicer will confirm the problem and determine the affected versions.
We will audit the code to find any similar issues and prepare fixes for all releases still under maintenance.
Fixes will be released as quickly as possible.
* Third-Party Modules:
Report security issues in third-party modules to the respective maintainer of those modules.
## Security Disclosure Guidelines
When disclosing a vulnerability, please follow these steps to ensure your report is clear and actionable:
* Provide Detailed Information:
Scope: Clearly define the scope of the vulnerability.
Potential Impact: Let us know who could be affected by this exploit.
Reproduction Steps: Document detailed steps to reproduce the vulnerability.
Reference OWASP Guidelines:
Follow the <a href="https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html">OWASP Vulnerability Disclosure Cheat Sheet</a> for best practices in vulnerability disclosure.
## Security Recommendations
To enhance security when using OrcaSlicer, we recommend following these steps:
* SEE SOMETHING: If you notice anything suspicious or have concerns, please report it.
* SAY SOMETHING: If you have any doubts or need assistance, do not hesitate to contact us.
### Thank you for your commitment to the security of OrcaSlicer. Your efforts help us maintain a safe and reliable ecosystem.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 729 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+109
View File
@@ -0,0 +1,109 @@
src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp
src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp
src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp
src/slic3r/GUI/Gizmos/GLGizmoMove.cpp
src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp
src/slic3r/GUI/Gizmos/GLGizmoScale.cpp
src/slic3r/GUI/Gizmos/GLGizmosManager.cpp
src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp
src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp
src/slic3r/GUI/Gizmos/GLGizmoAdvancedCut.cpp
src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp
src/slic3r/GUI/Gizmos/GLGizmoFaceDetector.cpp
src/slic3r/GUI/Gizmos/GLGizmoModifier.cpp
src/slic3r/GUI/GUI.cpp
src/slic3r/GUI/GUI_App.cpp
src/slic3r/GUI/GUI_Init.cpp
src/slic3r/GUI/GUI_Factories.cpp
src/slic3r/GUI/GUI_ObjectList.cpp
src/slic3r/GUI/GUI_ObjectSettings.cpp
src/slic3r/GUI/GUI_ObjectTable.cpp
src/slic3r/GUI/GUI_ObjectTable.hpp
src/slic3r/GUI/GUI_ObjectTableSettings.cpp
src/slic3r/GUI/GUI_ObjectTableSettings.hpp
src/slic3r/GUI/GUI_Preview.cpp
src/slic3r/GUI/Widgets/SideTools.cpp
src/slic3r/GUI/Widgets/AMSControl.cpp
src/slic3r/GUI/ImGuiWrapper.cpp
src/slic3r/GUI/Jobs/ArrangeJob.cpp
src/slic3r/GUI/Jobs/OrientJob.cpp
src/slic3r/GUI/Jobs/FillBedJob.cpp
src/slic3r/GUI/Jobs/Job.cpp
src/slic3r/GUI/Jobs/PlaterJob.cpp
src/slic3r/GUI/Jobs/RotoptimizeJob.cpp
src/slic3r/GUI/Jobs/BindJob.cpp
src/slic3r/GUI/Jobs/PrintJob.cpp
src/slic3r/GUI/AboutDialog.cpp
src/slic3r/GUI/AMSMaterialsSetting.cpp
src/slic3r/GUI/AMSSetting.cpp
src/slic3r/GUI/BBLTopbar.cpp
src/slic3r/GUI/BackgroundSlicingProcess.cpp
src/slic3r/GUI/BedShapeDialog.cpp
src/slic3r/GUI/BedShapeDialog.hpp
src/slic3r/GUI/ConfigManipulation.cpp
src/slic3r/GUI/DeviceManager.cpp
src/slic3r/GUI/ExtraRenderers.cpp
src/slic3r/GUI/Field.cpp
src/slic3r/GUI/GCodeViewer.cpp
src/slic3r/GUI/GLCanvas3D.cpp
src/slic3r/GUI/Calibration.cpp
src/slic3r/GUI/CameraPopup.cpp
src/slic3r/GUI/ConnectPrinter.cpp
src/slic3r/GUI/DebugToolDialog.cpp
src/slic3r/GUI/HMSPanel.cpp
src/slic3r/GUI/MainFrame.cpp
src/slic3r/GUI/MediaPlayCtrl.cpp
src/slic3r/GUI/Mouse3DController.cpp
src/slic3r/GUI/StatusPanel.cpp
src/slic3r/GUI/Monitor.cpp
src/slic3r/GUI/MsgDialog.cpp
src/slic3r/GUI/NotificationManager.hpp
src/slic3r/GUI/NotificationManager.cpp
src/slic3r/GUI/ObjectDataViewModel.cpp
src/slic3r/GUI/OpenGLManager.cpp
src/slic3r/GUI/OptionsGroup.cpp
src/slic3r/GUI/ParamsPanel.cpp
src/slic3r/GUI/PartPlate.cpp
src/slic3r/GUI/Plater.cpp
src/slic3r/GUI/Preferences.cpp
src/slic3r/GUI/PresetComboBoxes.cpp
src/slic3r/GUI/PresetHints.cpp
src/slic3r/GUI/ProgressStatusBar.cpp
src/slic3r/GUI/PublishDialog.cpp
src/slic3r/GUI/SavePresetDialog.cpp
src/slic3r/GUI/Search.cpp
src/slic3r/GUI/Selection.cpp
src/slic3r/GUI/SelectMachine.cpp
src/slic3r/GUI/SendSystemInfoDialog.cpp
src/slic3r/GUI/BindDialog.cpp
src/slic3r/GUI/Tab.cpp
src/slic3r/GUI/Tab.hpp
src/slic3r/GUI/UnsavedChangesDialog.cpp
src/slic3r/GUI/Auxiliary.cpp
src/slic3r/GUI/UpdateDialogs.cpp
src/slic3r/GUI/WipeTowerDialog.cpp
src/slic3r/GUI/wxExtensions.cpp
src/slic3r/GUI/WebUserLoginDialog.cpp
src/slic3r/GUI/WebGuideDialog.cpp
src/slic3r/GUI/KBShortcutsDialog.hpp
src/slic3r/GUI/KBShortcutsDialog.cpp
src/slic3r/Utils/FixModelByWin10.cpp
src/slic3r/Utils/PresetUpdater.cpp
src/slic3r/Utils/Http.cpp
src/slic3r/Utils/Process.cpp
src/slic3r/GUI/Jobs/PrintJob.cpp
src/libslic3r/GCode.cpp
src/libslic3r/ExtrusionEntity.cpp
src/libslic3r/Flow.cpp
src/libslic3r/Format/AMF.cpp
src/libslic3r/miniz_extension.cpp
src/libslic3r/Preset.cpp
src/libslic3r/Print.cpp
src/libslic3r/PrintBase.cpp
src/libslic3r/PrintConfig.cpp
src/libslic3r/Zipper.cpp
src/libslic3r/PrintObject.cpp
src/libslic3r/PrintObjectSlice.cpp
src/libslic3r/PlaceholderParser.cpp
src/libslic3r/TreeSupport.cpp
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
# Add individual tools as executables in separate directories
add_subdirectory(bbs_gcode_checker)
install(TARGETS bbs_gcode_checker RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}")
@@ -0,0 +1,6 @@
cmake_minimum_required (VERSION 3.8)
project ("bbs_gcode_checker")
add_executable (bbs_gcode_checker "main.cpp" "GCodeChecker.cpp" "GCodeChecker.h" )
@@ -0,0 +1,602 @@
#include "GCodeChecker.h"
#include <fstream>
#include <math.h>
#include <map>
namespace BambuStudio {
//BBS: only check wodth when dE is longer than this value
const double CHECK_WIDTH_E_THRESHOLD = 0.0025;
const double WIDTH_THRESHOLD = 0.012;
const double RADIUS_THRESHOLD = 0.005;
const double filament_diameter = 1.75;
const double Pi = 3.14159265358979323846;
const std::string Extrusion_Role_Tag = " FEATURE: ";
const std::string Width_Tag = " LINE_WIDTH: ";
const std::string Wipe_Start_Tag = " WIPE_START";
const std::string Wipe_End_Tag = " WIPE_END";
const std::string Layer_Change_Tag = " CHANGE_LAYER";
const std::string Height_Tag = " LAYER_HEIGHT: ";
GCodeCheckResult GCodeChecker::parse_file(const std::string& path)
{
std::ifstream file(path);
if (file.fail()) {
std::cout << "Failed to open file " << path << std::endl;
return GCodeCheckResult::ParseFailed;
}
std::string line_raw;
std::string line;
while (std::getline(file, line_raw)) {
const char *c = line_raw.c_str();
c = skip_whitespaces(c);
if (std::toupper(*c) == 'N')
c = skip_word(c);
c = skip_whitespaces(c);
line = c;
if (parse_line(line) != GCodeCheckResult::Success) {
std::cout << "Failed to parse line " << line_raw << std::endl;
return GCodeCheckResult::ParseFailed;
}
}
if (m_layer_num == 0) {
std::cout << "Invalid gcode file without layer change comment" << std::endl;
return GCodeCheckResult::ParseFailed;
}
return GCodeCheckResult::Success;
}
bool GCodeChecker::include_chinese(const char* str)
{
char c;
while(1)
{
c=*str++;
if (is_end_of_line(c))
break;
if ((c & 0x80) && (*str & 0x80))
return true;
}
return false;
}
GCodeCheckResult GCodeChecker::parse_line(const std::string& line)
{
// update start position
m_start_position = m_end_position;
GCodeCheckResult ret;
const char *c = skip_whitespaces(line.c_str());
if (include_chinese(c)) {
//chinese is forbidden
return GCodeCheckResult::ParseFailed;
} if (is_end_of_line(*c)) {
//BBS: skip empty line
return GCodeCheckResult::Success;
} else if (is_comment_line(*c)) {
GCodeLine gcode_line;
gcode_line.m_raw = c;
ret = parse_comment(gcode_line);
if (ret != GCodeCheckResult::Success)
return ret;
} else {
GCodeLine gcode_line;
gcode_line.m_raw = c;
ret = parse_command(gcode_line);
if (ret != GCodeCheckResult::Success)
return ret;
ret = check_line_width(gcode_line);
if (ret != GCodeCheckResult::Success)
return ret;
}
return GCodeCheckResult::Success;
}
GCodeCheckResult GCodeChecker::parse_comment(GCodeLine& line)
{
const char *c = line.m_raw.c_str();
c++;
std::string comment = c;
// extrusion role tag
if (starts_with(comment, Extrusion_Role_Tag)) {
m_role = string_to_role(comment.substr(Extrusion_Role_Tag.length()));
} else if (starts_with(comment, Wipe_Start_Tag)) {
m_wiping = true;
} else if (starts_with(comment, Wipe_End_Tag)) {
m_wiping = false;
} else if (starts_with(comment, Height_Tag)) {
std::string str = comment.substr(Height_Tag.size());
if (!parse_double_from_str(str, m_height)) {
std::cout << "invalid height comment with invalid value!" << std::endl;
return GCodeCheckResult::ParseFailed;
}
} else if (starts_with(comment, Width_Tag)) {
std::string str = comment.substr(Width_Tag.size());
if (!parse_double_from_str(str, m_width)) {
std::cout << "invalid width comment with invalid value!" << std::endl;
return GCodeCheckResult::ParseFailed;
}
} else if (starts_with(comment, Layer_Change_Tag)) {
m_layer_num++;
}
return GCodeCheckResult::Success;
}
GCodeCheckResult GCodeChecker::parse_command(GCodeLine& gcode_line)
{
const std::string cmd = gcode_line.cmd();
GCodeCheckResult ret = GCodeCheckResult::Success;
switch (::toupper(cmd[0])) {
case 'G':
{
switch (::atoi(&cmd[1]))
{
case 0:
case 1: { ret = parse_G0_G1(gcode_line); break; } // Move
case 2:
case 3: { ret = parse_G2_G3(gcode_line); break; } // Move
case 90: { ret = parse_G90(gcode_line); break; } // Set to Absolute Positioning
case 91: { ret = parse_G91(gcode_line); break; } // Set to Relative Positioning
case 92: { ret = parse_G92(gcode_line); break; } // Set Position
default: { break; }
}
break;
}
case 'M':{
switch (::atoi(&cmd[1]))
{
case 82: { ret = parse_M82(gcode_line); break; } // Set to Absolute extrusion
case 83: { ret = parse_M83(gcode_line); break; } // Set to Relative extrusion
default: { break; }
}
break;
}
case 'T':{
break;
}
default: {
//BBS: other g command? impossible! must be invalid
ret = GCodeCheckResult::ParseFailed;
break;
}
}
return ret;
}
GCodeCheckResult GCodeChecker::parse_axis(GCodeLine& gcode_line)
{
const std::string cmd = gcode_line.m_raw;
const char* c = cmd.c_str();
c = skip_word(c);
while (! is_end_of_gcode_line(*c)) {
c = skip_whitespaces(c);
if (is_end_of_gcode_line(*c))
break;
Axis axis = UNKNOWN_AXIS;
switch (*c) {
case 'X': axis = X; break;
case 'Y': axis = Y; break;
case 'Z': axis = Z; break;
case 'E': axis = E; break;
case 'F': axis = F; break;
case 'I': axis = I; break;
case 'J': axis = J; break;
default:
//BBS: invalid command which has invalid axis
std::cout << "Invalid gcode because of invalid axis!" << std::endl;
return GCodeCheckResult::ParseFailed;
}
char *pend = nullptr;
double v = strtod(++c, &pend);
if (pend != nullptr && is_end_of_word(*pend) && !isnan(v) && !isinf(v)) {
gcode_line.m_axis[int(axis)] = v;
if (gcode_line.m_mask & (1 << int(axis))) {
//BBS: invalid command which has duplicated axis
std::cout << "Invalid gcode because of duplicated axis!" << std::endl;
return GCodeCheckResult::ParseFailed;
} else {
gcode_line.m_mask |= 1 << int(axis);
}
if (c == pend) {
//BBS: invalid command which has invalid axis value
std::cout << "Invalid gcode because of invalid axis value!" << std::endl;
return GCodeCheckResult::ParseFailed;
}
c = pend;
} else {
//BBS: invalid command for invalid axis value
std::cout << "Invalid gcode because of invalid axis value!" << std::endl;
return GCodeCheckResult::ParseFailed;
}
}
return GCodeCheckResult::Success;
}
GCodeCheckResult GCodeChecker::parse_G0_G1(GCodeLine& gcode_line)
{
if (parse_axis(gcode_line) != GCodeCheckResult::Success)
return GCodeCheckResult::ParseFailed;
//BBS: invalid G1 command which has no axis or invalid axis
if ((!gcode_line.m_mask) ||
gcode_line.has(I) ||
gcode_line.has(J)) {
std::cout << "Invalid G0_G1 gcode because of no axis or invalid axis!" << std::endl;
return GCodeCheckResult::ParseFailed;
}
//BBS: invalid G1 command which has zero speed
if (gcode_line.has(F) && gcode_line.get(F) == 0.0) {
std::cout << "Invalid G0_G1 gcode because has F axis but 0 speed!" << std::endl;
return GCodeCheckResult::ParseFailed;
}
return GCodeCheckResult::Success;
}
GCodeCheckResult GCodeChecker::parse_G2_G3(GCodeLine& gcode_line)
{
if (parse_axis(gcode_line) != GCodeCheckResult::Success)
return GCodeCheckResult::ParseFailed;
//BBS: invalid G2_G3 command which has no axis or Z axis
if (!gcode_line.m_mask) {
std::cout << "Invalid G2_G3 gcode because of no axis or has Z axis!" << std::endl;
return GCodeCheckResult::ParseFailed;
}
//BBS: invalid G2_G3 command which has zero speed
if (gcode_line.has(F) && gcode_line.get(F) == 0.0) {
std::cout << "Invalid G2_G3 gcode because has F axis but 0 speed!" << std::endl;
return GCodeCheckResult::ParseFailed;
}
//BBS: invalid G2_G3 command which has no I and J axis
if (!gcode_line.has(I) &&
!gcode_line.has(J)) {
std::cout << "Invalid G2_G3 gcode because of no I and J axis at same time!" << std::endl;
return GCodeCheckResult::ParseFailed;
}
//BBS: invalid G2_G3 command which has no X and Y axis at same time
if (!gcode_line.has(X) &&
!gcode_line.has(Y)) {
if (!gcode_line.has(X) || !gcode_line.has(P) || (int)gcode_line.get(P) != 1) {
std::cout << "Invalid G2_G3 gcode because of no X and Y axis at same time!" << std::endl;
return GCodeCheckResult::ParseFailed;
}
}
return GCodeCheckResult::Success;
}
GCodeCheckResult GCodeChecker::parse_G90(const GCodeLine& gcode_line)
{
const char* c = gcode_line.m_raw.c_str();
//BBS: G90 is single command with no argument
if (!is_single_gcode_word(c)) {
std::cout << "Invalid G90 gcode with invalid end!" << std::endl;
return GCodeCheckResult::ParseFailed;
}
m_global_positioning_type = EPositioningType::Absolute;
return GCodeCheckResult::Success;
}
GCodeCheckResult GCodeChecker::parse_G91(const GCodeLine& gcode_line)
{
const char* c = gcode_line.m_raw.c_str();
//BBS: G91 is single command with no argument
if (!is_single_gcode_word(c)) {
std::cout << "Invalid G91 gcode with invalid end!" << std::endl;
return GCodeCheckResult::ParseFailed;
}
m_global_positioning_type = EPositioningType::Relative;
return GCodeCheckResult::Success;
}
GCodeCheckResult GCodeChecker::parse_G92(GCodeLine& gcode_line)
{
if (parse_axis(gcode_line) != GCodeCheckResult::Success)
return GCodeCheckResult::ParseFailed;
//BBS: invalid G92 command which has no axis or invalid axis
if (!gcode_line.m_mask ||
gcode_line.has(F) ||
gcode_line.has(I) ||
gcode_line.has(J)) {
std::cout << "Invalid G2_G3 gcode because of no axis or invalid axis!" << std::endl;
return GCodeCheckResult::ParseFailed;
}
if (gcode_line.has(X))
m_origin[X] = m_end_position[X] - gcode_line.get(X);
if (gcode_line.has(Y))
m_origin[Y] = m_end_position[Y] - gcode_line.get(Y);
if (gcode_line.has(Z))
m_origin[Z] = m_end_position[Z] - gcode_line.get(Z);
if (gcode_line.has(E))
m_end_position[E] = gcode_line.get(E);
for (unsigned char a = X; a <= E; ++a) {
m_origin[a] = m_end_position[a];
}
return GCodeCheckResult::Success;
}
GCodeCheckResult GCodeChecker::parse_M82(const GCodeLine& gcode_line)
{
const char* c = gcode_line.m_raw.c_str();
//BBS: M82 is single command with no argument
if (!is_single_gcode_word(c)) {
std::cout << "Invalid M82 gcode with invalid end!" << std::endl;
return GCodeCheckResult::ParseFailed;
}
m_e_local_positioning_type = EPositioningType::Absolute;
return GCodeCheckResult::Success;
}
GCodeCheckResult GCodeChecker::parse_M83(const GCodeLine& gcode_line)
{
const char* c = gcode_line.m_raw.c_str();
//BBS: M83 is single command with no argument
if (!is_single_gcode_word(c)) {
std::cout << "Invalid M83 gcode with invalid end!" << std::endl;
return GCodeCheckResult::ParseFailed;
}
m_e_local_positioning_type = EPositioningType::Relative;
return GCodeCheckResult::Success;
}
double GCodeChecker::calculate_G1_width(const std::array<double, 3>& source,
const std::array<double, 3>& target,
double e, double height, bool is_bridge) const
{
double volume = e * Pi * (filament_diameter/2.0f) * (filament_diameter/2.0f);
std::array<double, 3> delta = { target[0] - source[0],
target[1] - source[1],
target[2] - source[2] };
double length = sqrt(delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2]);
double mm3_per_mm = volume / length;
return is_bridge? 2 * sqrt(mm3_per_mm/Pi) :
(mm3_per_mm / height) + height * (1 - 0.25 * Pi);
}
double GCodeChecker::calculate_G2_G3_width(const std::array<double, 2>& source,
const std::array<double, 2>& target,
const std::array<double, 2>& center,
bool is_ccw, double e, double height,
bool is_bridge) const
{
std::array<double, 2> v1 = { source[0] - center[0], source[1] - center[1] };
std::array<double, 2> v2 = { target[0] - center[0], target[1] - center[1] };
double dot = v1[0] * v2[0] + v1[1] * v2[1];
double cross = v1[0] * v2[1] - v1[1] * v2[0];
double radian = atan2(cross, dot);
radian = is_ccw ?
(radian < 0 ? 2 * Pi + radian : radian) :
(radian < 0 ? -radian : 2 * Pi - radian);
double radius = sqrt(v1[0] * v1[0] + v1[1] * v1[1]);
double length = radius * radian;
double volume = e * Pi * (filament_diameter/2) * (filament_diameter/2);
double mm3_per_mm = volume / length;
return is_bridge? 2 * sqrt(mm3_per_mm/Pi) :
(mm3_per_mm / height) + height * (1 - 0.25 * Pi);
}
GCodeCheckResult GCodeChecker::check_line_width(const GCodeLine& gcode_line)
{
//BBS: don't need to check extrusion before first layer
if (m_layer_num <= 0) {
return GCodeCheckResult::Success;
}
GCodeCheckResult ret = GCodeCheckResult::Success;
//BBS: only need to handle G0 G1 G2 G3
const std::string cmd = gcode_line.cmd();
int cmd_id = ::atoi(&cmd[1]);
if (::toupper(cmd[0]) == 'G')
switch (::atoi(&cmd[1]))
{
case 0:
case 1: { ret = check_G0_G1_width(gcode_line); break; }
case 2:
case 3: { ret = check_G2_G3_width(gcode_line); break; }
default: { break; }
}
return ret;
}
GCodeCheckResult GCodeChecker::check_G0_G1_width(const GCodeLine& line)
{
auto absolute_position = [this](Axis axis, const GCodeLine& lineG1) {
bool is_relative = (m_global_positioning_type == EPositioningType::Relative);
if (axis == E)
is_relative |= (m_e_local_positioning_type == EPositioningType::Relative);
if (lineG1.has(Axis(axis))) {
double ret = lineG1.get(Axis(axis));
return is_relative ? m_start_position[axis] + ret : m_origin[axis] + ret;
} else
return m_start_position[axis];
};
auto move_type = [this](const std::array<double, 4>& delta_pos) {
EMoveType type = EMoveType::Noop;
if (m_wiping)
type = EMoveType::Wipe;
else if (delta_pos[E] < 0.0f)
type = (delta_pos[X] != 0.0f || delta_pos[Y] != 0.0f || delta_pos[Z] != 0.0f) ? EMoveType::Travel : EMoveType::Retract;
else if (delta_pos[E] > 0.0f) {
if (delta_pos[X] == 0.0f && delta_pos[Y] == 0.0f)
type = (delta_pos[Z] == 0.0f) ? EMoveType::Unretract : EMoveType::Travel;
else if (delta_pos[X] != 0.0f || delta_pos[Y] != 0.0f)
type = EMoveType::Extrude;
}
else if (delta_pos[X] != 0.0f || delta_pos[Y] != 0.0f || delta_pos[Z] != 0.0f)
type = EMoveType::Travel;
return type;
};
for (unsigned char a = X; a <= E; ++a) {
m_end_position[a] = absolute_position((Axis)a, line);
}
// calculates movement deltas
std::array<double, 4> delta_pos;
for (unsigned char a = X; a <= E; ++a)
delta_pos[a] = m_end_position[a] - m_start_position[a];
// Todo: currently, for precision, there alwasy has possible to generate
// such gcode because of decimal truncation
/*if (line.has(Axis(E)) && delta_pos[E] == 0.0 && !m_wiping) {
std::cout << "Invalid GCode because has E axis but 0 extrusion" << std::endl;
return GCodeCheckResult::CheckFailed;
}*/
EMoveType type = move_type(delta_pos);
if (type == EMoveType::Extrude && m_end_position[Z] == 0.0f)
type = EMoveType::Travel;
//BBS: calculate line width and compare.
//Don't need to check gap fill which has verious width
if (type == EMoveType::Extrude &&
m_role != erGapFill &&
delta_pos[E] > CHECK_WIDTH_E_THRESHOLD) {
std::array<double, 3> source = { m_start_position[X], m_start_position[Y], m_start_position[Z] };
std::array<double, 3> target = { m_end_position[X], m_end_position[Y], m_end_position[Z] };
bool is_bridge = m_role == erOverhangPerimeter || m_role == erBridgeInfill;
double width_real = calculate_G1_width(source, target, delta_pos[E], m_height, is_bridge);
if (fabs(width_real - m_width) > WIDTH_THRESHOLD) {
std::cout << "Invalid G0_G1 because has abnormal line width." << std::endl;
std::cout << "Width: " << m_width << " Width_real: " << width_real << std::endl;
return GCodeCheckResult::CheckFailed;
}
}
return GCodeCheckResult::Success;
}
GCodeCheckResult GCodeChecker::check_G2_G3_width(const GCodeLine& line)
{
auto absolute_position = [this](Axis axis, const GCodeLine& lineG2_3) {
bool is_relative = (m_global_positioning_type == EPositioningType::Relative);
if (axis == E)
is_relative |= (m_e_local_positioning_type == EPositioningType::Relative);
if (lineG2_3.has(Axis(axis))) {
double ret = lineG2_3.get(Axis(axis));
if (axis == I)
return m_start_position[X] + ret;
else if (axis == J)
return m_start_position[Y] + ret;
else
return is_relative ? m_start_position[axis] + ret : m_origin[axis] + ret;
} else {
if (axis == I)
return m_start_position[X];
else if (axis == J)
return m_start_position[Y];
else
return m_start_position[axis];
}
};
auto move_type = [this](const double& delta_E) {
EMoveType type = EMoveType::Noop;
if (m_wiping)
type = EMoveType::Wipe;
else if (delta_E < 0.0f || delta_E == 0.0f)
type = EMoveType::Travel;
else
type = EMoveType::Extrude;
return type;
};
for (unsigned char a = X; a <= E; ++a) {
m_end_position[a] = absolute_position((Axis)a, line);
}
std::array<double, 2> source = { m_start_position[X], m_start_position[Y] };
std::array<double, 2> target = { m_end_position[X], m_end_position[Y] };
std::array<double, 2> center = { absolute_position(I, line),absolute_position(J, line) };
const std::string& cmd = line.cmd();
bool is_ccw = (::atoi(&cmd[1]) == 2) ? false : true;
double delta_e = m_end_position[E] - m_start_position[E];
EMoveType type = move_type(delta_e);
//BBS: judge whether is normal arc by radius
double radius1 = sqrt(pow((source[0] - center[0]), 2) + pow((source[1] - center[1]), 2));
double radius2 = sqrt(pow((target[0] - center[0]), 2) + pow((target[1] - center[1]), 2));
if (fabs(radius2 - radius1) > RADIUS_THRESHOLD) {
std::cout << "Invalid G2_G3 because of abnormal radius." << std::endl;
std::cout << "radius1: " << radius1 << " radius2: " << radius2 << std::endl;
return GCodeCheckResult::CheckFailed;
}
//BBS: calculate line width and compare
//Don't need to check gap fill which has verious width
if (type == EMoveType::Extrude &&
m_role != erGapFill &&
delta_e > CHECK_WIDTH_E_THRESHOLD) {
bool is_bridge = m_role == erOverhangPerimeter || m_role == erBridgeInfill;
double width_real = calculate_G2_G3_width(source, target, center, is_ccw, delta_e, m_height, is_bridge);
if (fabs(width_real - m_width) > WIDTH_THRESHOLD) {
std::cout << "Invalid G2_G3 because has abnormal line width." << std::endl;
std::cout << "Width: " << m_width << " Width_real: " << width_real << std::endl;
return GCodeCheckResult::CheckFailed;
}
}
return GCodeCheckResult::Success;
}
const std::map<std::string, ExtrusionRole> string_to_role_map = {
{ "Inner wall", erPerimeter },
{ "Outer wall", erExternalPerimeter },
{ "Overhang wall", erOverhangPerimeter },
{ "Sparse infill", erInternalInfill },
{ "Internal solid infill", erSolidInfill },
{ "Top surface", erTopSolidInfill },
{ "Bottom surface", erBottomSurface },
{ "Ironing", erIroning },
{ "Bridge", erBridgeInfill },
{ "Gap infill", erGapFill },
{ "Skirt", erSkirt },
{ "Brim", erBrim },
{ "Support", erSupportMaterial },
{ "Support interface", erSupportMaterialInterface },
{ "Support transition", erSupportTransition },
{ "Prime tower", erWipeTower },
{ "Custom", erCustom },
{ "Mixed", erMixed }
};
ExtrusionRole GCodeChecker::string_to_role(const std::string &role)
{
for (auto it = string_to_role_map.begin(); it != string_to_role_map.end(); it++) {
if (role == it->first)
return it->second;
}
return erNone;
}
}

Some files were not shown because too many files have changed in this diff Show More