Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
488a520
Add built executable
sumana-2705 Jun 28, 2025
ab8dc39
Remove built executable from repo and add PyInstaller .spec file
sumana-2705 Jul 3, 2025
feaf295
Update spec file.
JoeZiminski Jul 4, 2025
e93b848
Packaging with terminal working on Windows.
JoeZiminski Jul 22, 2025
b8284f3
Remove spec from gitignore.
JoeZiminski Jul 22, 2025
7caa5a2
Add macos packaging.
JoeZiminski Jul 24, 2025
ff61379
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 24, 2025
94cbea2
Tidying up on windows.
JoeZiminski Jul 25, 2025
f188a01
Working on windows.
JoeZiminski Jul 25, 2025
8b826e0
Working on macos but no installer yet.
JoeZiminski Jul 25, 2025
9ae5ded
Merge pull request #535 from sumana-2705/packaging_project
JoeZiminski Sep 2, 2025
ace948b
Start playing around with Linux.
JoeZiminski Sep 2, 2025
9e2e168
Start playing around with Linux 2.
JoeZiminski Sep 2, 2025
6685bf7
Start playing around with Linux 3.
JoeZiminski Sep 2, 2025
5b6a91c
Start playing around with Linux 4.
JoeZiminski Sep 2, 2025
a941e13
Start playing around with Linux 5.
JoeZiminski Sep 2, 2025
9c95587
Start playing around with Linux 6.
JoeZiminski Sep 2, 2025
3f1ef5c
Start playing around with Linux 7.
JoeZiminski Sep 2, 2025
acdaada
Start playing around with Linux 8.
JoeZiminski Sep 2, 2025
6d1c4d1
Working on linux.
JoeZiminski Sep 2, 2025
3848da1
Working on linux2
JoeZiminski Sep 2, 2025
a057e63
Merge branch 'cross-platform-packaging' of github.com:neuroinformatic…
JoeZiminski Sep 2, 2025
0313830
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 2, 2025
67af71d
More windows.
JoeZiminski Feb 24, 2026
5f06120
Windows package 2.
JoeZiminski Feb 24, 2026
1325762
Merge branch 'main' into cross-platform-packaging
JoeZiminski Feb 24, 2026
df22f0b
update ci.
JoeZiminski Feb 24, 2026
99ee8c5
Remove emojis.
JoeZiminski Feb 24, 2026
1fc6fc2
Remove emojis 2.
JoeZiminski Feb 24, 2026
22859aa
Make rclone search more general.
JoeZiminski Feb 24, 2026
a5cbb78
Fixes for ci.
JoeZiminski Feb 24, 2026
8936a62
Fix installer link.
JoeZiminski Feb 24, 2026
32240dc
Small tidy up.
JoeZiminski Feb 24, 2026
80d0f58
Remove linux.
JoeZiminski Feb 24, 2026
3efc710
Tidying up and finalising on Windows.
JoeZiminski Feb 25, 2026
9d75d86
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/package_windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: windows-build

on:
push:
branches: [ main ]
tags: [ "v*" ]
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build-windows:
runs-on: windows-latest

defaults:
run:
shell: powershell

steps:
- uses: actions/checkout@v4
- name: Set up Conda
uses: conda-incubator/setup-miniconda@v3
with:
python-version: ${{ matrix.python-version }}
auto-update-conda: true
channels: conda-forge
activate-environment: "datashuttle-test"

- name: Install rclone
run: |
conda activate datashuttle-test
conda install -c conda-forge rclone

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install .[dev]
pip install pyinstaller

# Install Inno Setup silently
- name: Install Inno Setup
run: |
choco install innosetup --yes

- name: Verify Inno Setup installation
run: |
Get-Command "C:\Program Files (x86)\Inno Setup 6\ISCC.exe"

- name: Run Windows packaging script
run: |
python package/package_windows.py

- name: Upload installer artifact
uses: actions/upload-artifact@v4
with:
name: windows-installer
path: package\Output\datashuttle_0.0.0.exe
Comment on lines +15 to +59

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

Copilot Autofix

AI 10 days ago

In general, to fix this class of problem you need to define a permissions block that explicitly restricts the GITHUB_TOKEN to the least privileges the workflow actually needs. You can add this either at the top (root) of the workflow to apply to all jobs, or under a specific job to limit only that job.

For this particular workflow, the build-windows job only checks out code and uploads an artifact; it doesn’t push code, modify releases, or interact with issues/PRs. These operations work with contents: read, and actions/upload-artifact does not require any repository write permissions. The best minimal fix is therefore to add a root-level permissions block just after the name: (or after on:) with contents: read. This documents the intent and ensures the token cannot be used to write to the repo even if organization defaults are broader.

Concretely:

  • Edit .github/workflows/package_windows.yml.
  • Insert a root-level permissions: mapping near the top of the file (e.g., after line 2 or after the on: block) with contents: read.
  • No other steps or actions in the shown workflow require additional scopes, so no further permissions are necessary.
Suggested changeset 1
.github/workflows/package_windows.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/package_windows.yml b/.github/workflows/package_windows.yml
--- a/.github/workflows/package_windows.yml
+++ b/.github/workflows/package_windows.yml
@@ -1,5 +1,8 @@
 name: windows-build
 
+permissions:
+  contents: read
+
 on:
   push:
     branches: [ main ]
EOF
@@ -1,5 +1,8 @@
name: windows-build

permissions:
contents: read

on:
push:
branches: [ main ]
Copilot is powered by AI and may make mistakes. Always verify output.
9 changes: 7 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
# Packaging
package/dist
package/build
package/_vendored
package/Output
package/inno_complie_script.iss

# python-dotenv environment file for test account credentials.
.env


# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down Expand Up @@ -35,7 +41,6 @@ MANIFEST
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
Expand Down
1 change: 1 addition & 0 deletions datashuttle/tui/css/tui_menu.tcss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#mainwindow_contents_container {
align: center top;
padding: 1 0 0 0;
overflow: hidden auto;
}
#mainwindow_contents_container > Button {
width: 50%;
Expand Down
5 changes: 4 additions & 1 deletion datashuttle/tui_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,7 @@ def main() -> None:


if __name__ == "__main__":
main()
try:
main()
except:
input("hello")
38 changes: 31 additions & 7 deletions datashuttle/utils/rclone.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,32 @@
import platform
import shlex
import subprocess
import sys
import tempfile

from datashuttle.configs import canonical_configs
from datashuttle.utils import rclone_encryption, utils


def get_command(command: str) -> str:
""" """
from pathlib import Path

if getattr(sys, "frozen", False):
# PyInstaller: binary extracted to _MEIPASS
if sys.platform == "win32":
format_command = (
f"{str(Path(sys._MEIPASS) / 'rclone.exe')} {command}"
)
else:
format_command = f"{sys._MEIPASS}/rclone {command}"
else:
# Normal Python execution: use PATH or fixed path
format_command = f"rclone {command}" # or provide full path if needed

return format_command


def call_rclone(command: str, pipe_std: bool = False) -> CompletedProcess:
"""Call rclone with the specified command.

Expand All @@ -49,13 +69,17 @@ def call_rclone(command: str, pipe_std: bool = False) -> CompletedProcess:
subprocess.CompletedProcess with `stdout` and `stderr` attributes.

"""
command = "rclone " + command
format_command = get_command(command)

if pipe_std:
output = subprocess.run(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
format_command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True,
)
else:
output = subprocess.run(command, shell=True)
output = subprocess.run(format_command, shell=True)

if output.returncode != 0:
prompt_rclone_download_if_does_not_exist()
Expand Down Expand Up @@ -101,18 +125,18 @@ def call_rclone_through_script_for_central_connection(
"""
system = platform.system()

command = "rclone " + command

if system == "Windows":
suffix = ".bat"
else:
suffix = ".sh"
command = "#!/bin/bash\n" + command

format_command = get_command(command)

with tempfile.NamedTemporaryFile(
mode="w", suffix=suffix, delete=False
) as tmp_script:
tmp_script.write(command)
tmp_script.write(format_command)
tmp_script_path = tmp_script.name

try:
Expand Down Expand Up @@ -594,7 +618,7 @@ def check_rclone_with_default_call() -> bool:
"""
try:
output = subprocess.run(
"rclone -h",
get_command("-h"),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True,
Expand Down
Binary file added package/NeuroBlueprint_icon.ico
Binary file not shown.
64 changes: 64 additions & 0 deletions package/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Packaging

Packaging datashuttle is slightly more complex than packaging a simple Python application,
because we need to vendor the terminal to run datashuttle in, because it can render strangely
depending on the platform (e.g. macOS terminal, older Windows terminals).

Wezterm, a cross-platform terminal that renders datashuttle well. It is vendored as part of
this distribution.

Packaging is a two-step process. First, we package datashuttle into an executable.
This executable contains all datashuttle dependencies and can be run from any terminal.

Next, a simple script ('terminal launcher') that runs the datashuttle executable in the vendored
Wezterm terminal is packaged using pyinstaller. This is the main entry point for the distribution.

Finally, this executable is distributed via an installer (inno setup on Windows, TODO on macos (`.dmg`)
This installs the executable in the proper place on the system, adds shortcuts etc.

So the call chain is:
1) The main executable is 'terminal launcher'
2) This opens the vendored Wezterm, and uses it to start the second, datashuttle executable.

## The `package` directory

The path handling is a little fiddly here, and some scrips are shared between platform.
Therefore, all materials are currently stored in this folder and not refactored into their own folder.

The key files are:

- **datashuttle.spec**: The pyinstaller command file that packages datashuttle, through `datashuttle_launcher.py` script.
This outputs a datashuttle executable that contains all datashuttle dependencies, and can be run through a terminal.
- **datashuttle_launcher.py**: A wrapper script that `datashuttle.spec` packages, that launches datashuttle.
- **license.txt**: The datashuttle license
- **package_<macos_or_windows>.py**: The entry point script that coordinates the packaging of datashuttle on macOS / Windows.
- **terminal_launcher_<macos_or_windows>.py**: The entry point script for the final datashuttle distribution.
It opens Wezterm and runs the datashuttle executable in it.
- **terminal_launcher_macos.spec**: The Pyinstaller command file that packages the `terminal_launcher` script into an executable.
- **wezterm_config.lua**: The Wezterm config. Use this to change settings that affect how the vendored Wezterm appears.
- **inno_compile_script.iss**: The Inno script used to package the Windows distribution into an installer. The output
of this script is shipped.
- **packaging_utils.py**: General utils used in the packaging scripts.

## Windows

When packaging on Windows, a number of intermediate folders will be created. Under normal circumstances
they will be deleted, but knowing their contents can be useful for debugging:

- **build**: Generated by pyinstaller, it contains intermediate files generating during the
packaging of `datashuttle` and `terminal_launcher`. Generally you do not need to inspect these files,
they are pyinstaller stuff.
- **dist**: Initially generated by pyinstaller, this is the complete distribution. During packaging,
this file is used for the Pyinstaller dist of `datashuttle` and `terminal_launcher`, then we
copy a few auxillary files (e.g. license, Wezterm config)). Because we are building two Pyinstaller
packages into one directory, some care had to be taken to avoid configs. The folders included are:
1) `datashuttle`: The datashuttle distribution generated by Pyinstaller
2) `terminal_launcher.exe` and `_internal` is the `terminal_launcher` distribution generated by Pyinstaller.
3) `_vendored` is our vendor folder, that contains the vendored Wezterm temrinal.
4) datashuttle license and icon
- **Output**: This is the output of Inno Setup, containing the installer. This is the final version to be shipped.




## macOS
1 change: 1 addition & 0 deletions package/TODO.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DONT FORGET TO INCLUDE THE LICENSE
79 changes: 79 additions & 0 deletions package/datashuttle.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# main.spec
# -*- mode: python ; coding: utf-8 -*-
import os
import sys
import platform
from glob import glob
from pathlib import Path
import shutil

# Include .tcss files
tcss_files = [
(f, os.path.join("datashuttle", "tui", "css"))
for f in glob("../datashuttle/tui/css/*.tcss")
]

# Get current conda environment prefix
env_prefix = sys.prefix

# Detect OS and set rclone path

rclone_src = shutil.which("rclone")

if rclone_src is None:
raise FileNotFoundError(
"rclone not found in PATH. Ensure it is installed before running PyInstaller."
)

binaries = [(rclone_src, ".")]

a = Analysis(
['datashuttle_launcher.py'], # terminal_launcher
pathex=[os.path.abspath('..')],
binaries=binaries,
datas=tcss_files,
hiddenimports=[
'datashuttle.tui_launcher',
'datashuttle.tui.app',
'textual.widgets._tab_pane',
'textual.widgets._input',
'textual.widgets._tree_control',
'rich._unicode_data.unicode17-0-0'
],
hookspath=['hooks'],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)

pyz = PYZ(a.pure)

exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='datashuttle',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='datashuttle'
)
24 changes: 24 additions & 0 deletions package/datashuttle_launcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import sys
from pathlib import Path

# Add project root to sys.path
project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root))

# Import main from tui_launcher
from datashuttle.tui_launcher import main as datashuttle_main


def run():
# Simulate: datashuttle launch
sys.argv = ["datashuttle", "launch"]
datashuttle_main()


if __name__ == "__main__":
try:
run()
except Exception as e:
print(f"\nError: {e}")
finally:
input("\nPress Enter to exit...")
1 change: 1 addition & 0 deletions package/license.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello world
Loading
Loading