CTkDeb 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ctkdeb/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .packager import Packager
2
+ from .main import show_supported_rpm_distros
ctkdeb/gui.py ADDED
@@ -0,0 +1,361 @@
1
+ import customtkinter as ctk
2
+ from tkinter import messagebox, filedialog, PhotoImage
3
+ import os
4
+ import sys
5
+ import json
6
+ from threading import Thread
7
+
8
+ from . import packager
9
+
10
+ PATH = os.path.dirname(os.path.realpath(__file__))
11
+
12
+ SUPPORTED = {
13
+ "DEB": "deb",
14
+ "RPM (Fedora)": "fedora",
15
+ "RPM (Mageia)": "mageia",
16
+ "RPM (OpenMandriva)": "openmandriva"
17
+ }
18
+
19
+ class CTkDeb(ctk.CTk):
20
+ def __init__(self):
21
+ super().__init__()
22
+
23
+ self.appearance_mode_var = ctk.StringVar(value="light")
24
+ self.update_appearance_mode()
25
+
26
+ self.title("CTkDeb")
27
+ self.geometry("500x600")
28
+
29
+ self.wm_iconbitmap()
30
+ iconpath = PhotoImage(file=os.path.join(PATH, "icon.png"))
31
+ self.iconphoto(False, iconpath)
32
+
33
+ self.title_font = ctk.CTkFont(size=18)
34
+
35
+ self.fields = {
36
+ "app": {
37
+ "title": "App",
38
+ "items": [
39
+ {"id": "name", "label": "Name", "placeholder": "myapp"},
40
+ {"id": "version", "label": "Version", "placeholder": "0.16.2"},
41
+ {"id": "entryPoint", "label": "Entry point", "placeholder": "main.py"},
42
+ {"id": "icon", "label": "Icon", "placeholder": "assets/icon.png"},
43
+ {"id": "iconSize", "label": "Icon size", "placeholder": "512x512"},
44
+ {"id": "pipDependencies", "label": "Pip dependencies", "placeholder": "somelib potatolib"}
45
+ ]
46
+ },
47
+ "desktopEntry": {
48
+ "title": "Desktop entry",
49
+ "items": [
50
+ {"type": "button", "text": "Add field", "command": self.add_new_desktop_entry_field},
51
+ {"id": "Name", "label": "Name", "placeholder": "MyApp"},
52
+ {"id": "Comment", "label": "Comment", "placeholder": "A simple alarm clock app"}
53
+ ]
54
+ },
55
+ "documentationFiles": {
56
+ "title": "Documentation files (licenses, README.md etc.)",
57
+ "items": [
58
+ {"type": "button", "text": "Add file", "command": self.add_new_doc_file}
59
+ ]
60
+ },
61
+ "excluded": {
62
+ "title": "Excluded files and folders",
63
+ "items": [
64
+ {"type": "button", "text": "Add file or folder", "command": self.add_new_excluded}
65
+ ]
66
+ },
67
+ "deb": {
68
+ "title": "DEB-specific settings",
69
+ "items": [
70
+ {"id": "section", "label": "Section", "placeholder": "graphics"},
71
+ {"id": "depends", "label": "Depends", "placeholder": "somelib, potatolib"},
72
+ {"id": "maintainer", "label": "Maintainer", "placeholder": "Ivan Ivanov <ivan_dev@example.com>"},
73
+ {"id": "description", "label": "Description", "placeholder": "A simple alarm clock app"},
74
+ ]
75
+ },
76
+ "rpm": {
77
+ "title": "RPM-specific settings",
78
+ "items": [
79
+ {"id": "summary", "label": "Summary", "placeholder": "A simple alarm clock app"},
80
+ {"id": "license", "label": "License", "placeholder": "MIT and GPL-3.0-or-later"},
81
+ {"id": "url", "label": "URL", "placeholder": "https://myapp.com"},
82
+ {"id": "description", "label": "Description",
83
+ "placeholder": "MyApp is a simple alarm clock app with a user-friendly interface."}
84
+ ]
85
+ },
86
+ "rpmRequires": {
87
+ "title": "RPM requires",
88
+ "items": [
89
+ {"id": "fedora", "label": "Fedora", "placeholder": "somelib potatolib"},
90
+ {"id": "mageia", "label": "Mageia", "placeholder": "libsome potato-library"},
91
+ {"id": "openmandriva", "label": "OpenMandriva", "placeholder": "some-lib potatolib3"}
92
+ ]
93
+ }
94
+ }
95
+ self.entries = {}
96
+ self.desktop_entry_entries = {}
97
+ self.doc_entries = []
98
+ self.excluded_entries = []
99
+
100
+ self.build_interface()
101
+
102
+ def update_appearance_mode(self):
103
+ ctk.set_appearance_mode(self.appearance_mode_var.get())
104
+
105
+ def build_interface(self):
106
+ self.create_header()
107
+ self.create_cards()
108
+ self.create_footer()
109
+
110
+ def create_header(self):
111
+ header = ctk.CTkFrame(self)
112
+ header.pack(padx=5, pady=5, side=ctk.TOP, fill=ctk.X)
113
+
114
+ ctk.CTkLabel(header, text="CTkDeb", font=self.title_font).pack(padx=10, pady=10, side=ctk.LEFT)
115
+
116
+ appearance_switch = ctk.CTkSwitch(header, text="Dark", variable=self.appearance_mode_var,
117
+ offvalue="light", onvalue="dark", command=self.update_appearance_mode)
118
+ appearance_switch.pack(padx=10, pady=10, side=ctk.RIGHT)
119
+
120
+ def create_footer(self):
121
+ footer = ctk.CTkFrame(self)
122
+ footer.pack(padx=5, pady=5, side=ctk.BOTTOM, fill=ctk.X)
123
+
124
+ config_frame = ctk.CTkFrame(footer)
125
+ config_frame.pack(side=ctk.LEFT, fill=ctk.X, expand=True)
126
+
127
+ import_config_button = ctk.CTkButton(config_frame, text="Import config", command=self.import_config)
128
+ import_config_button.pack(padx=10, pady=10, fill=ctk.X, expand=True)
129
+
130
+ export_config_button = ctk.CTkButton(config_frame, text="Export config", command=self.export_config)
131
+ export_config_button.pack(padx=10, pady=10, fill=ctk.X, expand=True)
132
+
133
+ create_frame = ctk.CTkFrame(footer)
134
+ create_frame.pack(side=ctk.RIGHT, fill=ctk.X, expand=True)
135
+
136
+ self.create_optionmenu = ctk.CTkOptionMenu(create_frame, values=list(SUPPORTED.keys()),
137
+ dynamic_resizing=False)
138
+ self.create_optionmenu.pack(padx=10, pady=10, fill=ctk.X, expand=True)
139
+
140
+ self.create_button = ctk.CTkButton(create_frame, text="CREATE", command=self.on_create_clicked)
141
+ self.create_button.pack(padx=10, pady=10, fill=ctk.X, expand=True)
142
+
143
+ def create_cards(self):
144
+ scrollable = ctk.CTkScrollableFrame(self, fg_color="transparent")
145
+ scrollable.pack(padx=5, fill=ctk.BOTH, expand=True)
146
+
147
+ for section, items in self.fields.items():
148
+ self.entries[section] = {}
149
+
150
+ card = ctk.CTkFrame(scrollable)
151
+ card.pack(padx=10, pady=10, fill=ctk.X)
152
+
153
+ card.grid_columnconfigure(1, weight=1)
154
+
155
+ if section == "desktopEntry":
156
+ self.desktop_entry_card = card
157
+ elif section == "documentationFiles":
158
+ self.docs_card = card
159
+ elif section == "excluded":
160
+ self.excluded_card = card
161
+
162
+ title = ctk.CTkLabel(card, text=items["title"], font=self.title_font)
163
+ title.grid(row=0, column=0, padx=10, pady=10, columnspan=2, sticky=ctk.W)
164
+
165
+ for row, widget in enumerate(items["items"], start=1):
166
+ self.create_widget(section, card, widget, row)
167
+
168
+ def create_widget(self, section: str, card: ctk.CTkEntry, widget: dict, row: int):
169
+ if widget.get("type") == "button":
170
+ button = ctk.CTkButton(card, text=widget["text"], command=widget["command"])
171
+ button.grid(row=row, column=0, padx=10, pady=(0, 10), columnspan=2, sticky=ctk.EW)
172
+ else:
173
+ if packager.REQUIRED_FIELDS.get(section) and widget["id"] in packager.REQUIRED_FIELDS.get(section):
174
+ characters = "*:"
175
+ else:
176
+ characters = ":"
177
+
178
+ entry = self.add_labeled_entry(card, widget["label"] + characters, widget["placeholder"], row)
179
+ self.entries[section][widget["id"]] = entry
180
+
181
+ def add_labeled_entry(
182
+ self,
183
+ card: ctk.CTkFrame,
184
+ label_text: str,
185
+ placeholder: str,
186
+ row: int,
187
+ entry_type: bool = False
188
+ ):
189
+ if entry_type:
190
+ label = ctk.CTkEntry(card, placeholder_text=label_text)
191
+ else:
192
+ label = ctk.CTkLabel(card, text=label_text)
193
+ label.grid(row=row, column=0, padx=10, pady=(0, 10), sticky=ctk.W)
194
+
195
+ entry = ctk.CTkEntry(card, placeholder_text=placeholder)
196
+ entry.grid(row=row, column=1, padx=10, pady=(0, 10), sticky=ctk.EW)
197
+
198
+ if entry_type:
199
+ return label, entry
200
+ else:
201
+ return entry
202
+
203
+ def add_new_desktop_entry_field(self) -> ctk.CTkEntry:
204
+ next_row = self.desktop_entry_card.grid_size()[1]
205
+
206
+ entry = self.add_labeled_entry(self.desktop_entry_card, "Categories", "Application;Utility;", next_row, True)
207
+
208
+ return entry
209
+
210
+ def add_new_doc_file(self):
211
+ self.add_new_resource(self.docs_card, "File:", "LICENSE", self.doc_entries)
212
+
213
+ def add_new_excluded(self):
214
+ self.add_new_resource(self.excluded_card, "Item:", ".git", self.excluded_entries)
215
+
216
+ def add_new_resource(self, card: ctk.CTkFrame, label: str, placeholder: str, entries_list: list):
217
+ next_row = card.grid_size()[1]
218
+
219
+ entry = self.add_labeled_entry(card, label, placeholder, next_row)
220
+ entries_list.append(entry)
221
+
222
+ def import_config(self):
223
+ file_path = filedialog.askopenfilename(
224
+ title="Import config",
225
+ filetypes=([("JSON", "*.json")]),
226
+ defaultextension=".json"
227
+ )
228
+ if not file_path:
229
+ return
230
+
231
+ with open(file_path, "r", encoding="utf-8") as f:
232
+ config = json.load(f)
233
+
234
+ self.destroy_user_widgets()
235
+
236
+ # Regular fields
237
+ for section, entries in self.entries.items():
238
+ if section in config:
239
+ for field_id, entry in entries.items():
240
+ if field_id in config[section]:
241
+ self.entry_set_text(entry, config[section][field_id])
242
+
243
+ # Desktop entry
244
+ for key, value in config["desktopEntry"].items():
245
+ if key in self.entries["desktopEntry"]:
246
+ self.entry_set_text(self.entries["desktopEntry"][key], value)
247
+ else:
248
+ label, entry = self.add_new_desktop_entry_field()
249
+ self.entry_set_text(label, key)
250
+ self.entry_set_text(entry, value)
251
+
252
+ # Documentation files
253
+ for value in config["documentationFiles"]:
254
+ self.add_new_doc_file()
255
+ self.entry_set_text(self.doc_entries[-1], value)
256
+
257
+ # Excluded files and folders
258
+ if config.get("excluded"):
259
+ for value in config["excluded"]:
260
+ self.add_new_excluded()
261
+ self.entry_set_text(self.excluded_entries[-1], value)
262
+
263
+ messagebox.showinfo("Config imported", "Config successfully imported")
264
+
265
+ def get_config_dict(self) -> dict:
266
+ config = {}
267
+
268
+ # Regular fields
269
+ for section, entries in self.entries.items():
270
+ config[section] = {}
271
+ for field_id, entry in entries.items():
272
+ value = entry.get()
273
+ if value:
274
+ config[section][field_id] = value
275
+
276
+ # Desktop entry
277
+ for child in self.desktop_entry_card.winfo_children():
278
+ info = child.grid_info()
279
+ row, col = info["row"], info["column"]
280
+
281
+ if col == 0 and isinstance(child, ctk.CTkEntry):
282
+ key = child.get()
283
+ if not key:
284
+ continue
285
+
286
+ for other in self.desktop_entry_card.winfo_children():
287
+ other_info = other.grid_info()
288
+ if other_info["row"] == row and other_info["column"] == 1:
289
+ config["desktopEntry"][key] = other.get()
290
+
291
+ config["documentationFiles"] = [entry.get() for entry in self.doc_entries if entry.get()]
292
+ config["excluded"] = [entry.get() for entry in self.excluded_entries if entry.get()]
293
+
294
+ return config
295
+
296
+ def export_config(self):
297
+ config = self.get_config_dict()
298
+
299
+ file_path = filedialog.asksaveasfilename(
300
+ title="Export config",
301
+ filetypes=([("JSON", "*.json")]),
302
+ defaultextension=".json",
303
+ initialfile="ctkdeb.json"
304
+ )
305
+ if file_path:
306
+ with open(file_path, "w", encoding="utf-8") as f:
307
+ json.dump(config, f, indent="\t", ensure_ascii=False)
308
+ messagebox.showinfo("Config exported", "Config successfully exported")
309
+
310
+ def entry_set_text(self, entry: ctk.CTkEntry, value: str):
311
+ entry.delete(0, ctk.END)
312
+ entry.insert(0, value)
313
+
314
+ def destroy_user_widgets(self):
315
+ # Desktop entry
316
+ for child in self.desktop_entry_card.winfo_children():
317
+ info = child.grid_info()
318
+ row, col = info["row"], info["column"]
319
+
320
+ if col == 0 and isinstance(child, ctk.CTkEntry):
321
+ for other in self.desktop_entry_card.winfo_children():
322
+ if other.grid_info()["row"] == row:
323
+ other.destroy()
324
+
325
+ # Documentation files
326
+ for entry in self.doc_entries:
327
+ entry.destroy()
328
+ self.doc_entries.clear()
329
+
330
+ def on_create_clicked(self):
331
+ self.progressbar = ctk.CTkProgressBar(self, mode="determinate")
332
+ self.progressbar.pack(padx=5, pady=5, fill=ctk.X)
333
+ self.progressbar.start()
334
+
335
+ self.create_button.configure(state="disabled")
336
+
337
+ thread = Thread(target=self.create_package)
338
+ thread.start()
339
+
340
+ def create_package(self):
341
+ value = self.create_optionmenu.get()
342
+ value_name = SUPPORTED[value]
343
+
344
+ config = self.get_config_dict()
345
+
346
+ p = packager.Packager(config)
347
+
348
+ if value_name == "deb":
349
+ p.create_deb()
350
+ elif value_name in packager.RPM_REQUIRES.keys():
351
+ p.create_rpm(value_name)
352
+
353
+ self.after(0, self.on_package_created, value)
354
+
355
+ def on_package_created(self, value):
356
+ self.progressbar.stop()
357
+ self.progressbar.destroy()
358
+
359
+ self.create_button.configure(state="normal")
360
+
361
+ messagebox.showinfo(f"{value} created", f"{value} successfully created")
ctkdeb/icon.png ADDED
Binary file
ctkdeb/main.py ADDED
@@ -0,0 +1,45 @@
1
+ from .packager import RPM_REQUIRES
2
+
3
+ def cli():
4
+ import argparse
5
+ from .packager import Packager
6
+
7
+ parser = argparse.ArgumentParser(
8
+ prog="CTkDeb",
9
+ description="A tool for easily packaging CustomTkinter projects into DEB and RPM"
10
+ )
11
+ parser.add_argument("json", type=str, help="Path to the JSON config")
12
+ parser.add_argument("-c", "--create", required=True, choices=["deb"] + list(RPM_REQUIRES.keys()))
13
+ parser.add_argument("-pf", "--project-folder", type=str,
14
+ help="Path to the project folder (if the command is not executed from it)")
15
+ parser.add_argument("-d", "--desktop-entry", type=str,
16
+ help="Path to the custom desktop entry (or its text)")
17
+ parser.add_argument("-of", "--output-folder", type=str, help="Path to the output folder")
18
+
19
+ args = parser.parse_args()
20
+
21
+ packager = Packager(
22
+ args.json,
23
+ project_folder=args.project_folder,
24
+ desktop_entry=args.desktop_entry,
25
+ output_folder=args.output_folder
26
+ )
27
+
28
+ if args.create == "deb":
29
+ packager.create_deb()
30
+ elif args.create in RPM_REQUIRES.keys():
31
+ packager.create_rpm(args.create)
32
+
33
+ def gui():
34
+ from .gui import CTkDeb
35
+
36
+ app = CTkDeb()
37
+ app.mainloop()
38
+
39
+ def show_supported_rpm_distros():
40
+ print("Supported RPM distros:")
41
+ for distro in RPM_REQUIRES.keys():
42
+ print(f"- '{distro}'")
43
+
44
+ if __name__ == "__main__":
45
+ cli()
ctkdeb/packager.py ADDED
@@ -0,0 +1,327 @@
1
+ import json
2
+ from pathlib import Path
3
+ import shutil
4
+ import tempfile
5
+ import tarfile
6
+ import subprocess
7
+
8
+ REQUIRED_FIELDS = {
9
+ "app": ["name", "version", "entryPoint", "icon", "iconSize"],
10
+ "desktopEntry": ["Name"]
11
+ }
12
+
13
+ RPM_REQUIRES = {
14
+ "fedora": "python3 python3-tkinter",
15
+ "mageia": "python3 tkinter3",
16
+ "openmandriva": "python python-customtkinter"
17
+ }
18
+
19
+ def _pip_install_to_dir(target_dir: Path, dependencies: str):
20
+ packages = dependencies.split()
21
+
22
+ cmd = ["pip", "install", "--target", str(target_dir)] + packages
23
+ subprocess.run(cmd)
24
+
25
+ # Clean garbage
26
+ for p in target_dir.rglob("*.py[co]"):
27
+ p.unlink()
28
+ for p in target_dir.rglob("__pycache__"):
29
+ shutil.rmtree(p)
30
+
31
+ class Packager:
32
+ def __init__(
33
+ self,
34
+ json_path: str | dict,
35
+ project_folder: str | None = None,
36
+ desktop_entry: str | None = None,
37
+ output_folder: str | None = None
38
+ ):
39
+ if isinstance(json_path, str):
40
+ with open(json_path, "r", encoding="utf-8") as f:
41
+ self.json = json.load(f)
42
+ elif isinstance(json_path, dict):
43
+ self.json = json_path
44
+ else:
45
+ raise TypeError("'json_path' can only be str or dict.")
46
+
47
+ self._check_required_fields()
48
+
49
+ if project_folder:
50
+ self.project_folder = Path(project_folder)
51
+ else:
52
+ self.project_folder = Path.cwd()
53
+
54
+ if desktop_entry:
55
+ try:
56
+ if Path(desktop_entry).exists():
57
+ with open(desktop_entry, "r", encoding="utf-8") as f:
58
+ self.desktop_entry = f.read()
59
+ else:
60
+ self.desktop_entry = desktop_entry
61
+ except OSError:
62
+ self.desktop_entry = desktop_entry
63
+ else:
64
+ self.desktop_entry = self._create_desktop_entry()
65
+
66
+ if output_folder:
67
+ self.output_folder = output_folder
68
+ else:
69
+ self.output_folder = Path.cwd()
70
+
71
+ def _check_required_fields(self):
72
+ for section, fields in REQUIRED_FIELDS.items():
73
+ if section not in self.json:
74
+ raise ValueError(f"Section '{section}' is missing")
75
+
76
+ for field in fields:
77
+ if not self.json[section].get(field):
78
+ raise ValueError(f"Field '{section}.{field}' is missing or empty")
79
+
80
+ def _create_desktop_entry(self) -> str:
81
+ desktop_entry_dict = {
82
+ "[Desktop Entry]": None,
83
+ "Type": "Application",
84
+ "Version": "1.0",
85
+ "Name": self.json["desktopEntry"]["Name"],
86
+ "Exec": f"python3 /opt/{self.project_folder.name}/{self.json['app']['entryPoint']} %f",
87
+ "Icon": self.json["app"]["name"],
88
+ "Terminal": "false"
89
+ }
90
+
91
+ for key, value in self.json["desktopEntry"].items():
92
+ desktop_entry_dict[key] = value
93
+
94
+ result_lines = []
95
+ for key, value in desktop_entry_dict.items():
96
+ if value is None:
97
+ result_lines.append(key) # for title
98
+ else:
99
+ result_lines.append(f"{key}={value}")
100
+
101
+ return "\n".join(result_lines)
102
+
103
+ def create_deb(self):
104
+ tmp_dir = tempfile.mkdtemp()
105
+
106
+ package_dir = Path(tmp_dir) / self.json["app"]["name"]
107
+ package_dir.mkdir()
108
+
109
+ share_dir = package_dir / "usr" / "share"
110
+ share_dir.mkdir(parents=True)
111
+
112
+ print("Copying app files...")
113
+
114
+ opt_dir = package_dir / "opt"
115
+ copy_path = opt_dir / self.project_folder.name
116
+
117
+ shutil.copytree(self.project_folder, copy_path)
118
+ self.delete_excluded_items(copy_path)
119
+
120
+ print("Copying the icon...")
121
+
122
+ apps_icons_dir = share_dir / "icons" / "hicolor" / self.json["app"]["iconSize"] / "apps"
123
+ apps_icons_dir.mkdir(parents=True)
124
+
125
+ shutil.copy(self.project_folder / self.json["app"]["icon"], apps_icons_dir / self._new_icon_name())
126
+
127
+ print("Creating the desktop entry...")
128
+
129
+ applications_dir = share_dir / "applications"
130
+ applications_dir.mkdir()
131
+
132
+ with open(applications_dir / (self.json["app"]["name"] + ".desktop"), "w", encoding="utf-8") as f:
133
+ f.write(self.desktop_entry)
134
+
135
+ print("Copying docs...")
136
+
137
+ app_doc_dir = share_dir / "doc" / self.json["app"]["name"]
138
+ app_doc_dir.mkdir(parents=True)
139
+
140
+ for f in self.json["documentationFiles"]:
141
+ shutil.copy(self.project_folder / f, app_doc_dir)
142
+
143
+ print("Install CustomTkinter and pip dependencies...")
144
+
145
+ dependencies = "customtkinter"
146
+ if self.json["app"].get("pipDependencies"):
147
+ dependencies += " " + self.json["app"]["pipDependencies"]
148
+
149
+ _pip_install_to_dir(self._pip_target_dir(opt_dir), dependencies)
150
+
151
+ print("Estimating package dir size...")
152
+
153
+ size_bytes = sum(f.stat().st_size for f in package_dir.rglob("*") if f.is_file())
154
+ size_kb = size_bytes // 1024
155
+
156
+ print("Creating control file...")
157
+
158
+ debian_dir = package_dir / "DEBIAN"
159
+ debian_dir.mkdir()
160
+
161
+ depends = "python3, python3-tk"
162
+ if self.json["deb"].get("depends"):
163
+ depends += ", " + self.json["deb"]["depends"]
164
+
165
+ deb_control = f"""Package: {self.json['app']['name']}
166
+ Version: {self.json['app']['version']}
167
+ Section: {self.json['deb'].get('section') or 'unknown'}
168
+ Priority: optional
169
+ Depends: {depends}
170
+ Architecture: all
171
+ Essential: no
172
+ Installed-Size: {size_kb}
173
+ Maintainer: {self.json['deb'].get('maintainer') or 'Unknown <unknown@example.com>'}
174
+ Description: {self.json['deb'].get('description') or 'Packaged via CTkDeb'}
175
+ """
176
+
177
+ with open(debian_dir / "control", "w", encoding="utf-8") as f:
178
+ f.write(deb_control)
179
+
180
+ print("Creating DEB package...")
181
+
182
+ subprocess.run(["dpkg-deb", "--build", package_dir])
183
+
184
+ print("Moving DEB and removing tmp dir...")
185
+
186
+ shutil.move(Path(tmp_dir) / (self.json["app"]["name"] + ".deb"), self.output_folder)
187
+ shutil.rmtree(tmp_dir)
188
+
189
+ print("DEB successfully created!")
190
+
191
+ def create_rpm(self, distribution: str):
192
+ supported_distros = RPM_REQUIRES.keys()
193
+ if distribution not in supported_distros:
194
+ raise ValueError(f"Only {' '.join(supported_distros)} are supported.")
195
+
196
+ tmp_dir = tempfile.mkdtemp()
197
+
198
+ package_dir = Path(tmp_dir) / self.json["app"]["name"]
199
+ package_dir.mkdir()
200
+
201
+ print("Creating SPEC file...")
202
+
203
+ requires = RPM_REQUIRES[distribution]
204
+ if self.json["rpmRequires"].get(distribution):
205
+ requires += " " + self.json["rpmRequires"][distribution]
206
+
207
+ docs_files_names = [Path(f).name for f in self.json["documentationFiles"]]
208
+
209
+ spec = f"""Name: {self.json['app']['name']}
210
+ Version: {self.json['app']['version']}
211
+ Release: 1.{distribution}
212
+ Summary: {self.json['rpm'].get('summary') or 'Packaged via CTkDeb'}
213
+ License: {self.json['rpm'].get('license') or 'Unknown'}
214
+ Source0: {self.json['app']['name']}.tar
215
+ BuildArch: noarch
216
+ URL: {self.json['rpm'].get('url') or 'https://example.com'}
217
+ Requires: {requires}
218
+
219
+ %description
220
+ {self.json['rpm'].get('description') or 'This app is packaged via CTkDeb.'}
221
+
222
+ %prep
223
+ %setup -q -n {self.json['app']['name']}
224
+
225
+ %install
226
+ mkdir -p %{{buildroot}}/opt
227
+ cp -r {self.project_folder.name} %{{buildroot}}/opt
228
+
229
+ mkdir -p %{{buildroot}}/usr/share/icons/hicolor/{self.json['app']['iconSize']}/apps
230
+ cp {self._new_icon_name()} %{{buildroot}}/usr/share/icons/hicolor/{self.json['app']['iconSize']}/apps
231
+
232
+ mkdir -p %{{buildroot}}/usr/share/applications
233
+ cp {self.json['app']['name']}.desktop %{{buildroot}}/usr/share/applications
234
+ {f"""
235
+ mkdir -p %{{buildroot}}/usr/share/doc/{self.json['app']['name']}
236
+ cp {' '.join(docs_files_names)} %{{buildroot}}/usr/share/doc/{self.json['app']['name']}
237
+ """ if docs_files_names else ''}
238
+ %files
239
+ /opt/{self.project_folder.name}
240
+ /usr/share/icons/hicolor/{self.json['app']['iconSize']}/apps/{self._new_icon_name()}
241
+ /usr/share/applications/{self.json['app']['name']}.desktop
242
+ {''.join(f"/usr/share/doc/{self.json['app']['name']}/{f}\n" for f in docs_files_names)
243
+ if docs_files_names else ''}"""
244
+
245
+ with open(package_dir / (self.json["app"]["name"] + ".spec"), "w", encoding="utf-8") as f:
246
+ f.write(spec)
247
+
248
+ print("Creating the desktop entry...")
249
+
250
+ with open(package_dir / (self.json["app"]["name"] + ".desktop"), "w", encoding="utf-8") as f:
251
+ f.write(self.desktop_entry)
252
+
253
+ print("Copying app files...")
254
+
255
+ copy_path = package_dir / self.project_folder.name
256
+
257
+ shutil.copytree(self.project_folder, copy_path)
258
+ self.delete_excluded_items(copy_path)
259
+
260
+ print("Copying the icon...")
261
+
262
+ shutil.copy(self.project_folder / self.json["app"]["icon"], package_dir / self._new_icon_name())
263
+
264
+ print("Copying docs...")
265
+
266
+ for f in self.json["documentationFiles"]:
267
+ shutil.copy(self.project_folder / f, package_dir)
268
+
269
+ print("Install CustomTkinter and pip dependencies...")
270
+
271
+ dependencies = []
272
+
273
+ if distribution != "openmandriva":
274
+ dependencies.append("customtkinter")
275
+
276
+ if self.json["app"].get("pipDependencies"):
277
+ dependencies.extend(self.json["app"]["pipDependencies"].split())
278
+
279
+ if dependencies:
280
+ _pip_install_to_dir(self._pip_target_dir(package_dir), " ".join(dependencies))
281
+
282
+ print("Creating tarball...")
283
+
284
+ tar_name = Path(tmp_dir) / (self.json["app"]["name"] + ".tar")
285
+
286
+ with tarfile.open(tar_name, "w") as f:
287
+ f.add(package_dir, arcname=self.json["app"]["name"])
288
+
289
+ print("Creating RPM package...")
290
+
291
+ subprocess.run(["rpmbuild", "-tb", tar_name, "--define", "_topdir " + tmp_dir])
292
+
293
+ print("Moving RPM and removing tmp dir...")
294
+
295
+ for f in (Path(tmp_dir) / "RPMS" / "noarch").glob("*.rpm"):
296
+ shutil.move(str(f), self.output_folder)
297
+
298
+ shutil.rmtree(tmp_dir)
299
+
300
+ print("RPM successfully created!")
301
+
302
+ def _new_icon_name(self) -> str:
303
+ icon_path = Path(self.json["app"]["icon"])
304
+ return self.json["app"]["name"] + icon_path.suffix
305
+
306
+ def _pip_target_dir(self, app_files_dir_parent: Path) -> Path:
307
+ entry_point = Path(self.json["app"]["entryPoint"])
308
+
309
+ if len(entry_point.parts) == 1:
310
+ d = app_files_dir_parent / self.project_folder.name
311
+ else:
312
+ d = app_files_dir_parent / self.project_folder.name / entry_point.parent
313
+
314
+ return d
315
+
316
+ def delete_excluded_items(self, path: Path):
317
+ if not self.json.get("excluded"):
318
+ return
319
+
320
+ for name in self.json["excluded"]:
321
+ target = path / name
322
+
323
+ if target.exists():
324
+ if target.is_file():
325
+ target.unlink()
326
+ elif target.is_dir():
327
+ shutil.rmtree(target)
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: CTkDeb
3
+ Version: 0.7.0
4
+ Summary: A tool for easily packaging CustomTkinter projects into DEB and RPM
5
+ Author: limafresh
6
+ Maintainer: limafresh
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/limafresh/CTkDeb
9
+ Project-URL: Documentation, https://github.com/limafresh/CTkDeb
10
+ Project-URL: Repository, https://github.com/limafresh/CTkDeb.git
11
+ Project-URL: Issues, https://github.com/limafresh/CTkDeb/issues
12
+ Project-URL: Changelog, https://github.com/limafresh/CTkDeb/blob/main/CHANGELOG.md
13
+ Keywords: gui,customtkinter,deb,rpm,linux,package
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Natural Language :: English
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: gui
24
+ Requires-Dist: customtkinter; extra == "gui"
25
+ Dynamic: license-file
26
+
27
+ # CTkDeb
28
+
29
+ ![Static Badge](https://img.shields.io/badge/No-PyInstaller-blue?style=for-the-badge)
30
+ ![Static Badge](https://img.shields.io/badge/Simple-GUI-blue?style=for-the-badge)
31
+
32
+ <p align="center">
33
+ <img src="https://raw.githubusercontent.com/limafresh/CTkDeb/main/ctkdeb/icon.png" width="100" height="100">
34
+ </p>
35
+
36
+ The easiest way to create a DEB or RPM package of your CustomTkinter project! You can create a small DEB or RPM package without deep packaging experience.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install CTkDeb[gui]
42
+ ```
43
+
44
+ - Creating **DEB** packages requires the `dpkg-deb` tool (pre-installed on most Debian-like systems, probably does not need to be installed separately).
45
+ - Creating **RPM** packages requires the `rpmbuild` tool.
46
+
47
+ ## Run
48
+
49
+ Run this in the project folder (relative to which the paths in the config are specified)
50
+
51
+ ```
52
+ ctkdeb-gui
53
+ ```
54
+
55
+ ## GUI
56
+
57
+ ![Screenshot](https://raw.githubusercontent.com/limafresh/CTkDeb/main/screenshot.png)
58
+
59
+ ## CLI
60
+
61
+ ```
62
+ ctkdeb config.json --create deb
63
+ ```
64
+
65
+ View all arguments:
66
+
67
+ ```
68
+ ctkdeb --help
69
+ ```
70
+
71
+ ## Python
72
+
73
+ ```python
74
+ from ctkdeb import Packager
75
+
76
+ packager = Packager("config.json")
77
+ packager.create_deb()
78
+ # or
79
+ packager.create_rpm("fedora")
80
+ ```
81
+
82
+ View all supported RPM distros:
83
+
84
+ ```python
85
+ from ctkdeb import show_supported_rpm_distros
86
+ show_supported_rpm_distros()
87
+ ```
88
+
89
+ Arguments:
90
+
91
+ | Argument | Type | Description |
92
+ |-|-|-|
93
+ | **json_path** | *str* or *dict* | Path to the JSON config (or its dict), required |
94
+ | **project_folder** | *str* | Path to the project folder (if the command is not executed from it) |
95
+ | **desktop_entry** | *str* | Path to the custom desktop entry (or its text) |
96
+ | **output_folder** | *str* | Path to the output folder |
97
+
98
+ ## FAQ
99
+
100
+ - Do I need to specify Tkinter and CustomTkinter as dependencies?
101
+
102
+ No, CTkDeb has already taken care of that; just specify third-party dependencies, if any.
103
+
104
+ - What about dependencies?
105
+
106
+ Pip dependencies are recommended only if they are written in pure Python (e.g., CTkMenuBar). If not, it's better to search for the required dependency in the distribution repositories and specify it as a DEB or RPM dependency. For example, for Pillow, specify `python3-pil` for DEB or `python3-pillow` for RPM (Fedora/Mageia) instead of `pillow` in the Pip dependencies line.
107
+
108
+ ## Other
109
+
110
+ App icon: Claude Sonnet 5.
@@ -0,0 +1,11 @@
1
+ ctkdeb/__init__.py,sha256=k_DMT5nbPuzf1y4f5E-CPYGndz1s5_LtQZqM0YOTweM,76
2
+ ctkdeb/gui.py,sha256=tLVi0FN7PuUcp_k-jWtkbbNH88ubhB3n6nCfFbaCJMg,13986
3
+ ctkdeb/icon.png,sha256=FyxyVnubMsvKfmOxU7X3pdeqBL_9G7tD09jD2D0hP_M,25249
4
+ ctkdeb/main.py,sha256=Z67CELvht-W0VVBe-zx0WrGVg4zYMjqX2Nks08sRXb8,1443
5
+ ctkdeb/packager.py,sha256=mpaTqW6Fxy0iLw7d7kDkLm8vEg9gSmuEEfPYUL6XoFI,10844
6
+ ctkdeb-0.7.0.dist-info/licenses/LICENSE,sha256=t7myKADPIxwryGtu0Hw4iu5_vRiDiDomJxd-kug6keE,1066
7
+ ctkdeb-0.7.0.dist-info/METADATA,sha256=AMeVDRYHtmNh9Pna080ax8r9B1zU6Xld1vJQjJPKB84,3311
8
+ ctkdeb-0.7.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
9
+ ctkdeb-0.7.0.dist-info/entry_points.txt,sha256=uFfm8FDJpu7njVLnUl0Go3WWHw6hadSwQ7gMb_5KLuw,87
10
+ ctkdeb-0.7.0.dist-info/top_level.txt,sha256=k5BoZR2FQt8ws_om-5eMVSdkpVxPXFjv6RpHDKgE8cQ,7
11
+ ctkdeb-0.7.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ ctkdeb = ctkdeb.main:cli
3
+
4
+ [gui_scripts]
5
+ ctkdeb-gui = ctkdeb.main:gui
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 limafresh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ ctkdeb