diff --git a/.github/workflows/build_releases.yml b/.github/workflows/build_releases.yml
index 1ceac53..808eecc 100644
--- a/.github/workflows/build_releases.yml
+++ b/.github/workflows/build_releases.yml
@@ -12,7 +12,7 @@ env:
 
 jobs:
   build-all:
-    runs-on: windows-latest
+    runs-on: ubuntu-24.04
     steps:
       - name: Checkout Backend Repo
         uses: actions/checkout@v4
@@ -28,10 +28,10 @@ jobs:
 
       - name: Build All
         run: |
-          .\build_release.ps1
+          python ./build_release.py
 
       - name: Save Zip of all targets
         uses: actions/upload-artifact@v4
         with:
           name: apc-p15-tool-release
-          path: .\_out\_release
+          path: ./_out/_release
diff --git a/build_release.ps1 b/build_release.ps1
deleted file mode 100644
index ead0c53..0000000
--- a/build_release.ps1
+++ /dev/null
@@ -1,54 +0,0 @@
-# base build path, relative to script
-$outRelativeDir = "\_out"
-
-###
-
-# Script dir is root
-$scriptDir = Get-Location
-$outBaseDir = Join-Path -Path $scriptDir -ChildPath $outRelativeDir
-$outReleaseDir = Join-Path -Path $outBaseDir -ChildPath "\_release"
-
-# ensure release path exists
-New-Item -ItemType Directory -Force -Path $outReleaseDir | Out-Null
-
-# get version number (tag)
-$gitTag = $(git describe --tags --abbrev=0)
-
-# GOOS_GOARCH to build for
-$targets = @(
-  "windows_amd64",
-  "linux_amd64",
-  "linux_arm64",
-  "darwin_amd64",
-  "darwin_arm64"
-)
-
-# loop through and build all targets
-foreach ($target in $targets) {
-  # environment vars
-  $split = $target.split("_")
-  $env:GOOS = $split[0]
-  $env:GOARCH = $split[1]
-  $env:CGO_ENABLED = 0
-
-  # send build product to GOOS_GOARCH subfolders
-  $targetOutDir = Join-Path -Path $outBaseDir -ChildPath "$($env:GOOS)_$($env:GOARCH)"
-
-  # special case to add file extensions
-  $extension = ""
-  if ($env:GOOS -eq "windows") {
-    $extension = ".exe"
-  }
-
-  # build binary and install only binary
-  go build -o "$($targetOutDir)\apc-p15-tool$($extension)" .\cmd\tool
-  go build -o "$($targetOutDir)\apc-p15-install$($extension)" .\cmd\install_only
-
-  # copy other important files for release
-  Copy-Item .\README.md $targetOutDir
-  Copy-Item .\CHANGELOG.md $targetOutDir
-  Copy-Item .\LICENSE.md $targetOutDir
-
-  # zip and drop into release folder
-  Compress-Archive -Path "$($targetOutDir)\*" -CompressionLevel Optimal -DestinationPath "$($outReleaseDir)\apc-p15-tool-$($gitTag)_$($target).zip" -Force
-}
diff --git a/build_release.py b/build_release.py
new file mode 100644
index 0000000..5f23be5
--- /dev/null
+++ b/build_release.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+import os.path
+import shutil
+import subprocess
+import tarfile
+
+# Configuration
+# output path (relative to this script)
+outRelativeDir = "_out"
+
+###
+
+# Script
+# relative dir is root
+scriptDir = dirname = os.path.dirname(__file__)
+outBaseDir = os.path.join(scriptDir, outRelativeDir)
+releaseDir = os.path.join(outBaseDir, "_release")
+
+# recreate paths
+if os.path.exists(outBaseDir):
+  shutil.rmtree(outBaseDir)
+os.makedirs(outBaseDir)
+os.makedirs(releaseDir)
+
+# get version number / tag
+gitTag = subprocess.check_output("git describe --tags --abbrev=0").decode('utf-8').strip()
+
+# # GOOS_GOARCH to build for
+targets = [
+   "windows_amd64",
+   "linux_amd64",
+   "linux_arm64",
+   "darwin_amd64",
+   "darwin_arm64",
+]
+
+# loop through and build all targets
+for target in targets:
+  # environment vars
+  split = target.split("_")
+  GOOS = split[0]
+  GOARCH = split[1]
+  os.environ["GOOS"] = GOOS
+  os.environ["GOARCH"] = GOARCH
+  os.environ["CGO_ENABLED"] = "0"
+
+  # send build product to GOOS_GOARCH subfolders
+  targetOutDir = os.path.join(outBaseDir, target)
+  if not os.path.exists(targetOutDir):
+    os.makedirs(targetOutDir)
+
+  # special case for windows to add file extensions
+  extension = ""
+  if GOOS.lower() == "windows":
+    extension = ".exe"
+
+  # build binary and install only binary
+  subprocess.run(f"go build -o {targetOutDir}/apc-p15-tool{extension} ./cmd/tool")
+  subprocess.run(f"go build -o {targetOutDir}/apc-p15-install{extension} ./cmd/install_only")
+
+  # copy other important files for release
+  shutil.copy("README.md", targetOutDir)
+  shutil.copy("CHANGELOG.md", targetOutDir)
+  shutil.copy("LICENSE.md", targetOutDir)
+
+  # compress release file
+  # special case for windows & mac to use zip format
+  if GOOS.lower() == "windows" or GOOS.lower() == "darwin":
+    shutil.make_archive(f"{releaseDir}/apc-p15-tool-{gitTag}_{target}", "zip", targetOutDir)
+  else:
+    # for others, use gztar and set permissions on the files
+
+    # filter for setting permissions
+    def set_permissions(tarinfo):
+      if tarinfo.name == "apc-p15-tool" or tarinfo.name == "apc-p15-install":
+        tarinfo.mode = 0o0755
+      else:
+        tarinfo.mode = 0o0644
+      return tarinfo
+
+    # make tar
+    with tarfile.open(f"{releaseDir}/apc-p15-tool-{gitTag}_{target}.tar.gz", "w:gz") as tar:
+        for file in os.listdir(targetOutDir):
+          tar.add(os.path.join(targetOutDir, file), arcname=file, recursive=False, filter=set_permissions)