xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/llvm_bisection.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1*760c253cSXin Li#!/usr/bin/env python3
2*760c253cSXin Li# Copyright 2019 The ChromiumOS Authors
3*760c253cSXin Li# Use of this source code is governed by a BSD-style license that can be
4*760c253cSXin Li# found in the LICENSE file.
5*760c253cSXin Li
6*760c253cSXin Li"""Performs bisection on LLVM based off a .JSON file."""
7*760c253cSXin Li
8*760c253cSXin Liimport argparse
9*760c253cSXin Liimport enum
10*760c253cSXin Liimport errno
11*760c253cSXin Liimport json
12*760c253cSXin Liimport os
13*760c253cSXin Liimport subprocess
14*760c253cSXin Liimport sys
15*760c253cSXin Li
16*760c253cSXin Liimport chroot
17*760c253cSXin Liimport get_llvm_hash
18*760c253cSXin Liimport git_llvm_rev
19*760c253cSXin Liimport modify_a_tryjob
20*760c253cSXin Liimport update_chromeos_llvm_hash
21*760c253cSXin Liimport update_tryjob_status
22*760c253cSXin Li
23*760c253cSXin Li
24*760c253cSXin Liclass BisectionExitStatus(enum.Enum):
25*760c253cSXin Li    """Exit code when performing bisection."""
26*760c253cSXin Li
27*760c253cSXin Li    # Means that there are no more revisions available to bisect.
28*760c253cSXin Li    BISECTION_COMPLETE = 126
29*760c253cSXin Li
30*760c253cSXin Li
31*760c253cSXin Lidef GetCommandLineArgs():
32*760c253cSXin Li    """Parses the command line for the command line arguments."""
33*760c253cSXin Li
34*760c253cSXin Li    # Default path to the chroot if a path is not specified.
35*760c253cSXin Li    cros_root = os.path.expanduser("~")
36*760c253cSXin Li    cros_root = os.path.join(cros_root, "chromiumos")
37*760c253cSXin Li
38*760c253cSXin Li    # Create parser and add optional command-line arguments.
39*760c253cSXin Li    parser = argparse.ArgumentParser(
40*760c253cSXin Li        description="Bisects LLVM via tracking a JSON file."
41*760c253cSXin Li    )
42*760c253cSXin Li
43*760c253cSXin Li    # Add argument for other change lists that want to run alongside the tryjob
44*760c253cSXin Li    # which has a change list of updating a package's git hash.
45*760c253cSXin Li    parser.add_argument(
46*760c253cSXin Li        "--parallel",
47*760c253cSXin Li        type=int,
48*760c253cSXin Li        default=3,
49*760c253cSXin Li        help="How many tryjobs to create between the last good version and "
50*760c253cSXin Li        "the first bad version (default: %(default)s)",
51*760c253cSXin Li    )
52*760c253cSXin Li
53*760c253cSXin Li    # Add argument for the good LLVM revision for bisection.
54*760c253cSXin Li    parser.add_argument(
55*760c253cSXin Li        "--start_rev",
56*760c253cSXin Li        required=True,
57*760c253cSXin Li        type=int,
58*760c253cSXin Li        help="The good revision for the bisection.",
59*760c253cSXin Li    )
60*760c253cSXin Li
61*760c253cSXin Li    # Add argument for the bad LLVM revision for bisection.
62*760c253cSXin Li    parser.add_argument(
63*760c253cSXin Li        "--end_rev",
64*760c253cSXin Li        required=True,
65*760c253cSXin Li        type=int,
66*760c253cSXin Li        help="The bad revision for the bisection.",
67*760c253cSXin Li    )
68*760c253cSXin Li
69*760c253cSXin Li    # Add argument for the absolute path to the file that contains information
70*760c253cSXin Li    # on the previous tested svn version.
71*760c253cSXin Li    parser.add_argument(
72*760c253cSXin Li        "--last_tested",
73*760c253cSXin Li        required=True,
74*760c253cSXin Li        help="the absolute path to the file that contains the tryjobs",
75*760c253cSXin Li    )
76*760c253cSXin Li
77*760c253cSXin Li    # Add argument for the absolute path to the LLVM source tree.
78*760c253cSXin Li    parser.add_argument(
79*760c253cSXin Li        "--src_path",
80*760c253cSXin Li        help="the path to the LLVM source tree to use (used for retrieving the "
81*760c253cSXin Li        "git hash of each version between the last good version and first bad "
82*760c253cSXin Li        "version)",
83*760c253cSXin Li    )
84*760c253cSXin Li
85*760c253cSXin Li    # Add argument for other change lists that want to run alongside the tryjob
86*760c253cSXin Li    # which has a change list of updating a package's git hash.
87*760c253cSXin Li    parser.add_argument(
88*760c253cSXin Li        "--extra_change_lists",
89*760c253cSXin Li        type=int,
90*760c253cSXin Li        nargs="+",
91*760c253cSXin Li        help="change lists that would like to be run alongside the change list "
92*760c253cSXin Li        "of updating the packages",
93*760c253cSXin Li    )
94*760c253cSXin Li
95*760c253cSXin Li    # Add argument for custom options for the tryjob.
96*760c253cSXin Li    parser.add_argument(
97*760c253cSXin Li        "--options",
98*760c253cSXin Li        required=False,
99*760c253cSXin Li        nargs="+",
100*760c253cSXin Li        help="options to use for the tryjob testing",
101*760c253cSXin Li    )
102*760c253cSXin Li
103*760c253cSXin Li    # Add argument for the builder to use for the tryjob.
104*760c253cSXin Li    parser.add_argument(
105*760c253cSXin Li        "--builder", required=True, help="builder to use for the tryjob testing"
106*760c253cSXin Li    )
107*760c253cSXin Li
108*760c253cSXin Li    # Add argument for the description of the tryjob.
109*760c253cSXin Li    parser.add_argument(
110*760c253cSXin Li        "--description",
111*760c253cSXin Li        required=False,
112*760c253cSXin Li        nargs="+",
113*760c253cSXin Li        help="the description of the tryjob",
114*760c253cSXin Li    )
115*760c253cSXin Li
116*760c253cSXin Li    # Add argument for a specific chroot path.
117*760c253cSXin Li    parser.add_argument(
118*760c253cSXin Li        "--chromeos_path",
119*760c253cSXin Li        default=cros_root,
120*760c253cSXin Li        help="the path to the chroot (default: %(default)s)",
121*760c253cSXin Li    )
122*760c253cSXin Li
123*760c253cSXin Li    # Add argument for whether to display command contents to `stdout`.
124*760c253cSXin Li    parser.add_argument(
125*760c253cSXin Li        "--nocleanup",
126*760c253cSXin Li        action="store_false",
127*760c253cSXin Li        dest="cleanup",
128*760c253cSXin Li        help="Abandon CLs created for bisectoin",
129*760c253cSXin Li    )
130*760c253cSXin Li
131*760c253cSXin Li    args_output = parser.parse_args()
132*760c253cSXin Li
133*760c253cSXin Li    assert (
134*760c253cSXin Li        args_output.start_rev < args_output.end_rev
135*760c253cSXin Li    ), "Start revision %d is >= end revision %d" % (
136*760c253cSXin Li        args_output.start_rev,
137*760c253cSXin Li        args_output.end_rev,
138*760c253cSXin Li    )
139*760c253cSXin Li
140*760c253cSXin Li    if args_output.last_tested and not args_output.last_tested.endswith(
141*760c253cSXin Li        ".json"
142*760c253cSXin Li    ):
143*760c253cSXin Li        raise ValueError(
144*760c253cSXin Li            'Filed provided %s does not end in ".json"'
145*760c253cSXin Li            % args_output.last_tested
146*760c253cSXin Li        )
147*760c253cSXin Li
148*760c253cSXin Li    return args_output
149*760c253cSXin Li
150*760c253cSXin Li
151*760c253cSXin Lidef GetRemainingRange(start, end, tryjobs):
152*760c253cSXin Li    """Gets the start and end intervals in 'json_file'.
153*760c253cSXin Li
154*760c253cSXin Li    Args:
155*760c253cSXin Li        start: The start version of the bisection provided via the command line.
156*760c253cSXin Li        end: The end version of the bisection provided via the command line.
157*760c253cSXin Li        tryjobs: A list of tryjobs where each element is in the following
158*760c253cSXin Li        format:
159*760c253cSXin Li        [
160*760c253cSXin Li            {[TRYJOB_INFORMATION]},
161*760c253cSXin Li            {[TRYJOB_INFORMATION]},
162*760c253cSXin Li            ...,
163*760c253cSXin Li            {[TRYJOB_INFORMATION]}
164*760c253cSXin Li        ]
165*760c253cSXin Li
166*760c253cSXin Li    Returns:
167*760c253cSXin Li        The new start version and end version for bisection, a set of revisions
168*760c253cSXin Li        that are 'pending' and a set of revisions that are to be skipped.
169*760c253cSXin Li
170*760c253cSXin Li    Raises:
171*760c253cSXin Li        ValueError: The value for 'status' is missing or there is a mismatch
172*760c253cSXin Li        between 'start' and 'end' compared to the 'start' and 'end' in the JSON
173*760c253cSXin Li        file.
174*760c253cSXin Li        AssertionError: The new start version is >= than the new end version.
175*760c253cSXin Li    """
176*760c253cSXin Li
177*760c253cSXin Li    if not tryjobs:
178*760c253cSXin Li        return start, end, {}, {}
179*760c253cSXin Li
180*760c253cSXin Li    # Verify that each tryjob has a value for the 'status' key.
181*760c253cSXin Li    for cur_tryjob_dict in tryjobs:
182*760c253cSXin Li        if not cur_tryjob_dict.get("status", None):
183*760c253cSXin Li            raise ValueError(
184*760c253cSXin Li                '"status" is missing or has no value, please '
185*760c253cSXin Li                "go to %s and update it" % cur_tryjob_dict["link"]
186*760c253cSXin Li            )
187*760c253cSXin Li
188*760c253cSXin Li    all_bad_revisions = [end]
189*760c253cSXin Li    all_bad_revisions.extend(
190*760c253cSXin Li        cur_tryjob["rev"]
191*760c253cSXin Li        for cur_tryjob in tryjobs
192*760c253cSXin Li        if cur_tryjob["status"] == update_tryjob_status.TryjobStatus.BAD.value
193*760c253cSXin Li    )
194*760c253cSXin Li
195*760c253cSXin Li    # The minimum value for the 'bad' field in the tryjobs is the new end
196*760c253cSXin Li    # version.
197*760c253cSXin Li    bad_rev = min(all_bad_revisions)
198*760c253cSXin Li
199*760c253cSXin Li    all_good_revisions = [start]
200*760c253cSXin Li    all_good_revisions.extend(
201*760c253cSXin Li        cur_tryjob["rev"]
202*760c253cSXin Li        for cur_tryjob in tryjobs
203*760c253cSXin Li        if cur_tryjob["status"] == update_tryjob_status.TryjobStatus.GOOD.value
204*760c253cSXin Li    )
205*760c253cSXin Li
206*760c253cSXin Li    # The maximum value for the 'good' field in the tryjobs is the new start
207*760c253cSXin Li    # version.
208*760c253cSXin Li    good_rev = max(all_good_revisions)
209*760c253cSXin Li
210*760c253cSXin Li    # The good version should always be strictly less than the bad version;
211*760c253cSXin Li    # otherwise, bisection is broken.
212*760c253cSXin Li    assert (
213*760c253cSXin Li        good_rev < bad_rev
214*760c253cSXin Li    ), "Bisection is broken because %d (good) is >= " "%d (bad)" % (
215*760c253cSXin Li        good_rev,
216*760c253cSXin Li        bad_rev,
217*760c253cSXin Li    )
218*760c253cSXin Li
219*760c253cSXin Li    # Find all revisions that are 'pending' within 'good_rev' and 'bad_rev'.
220*760c253cSXin Li    #
221*760c253cSXin Li    # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
222*760c253cSXin Li    # that have already been launched (this set is used when constructing the
223*760c253cSXin Li    # list of revisions to launch tryjobs for).
224*760c253cSXin Li    pending_revisions = {
225*760c253cSXin Li        tryjob["rev"]
226*760c253cSXin Li        for tryjob in tryjobs
227*760c253cSXin Li        if tryjob["status"] == update_tryjob_status.TryjobStatus.PENDING.value
228*760c253cSXin Li        and good_rev < tryjob["rev"] < bad_rev
229*760c253cSXin Li    }
230*760c253cSXin Li
231*760c253cSXin Li    # Find all revisions that are to be skipped within 'good_rev' and 'bad_rev'.
232*760c253cSXin Li    #
233*760c253cSXin Li    # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
234*760c253cSXin Li    # that have already been marked as 'skip' (this set is used when
235*760c253cSXin Li    # constructing the list of revisions to launch tryjobs for).
236*760c253cSXin Li    skip_revisions = {
237*760c253cSXin Li        tryjob["rev"]
238*760c253cSXin Li        for tryjob in tryjobs
239*760c253cSXin Li        if tryjob["status"] == update_tryjob_status.TryjobStatus.SKIP.value
240*760c253cSXin Li        and good_rev < tryjob["rev"] < bad_rev
241*760c253cSXin Li    }
242*760c253cSXin Li
243*760c253cSXin Li    return good_rev, bad_rev, pending_revisions, skip_revisions
244*760c253cSXin Li
245*760c253cSXin Li
246*760c253cSXin Lidef GetCommitsBetween(
247*760c253cSXin Li    start, end, parallel, src_path, pending_revisions, skip_revisions
248*760c253cSXin Li):
249*760c253cSXin Li    """Determines the revisions between start and end."""
250*760c253cSXin Li
251*760c253cSXin Li    with get_llvm_hash.LLVMHash().CreateTempDirectory() as temp_dir:
252*760c253cSXin Li        # We have guaranteed contiguous revision numbers after this,
253*760c253cSXin Li        # and that guarnatee simplifies things considerably, so we don't
254*760c253cSXin Li        # support anything before it.
255*760c253cSXin Li        assert (
256*760c253cSXin Li            start >= git_llvm_rev.base_llvm_revision
257*760c253cSXin Li        ), f"{start} was too long ago"
258*760c253cSXin Li
259*760c253cSXin Li        with get_llvm_hash.CreateTempLLVMRepo(temp_dir) as new_repo:
260*760c253cSXin Li            if not src_path:
261*760c253cSXin Li                src_path = new_repo
262*760c253cSXin Li            index_step = (end - (start + 1)) // (parallel + 1)
263*760c253cSXin Li            if not index_step:
264*760c253cSXin Li                index_step = 1
265*760c253cSXin Li            revisions = [
266*760c253cSXin Li                rev
267*760c253cSXin Li                for rev in range(start + 1, end, index_step)
268*760c253cSXin Li                if rev not in pending_revisions and rev not in skip_revisions
269*760c253cSXin Li            ]
270*760c253cSXin Li            git_hashes = [
271*760c253cSXin Li                get_llvm_hash.GetGitHashFrom(src_path, rev) for rev in revisions
272*760c253cSXin Li            ]
273*760c253cSXin Li            return revisions, git_hashes
274*760c253cSXin Li
275*760c253cSXin Li
276*760c253cSXin Lidef Bisect(
277*760c253cSXin Li    revisions,
278*760c253cSXin Li    git_hashes,
279*760c253cSXin Li    bisect_state,
280*760c253cSXin Li    last_tested,
281*760c253cSXin Li    update_packages,
282*760c253cSXin Li    chromeos_path,
283*760c253cSXin Li    extra_change_lists,
284*760c253cSXin Li    options,
285*760c253cSXin Li    builder,
286*760c253cSXin Li):
287*760c253cSXin Li    """Adds tryjobs and updates the status file with the new tryjobs."""
288*760c253cSXin Li
289*760c253cSXin Li    try:
290*760c253cSXin Li        for svn_revision, git_hash in zip(revisions, git_hashes):
291*760c253cSXin Li            tryjob_dict = modify_a_tryjob.AddTryjob(
292*760c253cSXin Li                update_packages,
293*760c253cSXin Li                git_hash,
294*760c253cSXin Li                svn_revision,
295*760c253cSXin Li                chromeos_path,
296*760c253cSXin Li                extra_change_lists,
297*760c253cSXin Li                options,
298*760c253cSXin Li                builder,
299*760c253cSXin Li                svn_revision,
300*760c253cSXin Li            )
301*760c253cSXin Li
302*760c253cSXin Li            bisect_state["jobs"].append(tryjob_dict)
303*760c253cSXin Li    finally:
304*760c253cSXin Li        # Do not want to lose progress if there is an exception.
305*760c253cSXin Li        if last_tested:
306*760c253cSXin Li            new_file = "%s.new" % last_tested
307*760c253cSXin Li            with open(new_file, "w", encoding="utf-8") as json_file:
308*760c253cSXin Li                json.dump(
309*760c253cSXin Li                    bisect_state, json_file, indent=4, separators=(",", ": ")
310*760c253cSXin Li                )
311*760c253cSXin Li
312*760c253cSXin Li            os.rename(new_file, last_tested)
313*760c253cSXin Li
314*760c253cSXin Li
315*760c253cSXin Lidef LoadStatusFile(last_tested, start, end):
316*760c253cSXin Li    """Loads the status file for bisection."""
317*760c253cSXin Li
318*760c253cSXin Li    try:
319*760c253cSXin Li        with open(last_tested, encoding="utf-8") as f:
320*760c253cSXin Li            return json.load(f)
321*760c253cSXin Li    except IOError as err:
322*760c253cSXin Li        if err.errno != errno.ENOENT:
323*760c253cSXin Li            raise
324*760c253cSXin Li
325*760c253cSXin Li    return {"start": start, "end": end, "jobs": []}
326*760c253cSXin Li
327*760c253cSXin Li
328*760c253cSXin Lidef main(args_output):
329*760c253cSXin Li    """Bisects LLVM commits.
330*760c253cSXin Li
331*760c253cSXin Li    Raises:
332*760c253cSXin Li        AssertionError: The script was run inside the chroot.
333*760c253cSXin Li    """
334*760c253cSXin Li
335*760c253cSXin Li    chroot.VerifyOutsideChroot()
336*760c253cSXin Li    chroot.VerifyChromeOSRoot(args_output.chromeos_path)
337*760c253cSXin Li    start = args_output.start_rev
338*760c253cSXin Li    end = args_output.end_rev
339*760c253cSXin Li
340*760c253cSXin Li    bisect_state = LoadStatusFile(args_output.last_tested, start, end)
341*760c253cSXin Li    if start != bisect_state["start"] or end != bisect_state["end"]:
342*760c253cSXin Li        raise ValueError(
343*760c253cSXin Li            f"The start {start} or the end {end} version provided is "
344*760c253cSXin Li            f'different than "start" {bisect_state["start"]} or "end" '
345*760c253cSXin Li            f'{bisect_state["end"]} in the .JSON file'
346*760c253cSXin Li        )
347*760c253cSXin Li
348*760c253cSXin Li    # Pending and skipped revisions are between 'start_rev' and 'end_rev'.
349*760c253cSXin Li    start_rev, end_rev, pending_revs, skip_revs = GetRemainingRange(
350*760c253cSXin Li        start, end, bisect_state["jobs"]
351*760c253cSXin Li    )
352*760c253cSXin Li
353*760c253cSXin Li    revisions, git_hashes = GetCommitsBetween(
354*760c253cSXin Li        start_rev,
355*760c253cSXin Li        end_rev,
356*760c253cSXin Li        args_output.parallel,
357*760c253cSXin Li        args_output.src_path,
358*760c253cSXin Li        pending_revs,
359*760c253cSXin Li        skip_revs,
360*760c253cSXin Li    )
361*760c253cSXin Li
362*760c253cSXin Li    # No more revisions between 'start_rev' and 'end_rev', so
363*760c253cSXin Li    # bisection is complete.
364*760c253cSXin Li    #
365*760c253cSXin Li    # This is determined by finding all valid revisions between 'start_rev'
366*760c253cSXin Li    # and 'end_rev' and that are NOT in the 'pending' and 'skipped' set.
367*760c253cSXin Li    if not revisions:
368*760c253cSXin Li        if pending_revs:
369*760c253cSXin Li            # Some tryjobs are not finished which may change the actual bad
370*760c253cSXin Li            # commit/revision when those tryjobs are finished.
371*760c253cSXin Li            no_revisions_message = (
372*760c253cSXin Li                f"No revisions between start {start_rev} "
373*760c253cSXin Li                f"and end {end_rev} to create tryjobs\n"
374*760c253cSXin Li            )
375*760c253cSXin Li
376*760c253cSXin Li            if pending_revs:
377*760c253cSXin Li                no_revisions_message += (
378*760c253cSXin Li                    "The following tryjobs are pending:\n"
379*760c253cSXin Li                    + "\n".join(str(rev) for rev in pending_revs)
380*760c253cSXin Li                    + "\n"
381*760c253cSXin Li                )
382*760c253cSXin Li
383*760c253cSXin Li            if skip_revs:
384*760c253cSXin Li                no_revisions_message += (
385*760c253cSXin Li                    "The following tryjobs were skipped:\n"
386*760c253cSXin Li                    + "\n".join(str(rev) for rev in skip_revs)
387*760c253cSXin Li                    + "\n"
388*760c253cSXin Li                )
389*760c253cSXin Li
390*760c253cSXin Li            raise ValueError(no_revisions_message)
391*760c253cSXin Li
392*760c253cSXin Li        print(f"Finished bisecting for {args_output.last_tested}")
393*760c253cSXin Li        if args_output.src_path:
394*760c253cSXin Li            bad_llvm_hash = get_llvm_hash.GetGitHashFrom(
395*760c253cSXin Li                args_output.src_path, end_rev
396*760c253cSXin Li            )
397*760c253cSXin Li        else:
398*760c253cSXin Li            bad_llvm_hash = get_llvm_hash.LLVMHash().GetLLVMHash(end_rev)
399*760c253cSXin Li        print(
400*760c253cSXin Li            f"The bad revision is {end_rev} and its commit hash is "
401*760c253cSXin Li            f"{bad_llvm_hash}"
402*760c253cSXin Li        )
403*760c253cSXin Li        if skip_revs:
404*760c253cSXin Li            skip_revs_message = (
405*760c253cSXin Li                "\nThe following revisions were skipped:\n"
406*760c253cSXin Li                + "\n".join(str(rev) for rev in skip_revs)
407*760c253cSXin Li            )
408*760c253cSXin Li            print(skip_revs_message)
409*760c253cSXin Li
410*760c253cSXin Li        if args_output.cleanup:
411*760c253cSXin Li            # Abandon all the CLs created for bisection
412*760c253cSXin Li            gerrit = os.path.join(
413*760c253cSXin Li                args_output.chromeos_path, "chromite/bin/gerrit"
414*760c253cSXin Li            )
415*760c253cSXin Li            for build in bisect_state["jobs"]:
416*760c253cSXin Li                try:
417*760c253cSXin Li                    subprocess.check_output(
418*760c253cSXin Li                        [gerrit, "abandon", str(build["cl"])],
419*760c253cSXin Li                        stderr=subprocess.STDOUT,
420*760c253cSXin Li                        encoding="utf-8",
421*760c253cSXin Li                    )
422*760c253cSXin Li                except subprocess.CalledProcessError as err:
423*760c253cSXin Li                    # the CL may have been abandoned
424*760c253cSXin Li                    if "chromite.lib.gob_util.GOBError" not in err.output:
425*760c253cSXin Li                        raise
426*760c253cSXin Li
427*760c253cSXin Li        return BisectionExitStatus.BISECTION_COMPLETE.value
428*760c253cSXin Li
429*760c253cSXin Li    for rev in revisions:
430*760c253cSXin Li        if (
431*760c253cSXin Li            update_tryjob_status.FindTryjobIndex(rev, bisect_state["jobs"])
432*760c253cSXin Li            is not None
433*760c253cSXin Li        ):
434*760c253cSXin Li            raise ValueError(f'Revision {rev} exists already in "jobs"')
435*760c253cSXin Li
436*760c253cSXin Li    Bisect(
437*760c253cSXin Li        revisions,
438*760c253cSXin Li        git_hashes,
439*760c253cSXin Li        bisect_state,
440*760c253cSXin Li        args_output.last_tested,
441*760c253cSXin Li        update_chromeos_llvm_hash.DEFAULT_PACKAGES,
442*760c253cSXin Li        args_output.chromeos_path,
443*760c253cSXin Li        args_output.extra_change_lists,
444*760c253cSXin Li        args_output.options,
445*760c253cSXin Li        args_output.builder,
446*760c253cSXin Li    )
447*760c253cSXin Li
448*760c253cSXin Li
449*760c253cSXin Liif __name__ == "__main__":
450*760c253cSXin Li    sys.exit(main(GetCommandLineArgs()))
451