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 +2 -0
- ctkdeb/gui.py +361 -0
- ctkdeb/icon.png +0 -0
- ctkdeb/main.py +45 -0
- ctkdeb/packager.py +327 -0
- ctkdeb-0.7.0.dist-info/METADATA +110 -0
- ctkdeb-0.7.0.dist-info/RECORD +11 -0
- ctkdeb-0.7.0.dist-info/WHEEL +5 -0
- ctkdeb-0.7.0.dist-info/entry_points.txt +5 -0
- ctkdeb-0.7.0.dist-info/licenses/LICENSE +21 -0
- ctkdeb-0.7.0.dist-info/top_level.txt +1 -0
ctkdeb/__init__.py
ADDED
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
|
+

|
|
30
|
+

|
|
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
|
+

|
|
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,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
|