-
-
Notifications
You must be signed in to change notification settings - Fork 38
Expand file tree
/
Copy pathsdout.py
More file actions
270 lines (233 loc) · 10.4 KB
/
sdout.py
File metadata and controls
270 lines (233 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
##################################################################################
# File: sdout.py — SDOut Package Builder
#
# Description:
# This script automates the creation of `sdout.zip`, a complete deployment
# package for Ultrahand Overlay. It downloads, organizes, and assembles
# Ultrahand Overlay components, nx-ovlloader, and any additional files into
# a proper SD card structure ready for use.
#
# The script automatically creates the required folder structure, downloads
# and extracts dependencies, organizes their contents, and packages everything
# into `sdout.zip` — which can be extracted directly to the root of an SD card.
#
# Related Projects:
# - Ultrahand Overlay: https://github.com/ppkantorski/Ultrahand-Overlay
# - nx-ovlloader: https://github.com/ppkantorski/nx-ovlloader
#
# For the latest updates or to contribute, visit the GitHub repository:
# https://github.com/ppkantorski/Ultrahand-Overlay
#
# Note:
# This notice is part of the official project documentation and must not
# be altered or removed.
#
# Requirements:
# - Python 3.6+
# - requests library (`pip install requests`)
# - `ovlmenu.ovl` file in the script directory
#
# Licensed under GPLv2
# Copyright (c) 2025-2026 ppkantorski
##################################################################################
import os
import shutil
import zipfile
import requests
from pathlib import Path
import tempfile
def download_file(url, destination):
"""Download a file from URL to destination"""
print(f"Downloading {url}...")
response = requests.get(url, stream=True)
response.raise_for_status()
with open(destination, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"Downloaded to {destination}")
def extract_zip(zip_path, extract_to, exclude_metadata=True):
"""Extract zip file, optionally excluding metadata files"""
print(f"Extracting {zip_path}...")
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
for member in zip_ref.namelist():
# Skip metadata files like ._ files and __MACOSX
if exclude_metadata:
if member.startswith('__MACOSX') or '._' in member:
continue
zip_ref.extract(member, extract_to)
print(f"Extracted to {extract_to}")
def create_zip_without_metadata(source_dir, output_zip):
"""Create a zip file excluding metadata files, preserving empty directories"""
print(f"Creating {output_zip}...")
with zipfile.ZipFile(output_zip, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(source_dir):
# Write an explicit entry for empty directories
if not files and not dirs:
dir_arcname = os.path.relpath(root, source_dir) + '/'
zipf.mkdir(dir_arcname)
continue
for file in files:
# Skip metadata files
if file.startswith('._') or file == '.DS_Store':
continue
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, source_dir)
zipf.write(file_path, arcname)
print(f"Created {output_zip}")
def main():
script_dir = Path.cwd()
sdout_dir = script_dir / "sdout"
sdout_zip = script_dir / "sdout.zip"
temp_dir = tempfile.mkdtemp()
try:
# Clean up any existing sdout folder and zip file
print("Cleaning up previous builds...")
if sdout_dir.exists():
shutil.rmtree(sdout_dir)
print("Deleted existing sdout folder")
if sdout_zip.exists():
sdout_zip.unlink()
print("Deleted existing sdout.zip")
# Step 1: Create sdout folder structure
print("Creating folder structure...")
folders = [
"config/ultrahand",
"config/ultrahand/downloads",
"config/ultrahand/flags",
"config/ultrahand/flags/notifications",
"config/ultrahand/lang",
"config/ultrahand/notifications",
"config/ultrahand/payloads",
"config/ultrahand/sounds",
"config/ultrahand/themes",
"config/ultrahand/wallpapers",
"switch/.overlays",
"switch/.packages",
"config/ultrahand/.sounds",
"config/ultrahand/assets",
"config/ultrahand/assets/notifications",
"atmosphere/exefs_patches/audio_mastervolume"
]
for folder in folders:
folder_path = sdout_dir / folder
folder_path.mkdir(parents=True, exist_ok=True)
print(f"Created {folder_path}")
# Step 2: Download and extract nx-ovlloader.zip
ovlloader_zip = Path(temp_dir) / "nx-ovlloader.zip"
download_file(
"https://github.com/ppkantorski/nx-ovlloader/releases/latest/download/nx-ovlloader.zip",
ovlloader_zip
)
extract_zip(ovlloader_zip, sdout_dir)
# Step 3: Download and process Ultrahand-Overlay
ultrahand_zip = Path(temp_dir) / "ultrahand-main.zip"
ultrahand_temp = Path(temp_dir) / "ultrahand_temp"
download_file(
"https://github.com/ppkantorski/Ultrahand-Overlay/archive/refs/heads/main.zip",
ultrahand_zip
)
extract_zip(ultrahand_zip, ultrahand_temp)
# Find the extracted folder (it will be Ultrahand-Overlay-main)
extracted_folders = [f for f in ultrahand_temp.iterdir() if f.is_dir()]
if not extracted_folders:
raise Exception("Could not find extracted Ultrahand folder")
ultrahand_root = extracted_folders[0]
# Step 4: Copy lang files
lang_source = ultrahand_root / "lang"
lang_dest = sdout_dir / "config/ultrahand/lang"
if lang_source.exists():
print("Copying language files...")
for json_file in lang_source.glob("*.json"):
shutil.copy2(json_file, lang_dest)
print(f"Copied {json_file.name}")
# Step 5: Copy ultrahand_updater.bin
payload_source = ultrahand_root / "payloads/ultrahand_updater.bin"
payload_dest = sdout_dir / "config/ultrahand/payloads"
if payload_source.exists():
print("Copying payload file...")
shutil.copy2(payload_source, payload_dest)
print(f"Copied ultrahand_updater.bin")
# Step 6: Copy theme files from the downloaded repository
print("Copying theme files...")
theme_source = ultrahand_root / "themes"
theme_dest = sdout_dir / "config/ultrahand/themes"
theme_files = ["ultra.ini", "ultra-blue.ini"]
if theme_source.exists():
for theme_file in theme_files:
theme_file_path = theme_source / theme_file
if theme_file_path.exists():
shutil.copy2(theme_file_path, theme_dest)
print(f"Copied {theme_file}")
else:
print(f"Warning: {theme_file} not found in themes folder")
else:
print("Warning: themes folder not found in Ultrahand repository")
# Step 7: Copy sound files
print("Copying sound files...")
sounds_source = ultrahand_root / "sounds"
sounds_dest = sdout_dir / "config/ultrahand/sounds"
if sounds_source.exists():
for wav_file in sounds_source.glob("*.wav"):
shutil.copy2(wav_file, sounds_dest)
print(f"Copied {wav_file.name}")
else:
print("Warning: sounds folder not found in Ultrahand repository")
# Step 8: Copy .sounds folder from repo root to config/ultrahand/.sounds
print("Copying .sounds folder...")
dot_sounds_source = ultrahand_root / ".sounds"
dot_sounds_dest = sdout_dir / "config/ultrahand/.sounds"
if dot_sounds_source.exists():
for f in dot_sounds_source.iterdir():
if f.is_file():
shutil.copy2(f, dot_sounds_dest)
print(f"Copied {f.name}")
else:
print("Warning: .sounds folder not found in Ultrahand repository")
# Step 8b: Copy assets folder from repo root to config/ultrahand/assets
print("Copying assets folder...")
assets_source = ultrahand_root / "assets"
assets_dest = sdout_dir / "config/ultrahand/assets"
if assets_source.exists():
for f in assets_source.iterdir():
if f.is_file():
shutil.copy2(f, assets_dest)
print(f"Copied {f.name}")
else:
print("Warning: assets folder not found in Ultrahand repository")
# Step 8c: Copy common/audio_mastervolume (local repo) to atmosphere/exefs_patches/audio_mastervolume
print("Copying audio_mastervolume patches...")
audio_mv_source = script_dir / "common/audio_mastervolume"
audio_mv_dest = sdout_dir / "atmosphere/exefs_patches/audio_mastervolume"
if audio_mv_source.exists():
for f in audio_mv_source.iterdir():
if f.is_file():
shutil.copy2(f, audio_mv_dest)
print(f"Copied {f.name}")
else:
print("Warning: common/audio_mastervolume folder not found in local repository")
# Step 9: Copy ovlmenu.ovl
print("Copying ovlmenu.ovl...")
ovlmenu_source = script_dir / "ovlmenu.ovl"
ovlmenu_dest = sdout_dir / "switch/.overlays"
if ovlmenu_source.exists():
shutil.copy2(ovlmenu_source, ovlmenu_dest)
print(f"Copied ovlmenu.ovl")
else:
print("Warning: ovlmenu.ovl not found in script directory")
# Step 10: Clean up temporary files
print("Cleaning up temporary files...")
shutil.rmtree(temp_dir)
print("Temporary files deleted")
# Step 11: Create final zip
output_zip = script_dir / "sdout.zip"
create_zip_without_metadata(sdout_dir, output_zip)
print("\n✓ Successfully created sdout.zip!")
print(f"Location: {output_zip}")
except Exception as e:
print(f"\n✗ Error: {e}")
# Clean up on error
if Path(temp_dir).exists():
shutil.rmtree(temp_dir)
raise
if __name__ == "__main__":
main()