Version in base suite: 1.0.0+dfsg-8 Base version: freecad_1.0.0+dfsg-8 Target version: freecad_1.0.0+dfsg-8+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/f/freecad/freecad_1.0.0+dfsg-8.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/f/freecad/freecad_1.0.0+dfsg-8+deb13u1.dsc changelog | 7 gbp.conf | 2 patches/1100-cam-fanuc-post-fix.patch | 610 ++++++++++++++++++++++++++++++++++ patches/series | 1 4 files changed, 619 insertions(+), 1 deletion(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp38ihaw24/freecad_1.0.0+dfsg-8.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp38ihaw24/freecad_1.0.0+dfsg-8+deb13u1.dsc: no acceptable signature found diff -Nru freecad-1.0.0+dfsg/debian/changelog freecad-1.0.0+dfsg/debian/changelog --- freecad-1.0.0+dfsg/debian/changelog 2025-07-07 15:48:33.000000000 +0000 +++ freecad-1.0.0+dfsg/debian/changelog 2026-03-07 21:02:07.000000000 +0000 @@ -1,3 +1,10 @@ +freecad (1.0.0+dfsg-8+deb13u1) trixie; urgency=medium + + * Maintaner approvided upload. + * Get fanuc post processor working (Closes: #1117850). + + -- Petter Reinholdtsen Sat, 07 Mar 2026 22:02:07 +0100 + freecad (1.0.0+dfsg-8) unstable; urgency=medium * Disable accidential added autopkgtest starttest, which was not ready. diff -Nru freecad-1.0.0+dfsg/debian/gbp.conf freecad-1.0.0+dfsg/debian/gbp.conf --- freecad-1.0.0+dfsg/debian/gbp.conf 2025-03-23 17:00:14.000000000 +0000 +++ freecad-1.0.0+dfsg/debian/gbp.conf 2026-03-07 21:02:07.000000000 +0000 @@ -1,4 +1,4 @@ [DEFAULT] pristine-tar = True sign-tags = true -debian-branch = debian/unstable +debian-branch = debian/trixie diff -Nru freecad-1.0.0+dfsg/debian/patches/1100-cam-fanuc-post-fix.patch freecad-1.0.0+dfsg/debian/patches/1100-cam-fanuc-post-fix.patch --- freecad-1.0.0+dfsg/debian/patches/1100-cam-fanuc-post-fix.patch 1970-01-01 00:00:00.000000000 +0000 +++ freecad-1.0.0+dfsg/debian/patches/1100-cam-fanuc-post-fix.patch 2026-03-07 21:02:07.000000000 +0000 @@ -0,0 +1,610 @@ +Description: fix fanuc postprocessor +Author: Petter Reinholdtsen +Bug: https://github.com/FreeCAD/FreeCAD/issues/27814 +Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1117850 +Origin: https://github.com/FreeCAD/FreeCAD/pull/27964 +Applied-Upstream: https://github.com/FreeCAD/FreeCAD/commit/b0f4ad4a142e35c0e12c2ff241aaa7e3b23c5fa2 +Last-Update: 2026-03-02 + +commit 13c8babc1a63e1a656fb6559bff90cff58954375 +Author: Petter Reinholdtsen +Date: Mon Mar 2 16:18:03 2026 +0100 + + CAM: Correct Fanuc post processor to work properly. + + Fixes crash problem, milling, drilling, tool changes, thread + tapping and get the post processor working as it should. + + This patch is tested on a Fanuc controller and sent upstream as + + + Closes: #1117850 + +diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +index 2edc7abaf..15af728a1 100644 +--- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py ++++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +@@ -1,3 +1,11 @@ ++# SPDX-License-Identifier: LGPL-2.1-or-later ++ ++""" ++ ++CAM post processor for CNC machines with a Fanuc controller. ++ ++""" ++ + # *************************************************************************** + # * Copyright (c) 2014 sliptonic * + # * Copyright (c) 2021 shadowbane1000 * +@@ -29,8 +37,9 @@ import argparse + import datetime + import shlex + import os.path ++import Path.Base.Util as PathUtil + import Path.Post.Utils as PostUtils +-import PathScripts.PathUtils as PathUtils ++from PathScripts import PathUtils + from builtins import open as pyopen + + TOOLTIP = """ +@@ -46,6 +55,16 @@ import fanuc_post + fanuc_post.export(object,"/path/to/file.ncc","") + """ + ++# Preamble text will appear at the beginning of the GCODE output file. ++DEFAULT_PREAMBLE = """G17 G54 G40 G49 G80 G90 G94 ++""" ++ ++# Postamble text will appear following the last operation. ++DEFAULT_POSTAMBLE = """M05 ++G17 G54 G90 G80 G40 ++M30 ++""" ++ + now = datetime.datetime.now() + + parser = argparse.ArgumentParser(prog="fanuc", add_help=False) +@@ -57,14 +76,19 @@ parser.add_argument( + action="store_true", + help="don't pop up editor before writing output", + ) +-parser.add_argument("--precision", default="3", help="number of digits of precision, default=3") ++parser.add_argument("--precision", help="number of digits of precision, default=3 (mm) or 4 (in)") + parser.add_argument( + "--preamble", +- help='set commands to be issued before the first command, default="G17\nG90"', ++ help='set commands to be issued before the first command, default="' ++ + DEFAULT_PREAMBLE.replace("\n", "\\n") ++ + '"', + ) + parser.add_argument( + "--postamble", +- help='set commands to be issued after the last command, default="M05\nG17 G90\nM2"', ++ help="set commands to be issued after the last command, " ++ + 'default="' ++ + DEFAULT_POSTAMBLE.replace("\n", "\\n") ++ + '"', + ) + parser.add_argument( + "--inches", action="store_true", help="Convert output for US imperial mode (G20)" +@@ -82,6 +106,11 @@ parser.add_argument( + action="store_true", + help="suppress tool length offset (G43) following tool changes", + ) ++parser.add_argument( ++ "--end-spindle-empty", ++ action="store_true", ++ help="place last tool in tool change carousel before postamble", ++) + + TOOLTIP_ARGS = parser.format_help() + +@@ -98,6 +127,8 @@ OUTPUT_DOUBLES = ( + COMMAND_SPACE = " " + LINENR = 100 # line number starting value + ++END_SPINDLE_EMPTY = False ++ + # These globals will be reflected in the Machine configuration of the project + UNITS = "G21" # G21 for metric, G20 for us standard + UNIT_SPEED_FORMAT = "mm/min" +@@ -112,16 +143,8 @@ PRECISION = 3 + # rigid tapping. + tapSpeed = 0 + +-# Preamble text will appear at the beginning of the GCODE output file. +-PREAMBLE = """G17 G54 G40 G49 G80 G90 +-""" +- +-# Postamble text will appear following the last operation. +-POSTAMBLE = """M05 +-G17 G54 G90 G80 G40 +-M6 T0 +-M2 +-""" ++PREAMBLE = DEFAULT_PREAMBLE ++POSTAMBLE = DEFAULT_POSTAMBLE + + # Pre operation text will be inserted before every operation + PRE_OPERATION = """""" +@@ -130,15 +153,33 @@ PRE_OPERATION = """""" + POST_OPERATION = """""" + + # Tool Change commands will be inserted before a tool change +-TOOL_CHANGE = """""" ++# Move to tool change Z position ++TOOL_CHANGE = """G28 G91 Z0 ++""" ++ ++# List of drill G codes where some parameters are required and their ++# required parameters. ++DRILL_OPERATION = ("G73", "G81", "G82", "G83", "G85") ++DRILL_PARAM_REQ = ("L", "P", "Q", "R", "Z") ++ ++# The settings shared between methods ++PREAMBLE = None ++POSTAMBLE = None + + + def processArguments(argstring): ++ """ ++ Apply default values and command line arguments before ++ processing commands. ++ ++ """ + global OUTPUT_HEADER + global OUTPUT_COMMENTS + global OUTPUT_LINE_NUMBERS + global SHOW_EDITOR + global PRECISION ++ global DEFAULT_PREAMBLE ++ global DEFAULT_POSTAMBLE + global PREAMBLE + global POSTAMBLE + global UNITS +@@ -146,35 +187,66 @@ def processArguments(argstring): + global UNIT_FORMAT + global MODAL + global USE_TLO ++ global END_SPINDLE_EMPTY + global OUTPUT_DOUBLES ++ global LINENR + + try: + args = parser.parse_args(shlex.split(argstring)) + if args.no_header: + OUTPUT_HEADER = False ++ else: ++ OUTPUT_HEADER = True + if args.no_comments: + OUTPUT_COMMENTS = False ++ else: ++ OUTPUT_COMMENTS = True + if args.line_numbers: + OUTPUT_LINE_NUMBERS = True ++ LINENR = 100 ++ else: ++ OUTPUT_LINE_NUMBERS = False + if args.no_show_editor: + SHOW_EDITOR = False +- print("Show editor = %d" % SHOW_EDITOR) +- PRECISION = args.precision ++ else: ++ SHOW_EDITOR = True ++ # print("Show editor = %s" % SHOW_EDITOR) # Commented to reduce test noise + if args.preamble is not None: +- PREAMBLE = args.preamble ++ PREAMBLE = args.preamble.replace("\\n", "\n") ++ else: ++ PREAMBLE = DEFAULT_PREAMBLE + if args.postamble is not None: +- POSTAMBLE = args.postamble ++ POSTAMBLE = args.postamble.replace("\\n", "\n") ++ else: ++ POSTAMBLE = DEFAULT_POSTAMBLE + if args.inches: + UNITS = "G20" + UNIT_SPEED_FORMAT = "in/min" + UNIT_FORMAT = "in" + PRECISION = 4 ++ else: ++ UNITS = "G21" ++ UNIT_SPEED_FORMAT = "mm/min" ++ UNIT_FORMAT = "mm" ++ PRECISION = 3 ++ if args.precision: ++ PRECISION = int(args.precision) + if args.no_modal: + MODAL = False ++ else: ++ MODAL = True + if args.no_tlo: + USE_TLO = False ++ else: ++ USE_TLO = True + if args.no_axis_modal: + OUTPUT_DOUBLES = True ++ else: ++ OUTPUT_DOUBLES = False ++ if args.end_spindle_empty: ++ END_SPINDLE_EMPTY = True ++ else: ++ END_SPINDLE_EMPTY = False + + except Exception: + return False +@@ -198,41 +270,56 @@ def export(objectslist, filename, argstring): + ) + return None + +- print("postprocessing...") ++ # print("postprocessing...") # Commented to reduce test noise + gcode = "" + ++ gcode += "%\n" ++ + # write header + if OUTPUT_HEADER: +- gcode += "%\n" +- gcode += ";\n" ++ # Get current version info ++ major = int(FreeCAD.ConfigGet("BuildVersionMajor")) ++ minor = int(FreeCAD.ConfigGet("BuildVersionMinor")) ++ ++ # the filename variable always contain "-", use more relevant ++ # information ++ job = PathUtils.findParentJob(objectslist[0]) ++ if job: ++ body, job = job.FullName.split("#") ++ else: ++ # Workaround for the TestFanucPost code, where there is no ++ # job returned by findParentJob ++ body, job = ("FREECAD-FILENAME-GOES-HERE", "JOB-NAME-GOES-HERE") ++ gcode += "(" + body.upper() + ", " + job.upper() + ")\n" + gcode += ( +- os.path.split(filename)[-1] +- + " (" +- + "FREECAD-FILENAME-GOES-HERE" +- + ", " +- + "JOB-NAME-GOES-HERE" +- + ")\n" ++ linenumber() + "(POST PROCESSOR: FANUC USING FREECAD %d.%d" % (major, minor) + ")\n" + ) +- gcode += linenumber() + "(" + filename.upper() + ",EXPORTED BY FREECAD!)\n" +- gcode += linenumber() + "(POST PROCESSOR: " + __name__.upper() + ")\n" + gcode += linenumber() + "(OUTPUT TIME:" + str(now).upper() + ")\n" + + # Write the preamble + if OUTPUT_COMMENTS: + gcode += linenumber() + "(BEGIN PREAMBLE)\n" +- for line in PREAMBLE.splitlines(False): ++ for line in PREAMBLE.splitlines(): + gcode += linenumber() + line + "\n" + gcode += linenumber() + UNITS + "\n" + + for obj in objectslist: + ++ # to stay compatible with FreeCAD 1.0 ++ def activeForOp(obj): ++ # The activeForOp method is available since 2025-05-04 / ++ # commit 1e87d8e6681b755b9757f94b1201e50eb84b28a2 ++ if hasattr(PathUtil, "activeForOp"): ++ return PathUtil.activeForOp(obj) ++ if hasattr(obj, "Active"): ++ return obj.Active ++ if hasattr(obj, "Base") and hasattr(obj.Base, "Active"): ++ return obj.Base.Active ++ return True ++ + # Skip inactive operations +- if hasattr(obj, "Active"): +- if not obj.Active: +- continue +- if hasattr(obj, "Base") and hasattr(obj.Base, "Active"): +- if not obj.Base.Active: +- continue ++ if not activeForOp(obj): ++ continue + + # do the pre_op + if OUTPUT_COMMENTS: +@@ -241,13 +328,26 @@ def export(objectslist, filename, argstring): + for line in PRE_OPERATION.splitlines(True): + gcode += linenumber() + line + ++ # to stay compatible with FreeCAD 1.0 ++ def coolantModeForOp(obj): ++ # The coolantModeForOp method is available since ++ # 2025-05-04 / commit ++ # 1e87d8e6681b755b9757f94b1201e50eb84b28a2 ++ if hasattr(PathUtil, "coolantModeForOp"): ++ return PathUtil.coolantModeForOp(obj) ++ if ( ++ hasattr(obj, "CoolantMode") ++ or hasattr(obj, "Base") ++ and hasattr(obj.Base, "CoolantMode") ++ ): ++ if hasattr(obj, "CoolantMode"): ++ return obj.CoolantMode ++ else: ++ return obj.Base.CoolantMode ++ return "None" ++ + # get coolant mode +- coolantMode = "None" +- if hasattr(obj, "CoolantMode") or hasattr(obj, "Base") and hasattr(obj.Base, "CoolantMode"): +- if hasattr(obj, "CoolantMode"): +- coolantMode = obj.CoolantMode +- else: +- coolantMode = obj.Base.CoolantMode ++ coolantMode = coolantModeForOp(obj) + + # turn coolant on if required + if OUTPUT_COMMENTS: +@@ -273,12 +373,18 @@ def export(objectslist, filename, argstring): + gcode += linenumber() + "(COOLANT OFF:" + coolantMode.upper() + ")\n" + gcode += linenumber() + "M9" + "\n" + ++ if END_SPINDLE_EMPTY: ++ if OUTPUT_COMMENTS: ++ gcode += "(BEGIN MAKING SPINDLE EMPTY)\n" ++ gcode += linenumber() + "M05\n" ++ for line in TOOL_CHANGE.splitlines(True): ++ gcode += linenumber() + line ++ gcode += linenumber() + "M6 T0\n" + # do the post_amble + if OUTPUT_COMMENTS: + gcode += "(BEGIN POSTAMBLE)\n" +- for line in POSTAMBLE.splitlines(True): +- gcode += linenumber() + line +- gcode += "%\n" ++ for line in POSTAMBLE.splitlines(): ++ gcode += linenumber() + line + "\n" + + if FreeCAD.GuiUp and SHOW_EDITOR: + dia = PostUtils.GCodeEditorDialog() +@@ -291,7 +397,7 @@ def export(objectslist, filename, argstring): + else: + final = gcode + +- print("done postprocessing.") ++ # print("done postprocessing.") # Commented to reduce test noise + + if not filename == "-": + gfile = pyopen(filename, "w") +@@ -311,6 +417,8 @@ def linenumber(): + + def parse(pathobj): + global PRECISION ++ global DRILL_OPERATION ++ global DRILL_PARAM_REQ + global MODAL + global OUTPUT_DOUBLES + global UNIT_FORMAT +@@ -398,6 +506,7 @@ def parse(pathobj): + for index, c in enumerate(commands): + + outstring = [] ++ outsuffix = [] + command = c.Name + if index + 1 == len(commands): + nextcommand = "" +@@ -415,83 +524,100 @@ def parse(pathobj): + if command == "G0": + continue + +- # if it's a tap, we rigid tap, so don't start the spindle yet... ++ # If tool a tap, we will thread tap, so stop the spindle ++ # for now as there is no point in starting it to stop it ++ # in the G74/G84 operation after S29. This only trigger ++ # when pathobj is a ToolController. + if command == "M03" or command == "M3": +- if pathobj.Tool.ToolType == "Tap": ++ if ( ++ hasattr(pathobj, "Tool") ++ and getattr(pathobj.Tool, "ShapeType", "").lower() == "tap" ++ ): + tapSpeed = int(pathobj.SpindleSpeed) + continue + +- # convert drill cycles to tap cycles if tool is a tap +- if command == "G81" or command == "G83": +- if ( +- hasattr(pathobj, "ToolController") +- and pathobj.ToolController.Tool.ToolType == "Tap" +- ): +- command = "G84" +- out += linenumber() + "G95\n" +- paramstring = "" +- for param in ["X", "Y"]: +- if param in c.Parameters: +- if ( +- (not OUTPUT_DOUBLES) +- and (param in currLocation) +- and (currLocation[param] == c.Parameters[param]) +- ): +- continue +- else: +- pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) +- paramstring += ( +- " " +- + param +- + format( +- float(pos.getValueAs(UNIT_FORMAT)), +- precision_string, +- ) +- ) +- if paramstring != "": +- out += linenumber() + "G00" + paramstring + "\n" +- +- if "S" in c.Parameters: +- tapSpeed = int(c.Parameters["S"]) +- out += "M29 S" + str(tapSpeed) + "\n" +- +- for param in ["Z", "R"]: +- if param in c.Parameters: +- if ( +- (not OUTPUT_DOUBLES) +- and (param in currLocation) +- and (currLocation[param] == c.Parameters[param]) +- ): +- continue +- else: +- pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) +- paramstring += ( +- " " +- + param +- + format( +- float(pos.getValueAs(UNIT_FORMAT)), +- precision_string, +- ) ++ # Handle thread tapping cycles. Uses rigid tapping. ++ # This only trigger when pathobj is a Operation. ++ if command == "G74" or command == "G84": ++ pitch_mm = float(c.Parameters["F"]) ++ # Convert pitch to inches if needed ++ if UNITS == "G20": # imperial ++ pitch = pitch_mm / 25.4 ++ else: ++ pitch = pitch_mm ++ paramstring = "" ++ for param in ["X", "Y"]: ++ if param in c.Parameters: ++ if ( ++ (not OUTPUT_DOUBLES) ++ and (param in currLocation) ++ and (currLocation[param] == c.Parameters[param]) ++ ): ++ continue ++ else: ++ pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) ++ paramstring += ( ++ " " ++ + param ++ + format( ++ float(pos.getValueAs(UNIT_FORMAT)), ++ precision_string, + ) +- # in this mode, F is the distance per revolution of the thread (pitch) +- # P is the dwell time in seconds at the bottom of the thread +- # Q is the peck depth of the threading operation +- for param in ["F", "P", "Q"]: +- if param in c.Parameters: +- value = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) ++ ) ++ if paramstring != "": ++ out += linenumber() + "G00" + paramstring + "\n" ++ ++ if "S" in c.Parameters: ++ tapSpeed = int(c.Parameters["S"]) ++ out += "M29 S" + str(tapSpeed) + "\n" ++ ++ for param in ["Z", "R"]: ++ if param in c.Parameters: ++ if ( ++ (not OUTPUT_DOUBLES) ++ and (param in currLocation) ++ and (currLocation[param] == c.Parameters[param]) ++ ): ++ continue ++ else: ++ pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + paramstring += ( + " " + + param + + format( +- float(value.getValueAs(UNIT_FORMAT)), ++ float(pos.getValueAs(UNIT_FORMAT)), + precision_string, + ) + ) + +- out += linenumber() + "G84" + paramstring + "\n" +- out += linenumber() + "G80\n" +- out += linenumber() + "G94\n" +- continue ++ # Calculate feed rate as distance per minute ++ if tapSpeed is not None: ++ feed_rate = pitch * tapSpeed ++ speed = Units.Quantity(feed_rate, UNIT_SPEED_FORMAT) ++ paramstring += " F" + format( ++ float(speed.getValueAs(UNIT_SPEED_FORMAT)), precision_string ++ ) ++ else: ++ # No spindle speed found, output pitch as F ++ paramstring += " F" + format(pitch, precision_string) ++ ++ # P is the dwell time in seconds at the bottom of the thread ++ # Q is the peck depth of the threading operation ++ for param in ["P", "Q"]: ++ if param in c.Parameters: ++ value = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) ++ paramstring += ( ++ " " ++ + param ++ + format( ++ float(value.getValueAs(UNIT_FORMAT)), ++ precision_string, ++ ) ++ ) ++ ++ out += linenumber() + command + paramstring + "\n" ++ out += linenumber() + "G80\n" # End tapping cycle ++ continue + + outstring.append(command) + +@@ -504,7 +630,7 @@ def parse(pathobj): + if command == "G80" and lastcommand == nextcommand: + continue + +- if c.Name[0] == "(" and not OUTPUT_COMMENTS: # command is a comment ++ if c.Name.startswith("(") and not OUTPUT_COMMENTS: # command is a comment + continue + + # Now add the remaining parameters in order +@@ -540,7 +666,11 @@ def parse(pathobj): + if ( + (not OUTPUT_DOUBLES) + and (param in currLocation) +- and (currLocation[param] == c.Parameters[param]) ++ and currLocation[param] == c.Parameters[param] ++ and ( ++ command not in DRILL_OPERATION ++ or (command in DRILL_OPERATION and param not in DRILL_PARAM_REQ) ++ ) + ): + continue + else: +@@ -575,14 +705,14 @@ def parse(pathobj): + # Check for Tool Change: + if command == "M6": + # stop the spindle +- out += linenumber() + "M5\n" ++ out += linenumber() + "M05\n" + for line in TOOL_CHANGE.splitlines(True): + out += linenumber() + line + + # add height offset + if USE_TLO: +- tool_height = "\nG43 H" + str(int(c.Parameters["T"])) +- outstring.append(tool_height) ++ outsuffix.append("G91 G0 G43 G54 Z-[#[2000+#4120]] H#4120") ++ outsuffix.append("G90") + + if command == "message": + if OUTPUT_COMMENTS is False: +@@ -596,9 +726,11 @@ def parse(pathobj): + outstring.insert(0, (linenumber())) + + # append the line to the final output +- for w in outstring: +- out += w.upper() + COMMAND_SPACE ++ out += COMMAND_SPACE.join(outstring).upper() + out = out.strip() + "\n" ++ if len(outsuffix) >= 1: ++ for line in outsuffix: ++ out += linenumber() + line + "\n" + + return out + diff -Nru freecad-1.0.0+dfsg/debian/patches/series freecad-1.0.0+dfsg/debian/patches/series --- freecad-1.0.0+dfsg/debian/patches/series 2025-07-07 15:46:22.000000000 +0000 +++ freecad-1.0.0+dfsg/debian/patches/series 2026-03-07 21:02:07.000000000 +0000 @@ -11,3 +11,4 @@ 1091-freecad-spelling-others.patch 1110-GL_MULTISAMPLE.patch 2080-force-xcb-on-wayland.patch +1100-cam-fanuc-post-fix.patch