3 # Handles NEW and BYHAND packages
4 # Copyright (C) 2001, 2002, 2003, 2004, 2005, 2006 James Troup <james@nocrew.org>
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 ################################################################################
22 # 23:12|<aj> I will not hush!
24 # 23:12|<aj> Where there is injustice in the world, I shall be there!
25 # 23:13|<aj> I shall not be silenced!
26 # 23:13|<aj> The world shall know!
27 # 23:13|<aj> The world *must* know!
28 # 23:13|<elmo> oh dear, he's gone back to powerpuff girls... ;-)
29 # 23:13|<aj> yay powerpuff girls!!
30 # 23:13|<aj> buttercup's my favourite, who's yours?
31 # 23:14|<aj> you're backing away from the keyboard right now aren't you?
32 # 23:14|<aj> *AREN'T YOU*?!
33 # 23:15|<aj> I will not be treated like this.
34 # 23:15|<aj> I shall have my revenge.
35 # 23:15|<aj> I SHALL!!!
37 ################################################################################
39 import copy, errno, os, readline, stat, sys, time
40 import apt_pkg, apt_inst
41 import examine_package
42 from daklib import database
43 from daklib import logging
44 from daklib import queue
45 from daklib import utils
59 ################################################################################
60 ################################################################################
61 ################################################################################
63 def reject (str, prefix="Rejected: "):
66 reject_message += prefix + str + "\n"
70 files = Upload.pkg.files
73 for f in files.keys():
74 # The .orig.tar.gz can disappear out from under us is it's a
75 # duplicate of one in the archive.
76 if not files.has_key(f):
78 # Check that the source still exists
79 if files[f]["type"] == "deb":
80 source_version = files[f]["source version"]
81 source_package = files[f]["source package"]
82 if not Upload.pkg.changes["architecture"].has_key("source") \
83 and not Upload.source_exists(source_package, source_version, Upload.pkg.changes["distribution"].keys()):
84 source_epochless_version = utils.re_no_epoch.sub('', source_version)
85 dsc_filename = "%s_%s.dsc" % (source_package, source_epochless_version)
87 for q in ["Accepted", "Embargoed", "Unembargoed"]:
88 if Cnf.has_key("Dir::Queue::%s" % (q)):
89 if os.path.exists(Cnf["Dir::Queue::%s" % (q)] + '/' + dsc_filename):
92 reject("no source found for %s %s (%s)." % (source_package, source_version, f))
94 # Version and file overwrite checks
95 if files[f]["type"] == "deb":
96 reject(Upload.check_binary_against_db(f))
97 elif files[f]["type"] == "dsc":
98 reject(Upload.check_source_against_db(f))
99 (reject_msg, is_in_incoming) = Upload.check_dsc_against_db(f)
100 reject(reject_msg, "")
102 if reject_message.find("Rejected") != -1:
104 if Options["No-Action"] or Options["Automatic"]:
107 print "REJECT\n" + reject_message,
108 prompt = "[R]eject, Skip, Quit ?"
110 while prompt.find(answer) == -1:
111 answer = utils.our_raw_input(prompt)
112 m = queue.re_default_answer.match(prompt)
115 answer = answer[:1].upper()
118 Upload.do_reject(0, reject_message)
119 os.unlink(Upload.pkg.changes_file[:-8]+".dak")
129 ################################################################################
131 def indiv_sg_compare (a, b):
132 """Sort by source name, source, version, 'have source', and
133 finally by filename."""
134 # Sort by source version
135 q = apt_pkg.VersionCompare(a["version"], b["version"])
139 # Sort by 'have source'
140 a_has_source = a["architecture"].get("source")
141 b_has_source = b["architecture"].get("source")
142 if a_has_source and not b_has_source:
144 elif b_has_source and not a_has_source:
147 return cmp(a["filename"], b["filename"])
149 ############################################################
151 def sg_compare (a, b):
154 """Sort by have note, source already in database, time of oldest upload."""
156 a_note_state = a["note_state"]
157 b_note_state = b["note_state"]
158 if a_note_state < b_note_state:
160 elif a_note_state > b_note_state:
162 # Sort by source already in database (descending)
163 source_in_database = cmp(a["source_in_database"], b["source_in_database"])
164 if source_in_database:
165 return -source_in_database
167 # Sort by time of oldest upload
168 return cmp(a["oldest"], b["oldest"])
170 def sort_changes(changes_files):
171 """Sort into source groups, then sort each source group by version,
172 have source, filename. Finally, sort the source groups by have
173 note, time of oldest upload of each source upload."""
174 if len(changes_files) == 1:
179 # Read in all the .changes files
180 for filename in changes_files:
182 Upload.pkg.changes_file = filename
185 cache[filename] = copy.copy(Upload.pkg.changes)
186 cache[filename]["filename"] = filename
188 sorted_list.append(filename)
190 # Divide the .changes into per-source groups
192 for filename in cache.keys():
193 source = cache[filename]["source"]
194 if not per_source.has_key(source):
195 per_source[source] = {}
196 per_source[source]["list"] = []
197 per_source[source]["list"].append(cache[filename])
198 # Determine oldest time and have note status for each source group
199 for source in per_source.keys():
200 q = projectB.query("SELECT 1 FROM source WHERE source = '%s'" % source)
202 per_source[source]["source_in_database"] = len(ql)>0
203 source_list = per_source[source]["list"]
204 first = source_list[0]
205 oldest = os.stat(first["filename"])[stat.ST_MTIME]
207 for d in per_source[source]["list"]:
208 mtime = os.stat(d["filename"])[stat.ST_MTIME]
211 have_note += (d.has_key("process-new note"))
212 per_source[source]["oldest"] = oldest
214 per_source[source]["note_state"] = 0; # none
215 elif have_note < len(source_list):
216 per_source[source]["note_state"] = 1; # some
218 per_source[source]["note_state"] = 2; # all
219 per_source[source]["list"].sort(indiv_sg_compare)
220 per_source_items = per_source.items()
221 per_source_items.sort(sg_compare)
222 for i in per_source_items:
223 for j in i[1]["list"]:
224 sorted_list.append(j["filename"])
227 ################################################################################
229 class Section_Completer:
232 q = projectB.query("SELECT section FROM section")
233 for i in q.getresult():
234 self.sections.append(i[0])
236 def complete(self, text, state):
240 for word in self.sections:
242 self.matches.append(word)
244 return self.matches[state]
248 ############################################################
250 class Priority_Completer:
253 q = projectB.query("SELECT priority FROM priority")
254 for i in q.getresult():
255 self.priorities.append(i[0])
257 def complete(self, text, state):
261 for word in self.priorities:
263 self.matches.append(word)
265 return self.matches[state]
269 ################################################################################
271 def print_new (new, indexed, file=sys.stdout):
272 queue.check_valid(new)
275 for pkg in new.keys():
277 section = new[pkg]["section"]
278 priority = new[pkg]["priority"]
279 if new[pkg]["section id"] == -1:
282 if new[pkg]["priority id"] == -1:
286 line = "(%s): %-20s %-20s %-20s" % (index, pkg, priority, section)
288 line = "%-20s %-20s %-20s" % (pkg, priority, section)
289 line = line.strip()+'\n'
291 note = Upload.pkg.changes.get("process-new note")
298 ################################################################################
300 def index_range (index):
304 return "1-%s" % (index)
306 ################################################################################
307 ################################################################################
310 # Write the current data to a temporary file
311 temp_filename = utils.temp_filename()
312 temp_file = utils.open_file(temp_filename, 'w')
313 print_new (new, 0, temp_file)
315 # Spawn an editor on that file
316 editor = os.environ.get("EDITOR","vi")
317 result = os.system("%s %s" % (editor, temp_filename))
319 utils.fubar ("%s invocation failed for %s." % (editor, temp_filename), result)
320 # Read the edited data back in
321 temp_file = utils.open_file(temp_filename)
322 lines = temp_file.readlines()
324 os.unlink(temp_filename)
331 # Pad the list if necessary
332 s[len(s):3] = [None] * (3-len(s))
333 (pkg, priority, section) = s[:3]
334 if not new.has_key(pkg):
335 utils.warn("Ignoring unknown package '%s'" % (pkg))
337 # Strip off any invalid markers, print_new will readd them.
338 if section.endswith("[!]"):
339 section = section[:-3]
340 if priority.endswith("[!]"):
341 priority = priority[:-3]
342 for f in new[pkg]["files"]:
343 Upload.pkg.files[f]["section"] = section
344 Upload.pkg.files[f]["priority"] = priority
345 new[pkg]["section"] = section
346 new[pkg]["priority"] = priority
348 ################################################################################
350 def edit_index (new, index):
351 priority = new[index]["priority"]
352 section = new[index]["section"]
353 ftype = new[index]["type"]
356 print "\t".join([index, priority, section])
360 prompt = "[B]oth, Priority, Section, Done ? "
362 prompt = "[S]ection, Done ? "
363 edit_priority = edit_section = 0
365 while prompt.find(answer) == -1:
366 answer = utils.our_raw_input(prompt)
367 m = queue.re_default_answer.match(prompt)
370 answer = answer[:1].upper()
377 edit_priority = edit_section = 1
383 readline.set_completer(Priorities.complete)
385 while not got_priority:
386 new_priority = utils.our_raw_input("New priority: ").strip()
387 if new_priority not in Priorities.priorities:
388 print "E: '%s' is not a valid priority, try again." % (new_priority)
391 priority = new_priority
395 readline.set_completer(Sections.complete)
397 while not got_section:
398 new_section = utils.our_raw_input("New section: ").strip()
399 if new_section not in Sections.sections:
400 print "E: '%s' is not a valid section, try again." % (new_section)
403 section = new_section
405 # Reset the readline completer
406 readline.set_completer(None)
408 for f in new[index]["files"]:
409 Upload.pkg.files[f]["section"] = section
410 Upload.pkg.files[f]["priority"] = priority
411 new[index]["priority"] = priority
412 new[index]["section"] = section
415 ################################################################################
417 def edit_overrides (new):
428 prompt = "(%s) edit override <n>, Editor, Done ? " % (index_range(index))
431 while not got_answer:
432 answer = utils.our_raw_input(prompt)
433 if not answer.isdigit():
434 answer = answer[:1].upper()
435 if answer == "E" or answer == "D":
437 elif queue.re_isanum.match (answer):
439 if (answer < 1) or (answer > index):
440 print "%s is not a valid index (%s). Please retry." % (answer, index_range(index))
449 edit_index (new, new_index[answer])
453 ################################################################################
456 # Write the current data to a temporary file
457 temp_filename = utils.temp_filename()
458 temp_file = utils.open_file(temp_filename, 'w')
459 temp_file.write(note)
461 editor = os.environ.get("EDITOR","vi")
464 os.system("%s %s" % (editor, temp_filename))
465 temp_file = utils.open_file(temp_filename)
466 note = temp_file.read().rstrip()
469 print utils.prefix_multi_line_string(note," ")
470 prompt = "[D]one, Edit, Abandon, Quit ?"
472 while prompt.find(answer) == -1:
473 answer = utils.our_raw_input(prompt)
474 m = queue.re_default_answer.search(prompt)
477 answer = answer[:1].upper()
478 os.unlink(temp_filename)
484 Upload.pkg.changes["process-new note"] = note
485 Upload.dump_vars(Cnf["Dir::Queue::New"])
487 ################################################################################
491 less_fd = os.popen("less -R -", 'w', 0)
492 stdout_fd = sys.stdout
495 examine_package.display_changes(Upload.pkg.changes_file)
496 files = Upload.pkg.files
497 for f in files.keys():
498 if files[f].has_key("new"):
499 ftype = files[f]["type"]
501 examine_package.check_deb(f)
503 examine_package.check_dsc(f)
505 sys.stdout = stdout_fd
507 if e.errno == errno.EPIPE:
508 utils.warn("[examine_package] Caught EPIPE; skipping.")
512 except KeyboardInterrupt:
513 utils.warn("[examine_package] Caught C-c; skipping.")
516 ################################################################################
518 ## FIXME: horribly Debian specific
520 def do_bxa_notification():
521 files = Upload.pkg.files
523 for f in files.keys():
524 if files[f]["type"] == "deb":
525 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(f)))
527 summary += "Package: %s\n" % (control.Find("Package"))
528 summary += "Description: %s\n" % (control.Find("Description"))
529 Upload.Subst["__BINARY_DESCRIPTIONS__"] = summary
530 bxa_mail = utils.TemplateSubst(Upload.Subst,Cnf["Dir::Templates"]+"/process-new.bxa_notification")
531 utils.send_mail(bxa_mail)
533 ################################################################################
535 def add_overrides (new):
536 changes = Upload.pkg.changes
537 files = Upload.pkg.files
539 projectB.query("BEGIN WORK")
540 for suite in changes["suite"].keys():
541 suite_id = database.get_suite_id(suite)
542 for pkg in new.keys():
543 component_id = database.get_component_id(new[pkg]["component"])
544 type_id = database.get_override_type_id(new[pkg]["type"])
545 priority_id = new[pkg]["priority id"]
546 section_id = new[pkg]["section id"]
547 projectB.query("INSERT INTO override (suite, component, type, package, priority, section, maintainer) VALUES (%s, %s, %s, '%s', %s, %s, '')" % (suite_id, component_id, type_id, pkg, priority_id, section_id))
548 for f in new[pkg]["files"]:
549 if files[f].has_key("new"):
553 projectB.query("COMMIT WORK")
555 if Cnf.FindB("Dinstall::BXANotify"):
556 do_bxa_notification()
558 ################################################################################
560 def prod_maintainer ():
561 # Here we prepare an editor and get them ready to prod...
562 temp_filename = utils.temp_filename()
563 editor = os.environ.get("EDITOR","vi")
566 os.system("%s %s" % (editor, temp_filename))
567 f = utils.open_file(temp_filename)
568 prod_message = "".join(f.readlines())
570 print "Prod message:"
571 print utils.prefix_multi_line_string(prod_message," ",include_blank_lines=1)
572 prompt = "[P]rod, Edit, Abandon, Quit ?"
574 while prompt.find(answer) == -1:
575 answer = utils.our_raw_input(prompt)
576 m = queue.re_default_answer.search(prompt)
579 answer = answer[:1].upper()
580 os.unlink(temp_filename)
586 # Otherwise, do the proding...
587 user_email_address = utils.whoami() + " <%s>" % (
588 Cnf["Dinstall::MyAdminAddress"])
592 Subst["__FROM_ADDRESS__"] = user_email_address
593 Subst["__PROD_MESSAGE__"] = prod_message
594 Subst["__CC__"] = "Cc: " + Cnf["Dinstall::MyEmailAddress"]
596 prod_mail_message = utils.TemplateSubst(
597 Subst,Cnf["Dir::Templates"]+"/process-new.prod")
599 # Send the prod mail if appropriate
600 if not Cnf["Dinstall::Options::No-Mail"]:
601 utils.send_mail(prod_mail_message)
603 print "Sent proding message"
605 ################################################################################
609 files = Upload.pkg.files
610 changes = Upload.pkg.changes
612 # Make a copy of distribution we can happily trample on
613 changes["suite"] = copy.copy(changes["distribution"])
615 # Fix up the list of target suites
616 for suite in changes["suite"].keys():
617 override = Cnf.Find("Suite::%s::OverrideSuite" % (suite))
619 (olderr, newerr) = (database.get_suite_id(suite) == -1,
620 database.get_suite_id(override) == -1)
622 (oinv, newinv) = ("", "")
623 if olderr: oinv = "invalid "
624 if newerr: ninv = "invalid "
625 print "warning: overriding %ssuite %s to %ssuite %s" % (
626 oinv, suite, ninv, override)
627 del changes["suite"][suite]
628 changes["suite"][override] = 1
630 for suite in changes["suite"].keys():
631 suite_id = database.get_suite_id(suite)
633 utils.fubar("%s has invalid suite '%s' (possibly overriden). say wha?" % (changes, suite))
635 # The main NEW processing loop
638 # Find out what's new
639 new = queue.determine_new(changes, files, projectB)
645 if Options["No-Action"] or Options["Automatic"]:
648 (broken, note) = print_new(new, 0)
651 if not broken and not note:
652 prompt = "Add overrides, "
654 print "W: [!] marked entries must be fixed before package can be processed."
656 print "W: note must be removed before package can be processed."
657 prompt += "Remove note, "
659 prompt += "Edit overrides, Check, Manual reject, Note edit, Prod, [S]kip, Quit ?"
661 while prompt.find(answer) == -1:
662 answer = utils.our_raw_input(prompt)
663 m = queue.re_default_answer.search(prompt)
666 answer = answer[:1].upper()
669 done = add_overrides (new)
673 new = edit_overrides (new)
675 aborted = Upload.do_reject(1, Options["Manual-Reject"])
677 os.unlink(Upload.pkg.changes_file[:-8]+".dak")
680 edit_note(changes.get("process-new note", ""))
684 confirm = utils.our_raw_input("Really clear note (y/N)? ").lower()
686 del changes["process-new note"]
693 ################################################################################
694 ################################################################################
695 ################################################################################
697 def usage (exit_code=0):
698 print """Usage: dak process-new [OPTION]... [CHANGES]...
699 -a, --automatic automatic run
700 -h, --help show this help and exit.
701 -C, --comments-dir=DIR use DIR as comments-dir, for [o-]p-u-new
702 -m, --manual-reject=MSG manual reject with `msg'
703 -n, --no-action don't do anything
704 -V, --version display the version number and exit"""
707 ################################################################################
710 global Cnf, Options, Logger, Upload, projectB, Sections, Priorities
712 Cnf = utils.get_conf()
714 Arguments = [('a',"automatic","Process-New::Options::Automatic"),
715 ('h',"help","Process-New::Options::Help"),
716 ('C',"comments-dir","Process-New::Options::Comments-Dir", "HasArg"),
717 ('m',"manual-reject","Process-New::Options::Manual-Reject", "HasArg"),
718 ('n',"no-action","Process-New::Options::No-Action")]
720 for i in ["automatic", "help", "manual-reject", "no-action", "version", "comments-dir"]:
721 if not Cnf.has_key("Process-New::Options::%s" % (i)):
722 Cnf["Process-New::Options::%s" % (i)] = ""
724 changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv)
725 Options = Cnf.SubTree("Process-New::Options")
730 Upload = queue.Upload(Cnf)
732 if not Options["No-Action"]:
733 Logger = Upload.Logger = logging.Logger(Cnf, "process-new")
735 projectB = Upload.projectB
737 Sections = Section_Completer()
738 Priorities = Priority_Completer()
739 readline.parse_and_bind("tab: complete")
743 ################################################################################
748 files = Upload.pkg.files
752 for f in files.keys():
753 if files[f]["type"] == "byhand":
754 if os.path.exists(f):
755 print "W: %s still present; please process byhand components and try again." % (f)
761 if Options["No-Action"]:
764 if Options["Automatic"] and not Options["No-Action"]:
766 prompt = "[A]ccept, Manual reject, Skip, Quit ?"
768 prompt = "Manual reject, [S]kip, Quit ?"
770 while prompt.find(answer) == -1:
771 answer = utils.our_raw_input(prompt)
772 m = queue.re_default_answer.search(prompt)
775 answer = answer[:1].upper()
782 Upload.do_reject(1, Options["Manual-Reject"])
783 os.unlink(Upload.pkg.changes_file[:-8]+".dak")
791 ################################################################################
793 def get_accept_lock():
797 os.open(Cnf["Process-New::AcceptedLockFile"], os.O_RDONLY | os.O_CREAT | os.O_EXCL)
800 if e.errno == errno.EACCES or e.errno == errno.EEXIST:
803 utils.fubar("Couldn't obtain lock; assuming 'dak process-unchecked' is already running.")
805 print("Unable to get accepted lock (try %d of 10)" % retry)
810 def move_to_dir (dest, perms=0660, changesperms=0664):
811 utils.move (Upload.pkg.changes_file, dest, perms=changesperms)
812 file_keys = Upload.pkg.files.keys()
814 utils.move (f, dest, perms=perms)
818 if not Options["No-Action"]:
820 (summary, short_summary) = Upload.build_summaries()
821 if Cnf.FindB("Dinstall::SecurityQueueHandling"):
822 Upload.dump_vars(Cnf["Dir::Queue::Embargoed"])
823 move_to_dir(Cnf["Dir::Queue::Embargoed"])
824 Upload.queue_build("embargoed", Cnf["Dir::Queue::Embargoed"])
825 # Check for override disparities
826 Upload.Subst["__SUMMARY__"] = summary
828 Upload.accept(summary, short_summary)
829 os.unlink(Upload.pkg.changes_file[:-8]+".dak")
830 os.unlink(Cnf["Process-New::AcceptedLockFile"])
832 def check_status(files):
834 for f in files.keys():
835 if files[f]["type"] == "byhand":
837 elif files[f].has_key("new"):
841 def do_pkg(changes_file):
842 Upload.pkg.changes_file = changes_file
845 Upload.update_subst()
846 files = Upload.pkg.files
851 (new, byhand) = check_status(files)
857 (new, byhand) = check_status(files)
859 if not new and not byhand:
862 ################################################################################
865 accept_count = Upload.accept_count
866 accept_bytes = Upload.accept_bytes
872 sys.stderr.write("Accepted %d package %s, %s.\n" % (accept_count, sets, utils.size_type(int(accept_bytes))))
873 Logger.log(["total",accept_count,accept_bytes])
875 if not Options["No-Action"]:
878 ################################################################################
880 def do_comments(dir, opref, npref, line, fn):
881 for comm in [ x for x in os.listdir(dir) if x.startswith(opref) ]:
882 lines = open("%s/%s" % (dir, comm)).readlines()
883 if len(lines) == 0 or lines[0] != line + "\n": continue
884 changes_files = [ x for x in os.listdir(".") if x.startswith(comm[7:]+"_")
885 and x.endswith(".changes") ]
886 changes_files = sort_changes(changes_files)
887 for f in changes_files:
888 f = utils.validate_changes_file_arg(f, 0)
891 fn(f, "".join(lines[1:]))
893 if opref != npref and not Options["No-Action"]:
894 newcomm = npref + comm[len(opref):]
895 os.rename("%s/%s" % (dir, comm), "%s/%s" % (dir, newcomm))
897 ################################################################################
899 def comment_accept(changes_file, comments):
900 Upload.pkg.changes_file = changes_file
903 Upload.update_subst()
904 files = Upload.pkg.files
907 return # dak wants to REJECT, crap
909 (new, byhand) = check_status(files)
910 if not new and not byhand:
913 ################################################################################
915 def comment_reject(changes_file, comments):
916 Upload.pkg.changes_file = changes_file
919 Upload.update_subst()
922 pass # dak has its own reasons to reject as well, which is fine
925 print "REJECT\n" + reject_message,
926 if not Options["No-Action"]:
927 Upload.do_reject(0, reject_message)
928 os.unlink(Upload.pkg.changes_file[:-8]+".dak")
930 ################################################################################
933 changes_files = init()
934 if len(changes_files) > 50:
935 sys.stderr.write("Sorting changes...\n")
936 changes_files = sort_changes(changes_files)
938 # Kill me now? **FIXME**
939 Cnf["Dinstall::Options::No-Mail"] = ""
940 bcc = "X-DAK: dak process-new\nX-Katie: lisa $Revision: 1.31 $"
941 if Cnf.has_key("Dinstall::Bcc"):
942 Upload.Subst["__BCC__"] = bcc + "\nBcc: %s" % (Cnf["Dinstall::Bcc"])
944 Upload.Subst["__BCC__"] = bcc
946 commentsdir = Cnf.get("Process-New::Options::Comments-Dir","")
948 if changes_files != []:
949 sys.stderr.write("Can't specify any changes files if working with comments-dir")
951 do_comments(commentsdir, "ACCEPT.", "ACCEPTED.", "OK", comment_accept)
952 do_comments(commentsdir, "REJECT.", "REJECTED.", "NOTOK", comment_reject)
954 for changes_file in changes_files:
955 changes_file = utils.validate_changes_file_arg(changes_file, 0)
958 print "\n" + changes_file
959 do_pkg (changes_file)
963 ################################################################################
965 if __name__ == '__main__':