xref: /Lucene/dev-tools/scripts/releaseWizard.py (revision 3134f10a4255b5d766462251868a43e9bb9c03ce)
187c131baSJan Høydahl#!/usr/bin/env python3
287c131baSJan Høydahl# -*- coding: utf-8 -*-
387c131baSJan Høydahl# Licensed to the Apache Software Foundation (ASF) under one or more
487c131baSJan Høydahl# contributor license agreements.  See the NOTICE file distributed with
587c131baSJan Høydahl# this work for additional information regarding copyright ownership.
687c131baSJan Høydahl# The ASF licenses this file to You under the Apache License, Version 2.0
787c131baSJan Høydahl# (the "License"); you may not use this file except in compliance with
887c131baSJan Høydahl# the License.  You may obtain a copy of the License at
987c131baSJan Høydahl#
1087c131baSJan Høydahl#     http://www.apache.org/licenses/LICENSE-2.0
1187c131baSJan Høydahl#
1287c131baSJan Høydahl# Unless required by applicable law or agreed to in writing, software
1387c131baSJan Høydahl# distributed under the License is distributed on an "AS IS" BASIS,
1487c131baSJan Høydahl# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1587c131baSJan Høydahl# See the License for the specific language governing permissions and
1687c131baSJan Høydahl# limitations under the License.
1787c131baSJan Høydahl
1887c131baSJan Høydahl# This script is a wizard that aims to (some day) replace the todoList at https://wiki.apache.org/lucene-java/ReleaseTodo
1987c131baSJan Høydahl# It will walk you through the steps of the release process, asking for decisions or input along the way
2087c131baSJan Høydahl# CAUTION: This is an alpha version, please read the HELP section in the main menu.
2187c131baSJan Høydahl#
2287c131baSJan Høydahl# Requirements:
2387c131baSJan Høydahl#   Install requirements with this command:
2487c131baSJan Høydahl#   pip3 install -r requirements.txt
2587c131baSJan Høydahl#
2687c131baSJan Høydahl# Usage:
2787c131baSJan Høydahl#   releaseWizard.py [-h] [--dry-run] [--root PATH]
2887c131baSJan Høydahl#
2987c131baSJan Høydahl#   optional arguments:
3087c131baSJan Høydahl#   -h, --help   show this help message and exit
3187c131baSJan Høydahl#   --dry-run    Do not execute any commands, but echo them instead. Display
3287c131baSJan Høydahl#   extra debug info
3387c131baSJan Høydahl#   --root PATH  Specify different root folder than ~/.lucene-releases
3487c131baSJan Høydahl
3587c131baSJan Høydahlimport argparse
3687c131baSJan Høydahlimport copy
3787c131baSJan Høydahlimport fcntl
3887c131baSJan Høydahlimport json
3987c131baSJan Høydahlimport os
4087c131baSJan Høydahlimport platform
4187c131baSJan Høydahlimport re
4287c131baSJan Høydahlimport shlex
4387c131baSJan Høydahlimport shutil
4487c131baSJan Høydahlimport subprocess
4587c131baSJan Høydahlimport sys
4687c131baSJan Høydahlimport textwrap
4787c131baSJan Høydahlimport time
4887c131baSJan Høydahlimport urllib
4987c131baSJan Høydahlfrom collections import OrderedDict
5087c131baSJan Høydahlfrom datetime import datetime
5187c131baSJan Høydahlfrom datetime import timedelta
5287c131baSJan Høydahl
5387c131baSJan Høydahltry:
5487c131baSJan Høydahl    import holidays
5587c131baSJan Høydahl    import yaml
5687c131baSJan Høydahl    from ics import Calendar, Event
5787c131baSJan Høydahl    from jinja2 import Environment
5887c131baSJan Høydahlexcept:
5987c131baSJan Høydahl    print("You lack some of the module dependencies to run this script.")
6087c131baSJan Høydahl    print("Please run 'pip3 install -r requirements.txt' and try again.")
6187c131baSJan Høydahl    sys.exit(1)
6287c131baSJan Høydahl
6387c131baSJan Høydahlimport scriptutil
6487c131baSJan Høydahlfrom consolemenu import ConsoleMenu
6587c131baSJan Høydahlfrom consolemenu.items import FunctionItem, SubmenuItem, ExitItem
6687c131baSJan Høydahlfrom consolemenu.screen import Screen
67*3134f10aSMike Drobfrom scriptutil import BranchType, Version, download, run
6887c131baSJan Høydahl
6987c131baSJan Høydahl# Solr-to-Java version mapping
7087c131baSJan Høydahljava_versions = {6: 8, 7: 8, 8: 8, 9: 11}
71329e7c7bSJan Høydahleditor = None
7287c131baSJan Høydahl
7387c131baSJan Høydahl# Edit this to add other global jinja2 variables or filters
7487c131baSJan Høydahldef expand_jinja(text, vars=None):
7587c131baSJan Høydahl    global_vars = OrderedDict({
7687c131baSJan Høydahl        'script_version': state.script_version,
7787c131baSJan Høydahl        'release_version': state.release_version,
7887c131baSJan Høydahl        'release_version_underscore': state.release_version.replace('.', '_'),
7987c131baSJan Høydahl        'release_date': state.get_release_date(),
8087c131baSJan Høydahl        'ivy2_folder': os.path.expanduser("~/.ivy2/"),
8187c131baSJan Høydahl        'config_path': state.config_path,
8287c131baSJan Høydahl        'rc_number': state.rc_number,
8387c131baSJan Høydahl        'script_branch': state.script_branch,
8487c131baSJan Høydahl        'release_folder': state.get_release_folder(),
8587c131baSJan Høydahl        'git_checkout_folder': state.get_git_checkout_folder(),
86329e7c7bSJan Høydahl        'git_website_folder': state.get_website_git_folder(),
8787c131baSJan Høydahl        'dist_url_base': 'https://dist.apache.org/repos/dist/dev/lucene',
8887c131baSJan Høydahl        'm2_repository_url': 'https://repository.apache.org/service/local/staging/deploy/maven2',
8987c131baSJan Høydahl        'dist_file_path': state.get_dist_folder(),
9087c131baSJan Høydahl        'rc_folder': state.get_rc_folder(),
9187c131baSJan Høydahl        'base_branch': state.get_base_branch_name(),
9287c131baSJan Høydahl        'release_branch': state.release_branch,
9387c131baSJan Høydahl        'stable_branch': state.get_stable_branch_name(),
9487c131baSJan Høydahl        'minor_branch': state.get_minor_branch_name(),
9587c131baSJan Høydahl        'release_type': state.release_type,
9687c131baSJan Høydahl        'is_feature_release': state.release_type in ['minor', 'major'],
9787c131baSJan Høydahl        'release_version_major': state.release_version_major,
9887c131baSJan Høydahl        'release_version_minor': state.release_version_minor,
9987c131baSJan Høydahl        'release_version_bugfix': state.release_version_bugfix,
1000f06eff4SHouston Putman        'release_version_refguide': state.get_refguide_release() ,
10187c131baSJan Høydahl        'state': state,
102daf14981SAlan Woodward        'gpg_key' : state.get_gpg_key(),
103*3134f10aSMike Drob        'gradle_cmd' : 'gradlew.bat' if is_windows() else './gradlew',
10487c131baSJan Høydahl        'epoch': unix_time_millis(datetime.utcnow()),
10587c131baSJan Høydahl        'get_next_version': state.get_next_version(),
10687c131baSJan Høydahl        'current_git_rev': state.get_current_git_rev(),
10787c131baSJan Høydahl        'keys_downloaded': keys_downloaded(),
10887c131baSJan Høydahl        'editor': get_editor(),
10987c131baSJan Høydahl        'rename_cmd': 'ren' if is_windows() else 'mv',
11087c131baSJan Høydahl        'vote_close_72h': vote_close_72h_date().strftime("%Y-%m-%d %H:00 UTC"),
11187c131baSJan Høydahl        'vote_close_72h_epoch': unix_time_millis(vote_close_72h_date()),
112d86b473aSJan Høydahl        'vote_close_72h_holidays': vote_close_72h_holidays(),
11387c131baSJan Høydahl        'lucene_news_file': lucene_news_file,
11487c131baSJan Høydahl        'solr_news_file': solr_news_file,
11587c131baSJan Høydahl        'load_lines': load_lines,
11687c131baSJan Høydahl        'set_java_home': set_java_home,
11787c131baSJan Høydahl        'latest_version': state.get_latest_version(),
11887c131baSJan Høydahl        'latest_lts_version': state.get_latest_lts_version(),
11987c131baSJan Høydahl        'master_version': state.get_master_version(),
12087c131baSJan Høydahl        'mirrored_versions': state.get_mirrored_versions(),
12187c131baSJan Høydahl        'mirrored_versions_to_delete': state.get_mirrored_versions_to_delete(),
12287c131baSJan Høydahl        'home': os.path.expanduser("~")
12387c131baSJan Høydahl    })
12487c131baSJan Høydahl    global_vars.update(state.get_todo_states())
12587c131baSJan Høydahl    if vars:
12687c131baSJan Høydahl        global_vars.update(vars)
12787c131baSJan Høydahl
12887c131baSJan Høydahl    filled = replace_templates(text)
12987c131baSJan Høydahl
13087c131baSJan Høydahl    try:
13187c131baSJan Høydahl        env = Environment(lstrip_blocks=True, keep_trailing_newline=False, trim_blocks=True)
13287c131baSJan Høydahl        env.filters['path_join'] = lambda paths: os.path.join(*paths)
13387c131baSJan Høydahl        env.filters['expanduser'] = lambda path: os.path.expanduser(path)
13487c131baSJan Høydahl        env.filters['formatdate'] = lambda date: (datetime.strftime(date, "%-d %B %Y") if date else "<date>" )
13587c131baSJan Høydahl        template = env.from_string(str(filled), globals=global_vars)
13687c131baSJan Høydahl        filled = template.render()
13787c131baSJan Høydahl    except Exception as e:
13887c131baSJan Høydahl        print("Exception while rendering jinja template %s: %s" % (str(filled)[:10], e))
13987c131baSJan Høydahl    return filled
14087c131baSJan Høydahl
14187c131baSJan Høydahl
14287c131baSJan Høydahldef replace_templates(text):
14387c131baSJan Høydahl    tpl_lines = []
14487c131baSJan Høydahl    for line in text.splitlines():
14587c131baSJan Høydahl        if line.startswith("(( template="):
14687c131baSJan Høydahl            match = re.search(r"^\(\( template=(.+?) \)\)", line)
14787c131baSJan Høydahl            name = match.group(1)
14887c131baSJan Høydahl            tpl_lines.append(replace_templates(templates[name].strip()))
14987c131baSJan Høydahl        else:
15087c131baSJan Høydahl            tpl_lines.append(line)
15187c131baSJan Høydahl    return "\n".join(tpl_lines)
15287c131baSJan Høydahl
15387c131baSJan Høydahl
15487c131baSJan Høydahldef getScriptVersion():
15587c131baSJan Høydahl    topLevelDir = os.path.join(os.path.abspath("%s/" % script_path), os.path.pardir, os.path.pardir)
15687c131baSJan Høydahl    reBaseVersion = re.compile(r'version\.base\s*=\s*(\d+\.\d+\.\d+)')
15787c131baSJan Høydahl    return reBaseVersion.search(open('%s/lucene/version.properties' % topLevelDir).read()).group(1)
15887c131baSJan Høydahl
15987c131baSJan Høydahl
16087c131baSJan Høydahldef get_editor():
161329e7c7bSJan Høydahl    global editor
162329e7c7bSJan Høydahl    if editor is None:
163329e7c7bSJan Høydahl      if 'EDITOR' in os.environ:
164329e7c7bSJan Høydahl          if os.environ['EDITOR'] in ['vi', 'vim', 'nano', 'pico', 'emacs']:
165329e7c7bSJan Høydahl              print("WARNING: You have EDITOR set to %s, which will not work when launched from this tool. Please use an editor that launches a separate window/process" % os.environ['EDITOR'])
166329e7c7bSJan Høydahl          editor = os.environ['EDITOR']
167329e7c7bSJan Høydahl      elif is_windows():
168329e7c7bSJan Høydahl          editor = 'notepad.exe'
169329e7c7bSJan Høydahl      elif is_mac():
170329e7c7bSJan Høydahl          editor = 'open -a TextEdit'
171329e7c7bSJan Høydahl      else:
172329e7c7bSJan Høydahl          sys.exit("On Linux you have to set EDITOR variable to a command that will start an editor in its own window")
173329e7c7bSJan Høydahl    return editor
17487c131baSJan Høydahl
17587c131baSJan Høydahl
17687c131baSJan Høydahldef check_prerequisites(todo=None):
17787c131baSJan Høydahl    if sys.version_info < (3, 4):
17887c131baSJan Høydahl        sys.exit("Script requires Python v3.4 or later")
17987c131baSJan Høydahl    try:
18087c131baSJan Høydahl        gpg_ver = run("gpg --version").splitlines()[0]
18187c131baSJan Høydahl    except:
18287c131baSJan Høydahl        sys.exit("You will need gpg installed")
18326a32d79SAlan Woodward    if not 'GPG_TTY' in os.environ:
184674c2c28SMike Drob        print("WARNING: GPG_TTY environment variable is not set, GPG signing may not work correctly (try 'export GPG_TTY=$(tty)'")
185*3134f10aSMike Drob    if not 'JAVA11_HOME' in os.environ:
186*3134f10aSMike Drob        sys.exit("Please set environment variables JAVA11_HOME")
18787c131baSJan Høydahl    try:
18887c131baSJan Høydahl        asciidoc_ver = run("asciidoctor -V").splitlines()[0]
18987c131baSJan Høydahl    except:
19087c131baSJan Høydahl        asciidoc_ver = ""
19187c131baSJan Høydahl        print("WARNING: In order to export asciidoc version to HTML, you will need asciidoctor installed")
19287c131baSJan Høydahl    try:
19387c131baSJan Høydahl        git_ver = run("git --version").splitlines()[0]
19487c131baSJan Høydahl    except:
19587c131baSJan Høydahl        sys.exit("You will need git installed")
19687c131baSJan Høydahl    if not 'EDITOR' in os.environ:
19787c131baSJan Høydahl        print("WARNING: Environment variable $EDITOR not set, using %s" % get_editor())
19887c131baSJan Høydahl
19987c131baSJan Høydahl    if todo:
20087c131baSJan Høydahl        print("%s\n%s\n%s\n" % (gpg_ver, asciidoc_ver, git_ver))
20187c131baSJan Høydahl    return True
20287c131baSJan Høydahl
20387c131baSJan Høydahl
20487c131baSJan Høydahlepoch = datetime.utcfromtimestamp(0)
20587c131baSJan Høydahl
20687c131baSJan Høydahl
20787c131baSJan Høydahldef unix_time_millis(dt):
20887c131baSJan Høydahl    return int((dt - epoch).total_seconds() * 1000.0)
20987c131baSJan Høydahl
21087c131baSJan Høydahl
21187c131baSJan Høydahldef bootstrap_todos(todo_list):
21287c131baSJan Høydahl    # Establish links from commands to to_do for finding todo vars
21387c131baSJan Høydahl    for tg in todo_list:
21487c131baSJan Høydahl        if dry_run:
21587c131baSJan Høydahl            print("Group %s" % tg.id)
21687c131baSJan Høydahl        for td in tg.get_todos():
21787c131baSJan Høydahl            if dry_run:
21887c131baSJan Høydahl                print("  Todo %s" % td.id)
21987c131baSJan Høydahl            cmds = td.commands
22087c131baSJan Høydahl            if cmds:
22187c131baSJan Høydahl                if dry_run:
22287c131baSJan Høydahl                    print("  Commands")
22387c131baSJan Høydahl                cmds.todo_id = td.id
22487c131baSJan Høydahl                for cmd in cmds.commands:
22587c131baSJan Høydahl                    if dry_run:
22687c131baSJan Høydahl                        print("    Command %s" % cmd.cmd)
22787c131baSJan Høydahl                    cmd.todo_id = td.id
22887c131baSJan Høydahl
22987c131baSJan Høydahl    print("Loaded TODO definitions from releaseWizard.yaml")
23087c131baSJan Høydahl    return todo_list
23187c131baSJan Høydahl
23287c131baSJan Høydahl
23387c131baSJan Høydahldef maybe_remove_rc_from_svn():
23487c131baSJan Høydahl    todo = state.get_todo_by_id('import_svn')
23587c131baSJan Høydahl    if todo and todo.is_done():
23687c131baSJan Høydahl        print("import_svn done")
23787c131baSJan Høydahl        Commands(state.get_git_checkout_folder(),
23887c131baSJan Høydahl                 """Looks like you uploaded artifacts for {{ build_rc.git_rev | default("<git_rev>", True) }} to svn which needs to be removed.""",
23987c131baSJan Høydahl                 [Command(
24087c131baSJan Høydahl                 """svn -m "Remove cancelled Lucene/Solr {{ release_version }} RC{{ rc_number }}" rm {{ dist_url }}""",
24187c131baSJan Høydahl                 logfile="svn_rm.log",
24287c131baSJan Høydahl                 tee=True,
24387c131baSJan Høydahl                 vars={
24487c131baSJan Høydahl                     'dist_folder': """lucene-solr-{{ release_version }}-RC{{ rc_number }}-rev{{ build_rc.git_rev | default("<git_rev>", True) }}""",
24587c131baSJan Høydahl                     'dist_url': "{{ dist_url_base }}/{{ dist_folder }}"
24687c131baSJan Høydahl                 }
24787c131baSJan Høydahl             )],
24887c131baSJan Høydahl                 enable_execute=True, confirm_each_command=False).run()
24987c131baSJan Høydahl
25087c131baSJan Høydahl
25187c131baSJan Høydahl# To be able to hide fields when dumping Yaml
25287c131baSJan Høydahlclass SecretYamlObject(yaml.YAMLObject):
25387c131baSJan Høydahl    hidden_fields = []
25487c131baSJan Høydahl    @classmethod
25587c131baSJan Høydahl    def to_yaml(cls,dumper,data):
25687c131baSJan Høydahl        print("Dumping object %s" % type(data))
25787c131baSJan Høydahl
25887c131baSJan Høydahl        new_data = copy.deepcopy(data)
25987c131baSJan Høydahl        for item in cls.hidden_fields:
26087c131baSJan Høydahl            if item in new_data.__dict__:
26187c131baSJan Høydahl                del new_data.__dict__[item]
26287c131baSJan Høydahl        for item in data.__dict__:
26387c131baSJan Høydahl            if item in new_data.__dict__ and new_data.__dict__[item] is None:
26487c131baSJan Høydahl                del new_data.__dict__[item]
26587c131baSJan Høydahl        return dumper.represent_yaml_object(cls.yaml_tag, new_data, cls,
26687c131baSJan Høydahl                                            flow_style=cls.yaml_flow_style)
26787c131baSJan Høydahl
26887c131baSJan Høydahl
26987c131baSJan Høydahldef str_presenter(dumper, data):
27087c131baSJan Høydahl    if len(data.split('\n')) > 1:  # check for multiline string
27187c131baSJan Høydahl        return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|')
27287c131baSJan Høydahl    return dumper.represent_scalar('tag:yaml.org,2002:str', data)
27387c131baSJan Høydahl
27487c131baSJan Høydahl
27587c131baSJan Høydahlclass ReleaseState:
27687c131baSJan Høydahl    def __init__(self, config_path, release_version, script_version):
27787c131baSJan Høydahl        self.script_version = script_version
27887c131baSJan Høydahl        self.config_path = config_path
27987c131baSJan Høydahl        self.todo_groups = None
28087c131baSJan Høydahl        self.todos = None
28187c131baSJan Høydahl        self.latest_version = None
28287c131baSJan Høydahl        self.previous_rcs = {}
28387c131baSJan Høydahl        self.rc_number = 1
28487c131baSJan Høydahl        self.start_date = unix_time_millis(datetime.utcnow())
28587c131baSJan Høydahl        self.script_branch = run("git rev-parse --abbrev-ref HEAD").strip()
28687c131baSJan Høydahl        self.mirrored_versions = None
28787c131baSJan Høydahl        try:
28887c131baSJan Høydahl            self.script_branch_type = scriptutil.find_branch_type()
28987c131baSJan Høydahl        except:
29087c131baSJan Høydahl            print("WARNING: This script shold (ideally) run from the release branch, not a feature branch (%s)" % self.script_branch)
29187c131baSJan Høydahl            self.script_branch_type = 'feature'
29287c131baSJan Høydahl        self.set_release_version(release_version)
29387c131baSJan Høydahl
29487c131baSJan Høydahl    def set_release_version(self, version):
29587c131baSJan Høydahl        self.validate_release_version(self.script_branch_type, self.script_branch, version)
29687c131baSJan Høydahl        self.release_version = version
29787c131baSJan Høydahl        v = Version.parse(version)
29887c131baSJan Høydahl        self.release_version_major = v.major
29987c131baSJan Høydahl        self.release_version_minor = v.minor
30087c131baSJan Høydahl        self.release_version_bugfix = v.bugfix
30187c131baSJan Høydahl        self.release_branch = "branch_%s_%s" % (v.major, v.minor)
30287c131baSJan Høydahl        if v.is_major_release():
30387c131baSJan Høydahl            self.release_type = 'major'
30487c131baSJan Høydahl        elif v.is_minor_release():
30587c131baSJan Høydahl            self.release_type = 'minor'
30687c131baSJan Høydahl        else:
30787c131baSJan Høydahl            self.release_type = 'bugfix'
30887c131baSJan Høydahl
30987c131baSJan Høydahl    def is_released(self):
31087c131baSJan Høydahl        return self.get_todo_by_id('announce_lucene').is_done()
31187c131baSJan Høydahl
312daf14981SAlan Woodward    def get_gpg_key(self):
313daf14981SAlan Woodward        gpg_task = self.get_todo_by_id('gpg')
314daf14981SAlan Woodward        if gpg_task.is_done():
315daf14981SAlan Woodward            return gpg_task.get_state()['gpg_key']
316daf14981SAlan Woodward        else:
317daf14981SAlan Woodward            return None
318daf14981SAlan Woodward
31987c131baSJan Høydahl    def get_release_date(self):
32087c131baSJan Høydahl        publish_task = self.get_todo_by_id('publish_maven')
32187c131baSJan Høydahl        if publish_task.is_done():
32287c131baSJan Høydahl            return unix_to_datetime(publish_task.get_state()['done_date'])
32387c131baSJan Høydahl        else:
32487c131baSJan Høydahl            return None
32587c131baSJan Høydahl
326329e7c7bSJan Høydahl    def get_release_date_iso(self):
327329e7c7bSJan Høydahl        release_date = self.get_release_date()
328329e7c7bSJan Høydahl        if release_date is None:
329329e7c7bSJan Høydahl            return "yyyy-mm-dd"
330329e7c7bSJan Høydahl        else:
331329e7c7bSJan Høydahl            return release_date.isoformat()[:10]
332329e7c7bSJan Høydahl
33387c131baSJan Høydahl    def get_latest_version(self):
33487c131baSJan Høydahl        if self.latest_version is None:
33587c131baSJan Høydahl            versions = self.get_mirrored_versions()
33687c131baSJan Høydahl            latest = versions[0]
33787c131baSJan Høydahl            for ver in versions:
33887c131baSJan Høydahl                if Version.parse(ver).gt(Version.parse(latest)):
33987c131baSJan Høydahl                    latest = ver
34087c131baSJan Høydahl            self.latest_version = latest
34187c131baSJan Høydahl            self.save()
34287c131baSJan Høydahl        return state.latest_version
34387c131baSJan Høydahl
34487c131baSJan Høydahl    def get_mirrored_versions(self):
34587c131baSJan Høydahl        if state.mirrored_versions is None:
34687c131baSJan Høydahl            releases_str = load("https://projects.apache.org/json/foundation/releases.json", "utf-8")
34787c131baSJan Høydahl            releases = json.loads(releases_str)['lucene']
34887c131baSJan Høydahl            state.mirrored_versions = [ r for r in list(map(lambda y: y[7:], filter(lambda x: x.startswith('lucene-'), list(releases.keys())))) ]
34987c131baSJan Høydahl        return state.mirrored_versions
35087c131baSJan Høydahl
35187c131baSJan Høydahl    def get_mirrored_versions_to_delete(self):
35287c131baSJan Høydahl        versions = self.get_mirrored_versions()
35387c131baSJan Høydahl        to_keep = versions
35487c131baSJan Høydahl        if state.release_type == 'major':
35587c131baSJan Høydahl          to_keep = [self.release_version, self.get_latest_version()]
35687c131baSJan Høydahl        if state.release_type == 'minor':
35787c131baSJan Høydahl          to_keep = [self.release_version, self.get_latest_lts_version()]
35887c131baSJan Høydahl        if state.release_type == 'bugfix':
35987c131baSJan Høydahl          if Version.parse(state.release_version).major == Version.parse(state.get_latest_version()).major:
36087c131baSJan Høydahl            to_keep = [self.release_version, self.get_latest_lts_version()]
36187c131baSJan Høydahl          elif Version.parse(state.release_version).major == Version.parse(state.get_latest_lts_version()).major:
362329e7c7bSJan Høydahl            to_keep = [self.get_latest_version(), self.release_version]
36387c131baSJan Høydahl          else:
36487c131baSJan Høydahl            raise Exception("Release version %s must have same major version as current minor or lts release")
36587c131baSJan Høydahl        return [ver for ver in versions if ver not in to_keep]
36687c131baSJan Høydahl
36787c131baSJan Høydahl    def get_master_version(self):
36887c131baSJan Høydahl        v = Version.parse(self.get_latest_version())
36987c131baSJan Høydahl        return "%s.%s.%s" % (v.major + 1, 0, 0)
37087c131baSJan Høydahl
37187c131baSJan Høydahl    def get_latest_lts_version(self):
37287c131baSJan Høydahl        versions = self.get_mirrored_versions()
37387c131baSJan Høydahl        latest = self.get_latest_version()
37487c131baSJan Høydahl        lts_prefix = "%s." % (Version.parse(latest).major - 1)
37587c131baSJan Høydahl        lts_versions = list(filter(lambda x: x.startswith(lts_prefix), versions))
37687c131baSJan Høydahl        latest_lts = lts_versions[0]
37787c131baSJan Høydahl        for ver in lts_versions:
37887c131baSJan Høydahl            if Version.parse(ver).gt(Version.parse(latest_lts)):
37987c131baSJan Høydahl                latest_lts = ver
38087c131baSJan Høydahl        return latest_lts
38187c131baSJan Høydahl
38287c131baSJan Høydahl    def validate_release_version(self, branch_type, branch, release_version):
38387c131baSJan Høydahl        ver = Version.parse(release_version)
38487c131baSJan Høydahl        # print("release_version=%s, ver=%s" % (release_version, ver))
38587c131baSJan Høydahl        if branch_type == BranchType.release:
38687c131baSJan Høydahl            if not branch.startswith('branch_'):
38787c131baSJan Høydahl                sys.exit("Incompatible branch and branch_type")
38887c131baSJan Høydahl            if not ver.is_bugfix_release():
38987c131baSJan Høydahl                sys.exit("You can only release bugfix releases from an existing release branch")
39087c131baSJan Høydahl        elif branch_type == BranchType.stable:
39187c131baSJan Høydahl            if not branch.startswith('branch_') and branch.endswith('x'):
39287c131baSJan Høydahl                sys.exit("Incompatible branch and branch_type")
39387c131baSJan Høydahl            if not ver.is_minor_release():
39487c131baSJan Høydahl                sys.exit("You can only release minor releases from an existing stable branch")
39587c131baSJan Høydahl        elif branch_type == BranchType.unstable:
39687c131baSJan Høydahl            if not branch == 'master':
39787c131baSJan Høydahl                sys.exit("Incompatible branch and branch_type")
39887c131baSJan Høydahl            if not ver.is_major_release():
39987c131baSJan Høydahl                sys.exit("You can only release a new major version from master branch")
40087c131baSJan Høydahl        if not getScriptVersion() == release_version:
40187c131baSJan Høydahl            print("WARNING: Expected release version %s when on branch %s, but got %s" % (
40287c131baSJan Høydahl                getScriptVersion(), branch, release_version))
40387c131baSJan Høydahl
40487c131baSJan Høydahl    def get_base_branch_name(self):
40587c131baSJan Høydahl        v = Version.parse(self.release_version)
40687c131baSJan Høydahl        if v.is_major_release():
40787c131baSJan Høydahl            return 'master'
40887c131baSJan Høydahl        elif v.is_minor_release():
40987c131baSJan Høydahl            return self.get_stable_branch_name()
41087c131baSJan Høydahl        elif v.major == Version.parse(self.get_latest_version()).major:
41187c131baSJan Høydahl            return self.get_minor_branch_name()
41287c131baSJan Høydahl        else:
41387c131baSJan Høydahl            return self.release_branch
41487c131baSJan Høydahl
41587c131baSJan Høydahl    def clear_rc(self):
41687c131baSJan Høydahl        if ask_yes_no("Are you sure? This will clear and restart RC%s" % self.rc_number):
41787c131baSJan Høydahl            maybe_remove_rc_from_svn()
41887c131baSJan Høydahl            dict = {}
41987c131baSJan Høydahl            for g in list(filter(lambda x: x.in_rc_loop(), self.todo_groups)):
42087c131baSJan Høydahl                for t in g.get_todos():
42187c131baSJan Høydahl                    t.clear()
42287c131baSJan Høydahl            print("Cleared RC TODO state")
42387c131baSJan Høydahl            try:
42487c131baSJan Høydahl                shutil.rmtree(self.get_rc_folder())
42587c131baSJan Høydahl                print("Cleared folder %s" % self.get_rc_folder())
42687c131baSJan Høydahl            except Exception as e:
42787c131baSJan Høydahl                print("WARN: Failed to clear %s, please do it manually with higher privileges" % self.get_rc_folder())
42887c131baSJan Høydahl            self.save()
42987c131baSJan Høydahl
43087c131baSJan Høydahl    def new_rc(self):
43187c131baSJan Høydahl        if ask_yes_no("Are you sure? This will abort current RC"):
43287c131baSJan Høydahl            maybe_remove_rc_from_svn()
43387c131baSJan Høydahl            dict = {}
43487c131baSJan Høydahl            for g in list(filter(lambda x: x.in_rc_loop(), self.todo_groups)):
43587c131baSJan Høydahl                for t in g.get_todos():
43687c131baSJan Høydahl                    if t.applies(self.release_type):
43787c131baSJan Høydahl                        dict[t.id] = copy.deepcopy(t.state)
43887c131baSJan Høydahl                        t.clear()
43987c131baSJan Høydahl            self.previous_rcs["RC%d" % self.rc_number] = dict
44087c131baSJan Høydahl            self.rc_number += 1
44187c131baSJan Høydahl            self.save()
44287c131baSJan Høydahl
44387c131baSJan Høydahl    def to_dict(self):
44487c131baSJan Høydahl        tmp_todos = {}
44587c131baSJan Høydahl        for todo_id in self.todos:
44687c131baSJan Høydahl            t = self.todos[todo_id]
44787c131baSJan Høydahl            tmp_todos[todo_id] = copy.deepcopy(t.state)
44887c131baSJan Høydahl        dict = {
44987c131baSJan Høydahl            'script_version': self.script_version,
45087c131baSJan Høydahl            'release_version': self.release_version,
45187c131baSJan Høydahl            'start_date': self.start_date,
45287c131baSJan Høydahl            'rc_number': self.rc_number,
45387c131baSJan Høydahl            'script_branch': self.script_branch,
45487c131baSJan Høydahl            'todos': tmp_todos,
45587c131baSJan Høydahl            'previous_rcs': self.previous_rcs
45687c131baSJan Høydahl        }
45787c131baSJan Høydahl        if self.latest_version:
45887c131baSJan Høydahl            dict['latest_version'] = self.latest_version
45987c131baSJan Høydahl        return dict
46087c131baSJan Høydahl
46187c131baSJan Høydahl    def restore_from_dict(self, dict):
46287c131baSJan Høydahl        self.script_version = dict['script_version']
46387c131baSJan Høydahl        assert dict['release_version'] == self.release_version
46487c131baSJan Høydahl        if 'start_date' in dict:
46587c131baSJan Høydahl            self.start_date = dict['start_date']
46687c131baSJan Høydahl        if 'latest_version' in dict:
46787c131baSJan Høydahl            self.latest_version = dict['latest_version']
46887c131baSJan Høydahl        else:
46987c131baSJan Høydahl            self.latest_version = None
47087c131baSJan Høydahl        self.rc_number = dict['rc_number']
47187c131baSJan Høydahl        self.script_branch = dict['script_branch']
47287c131baSJan Høydahl        self.previous_rcs = copy.deepcopy(dict['previous_rcs'])
47387c131baSJan Høydahl        for todo_id in dict['todos']:
47487c131baSJan Høydahl            if todo_id in self.todos:
47587c131baSJan Høydahl                t = self.todos[todo_id]
47687c131baSJan Høydahl                for k in dict['todos'][todo_id]:
47787c131baSJan Høydahl                    t.state[k] = dict['todos'][todo_id][k]
47887c131baSJan Høydahl            else:
47987c131baSJan Høydahl                print("Warning: Could not restore state for %s, Todo definition not found" % todo_id)
48087c131baSJan Høydahl
48187c131baSJan Høydahl    def load(self):
48287c131baSJan Høydahl        if os.path.exists(os.path.join(self.config_path, self.release_version, 'state.yaml')):
48387c131baSJan Høydahl            state_file = os.path.join(self.config_path, self.release_version, 'state.yaml')
48487c131baSJan Høydahl            with open(state_file, 'r') as fp:
48587c131baSJan Høydahl                try:
48687c131baSJan Høydahl                    dict = yaml.load(fp, Loader=yaml.Loader)
48787c131baSJan Høydahl                    self.restore_from_dict(dict)
48887c131baSJan Høydahl                    print("Loaded state from %s" % state_file)
48987c131baSJan Høydahl                except Exception as e:
49087c131baSJan Høydahl                    print("Failed to load state from %s: %s" % (state_file, e))
49187c131baSJan Høydahl
49287c131baSJan Høydahl    def save(self):
49387c131baSJan Høydahl        print("Saving")
49487c131baSJan Høydahl        if not os.path.exists(os.path.join(self.config_path, self.release_version)):
49587c131baSJan Høydahl            print("Creating folder %s" % os.path.join(self.config_path, self.release_version))
49687c131baSJan Høydahl            os.makedirs(os.path.join(self.config_path, self.release_version))
49787c131baSJan Høydahl
49887c131baSJan Høydahl        with open(os.path.join(self.config_path, self.release_version, 'state.yaml'), 'w') as fp:
49987c131baSJan Høydahl            yaml.dump(self.to_dict(), fp, sort_keys=False, default_flow_style=False)
50087c131baSJan Høydahl
50187c131baSJan Høydahl    def clear(self):
50287c131baSJan Høydahl        self.previous_rcs = {}
50387c131baSJan Høydahl        self.rc_number = 1
50487c131baSJan Høydahl        for t_id in self.todos:
50587c131baSJan Høydahl            t = self.todos[t_id]
50687c131baSJan Høydahl            t.state = {}
50787c131baSJan Høydahl        self.save()
50887c131baSJan Høydahl
50987c131baSJan Høydahl    def get_rc_number(self):
51087c131baSJan Høydahl        return self.rc_number
51187c131baSJan Høydahl
51287c131baSJan Høydahl    def get_current_git_rev(self):
51387c131baSJan Høydahl        try:
51487c131baSJan Høydahl            return run("git rev-parse HEAD", cwd=self.get_git_checkout_folder()).strip()
51587c131baSJan Høydahl        except:
51687c131baSJan Høydahl            return "<git-rev>"
51787c131baSJan Høydahl
51887c131baSJan Høydahl    def get_group_by_id(self, id):
51987c131baSJan Høydahl        lst = list(filter(lambda x: x.id == id, self.todo_groups))
52087c131baSJan Høydahl        if len(lst) == 1:
52187c131baSJan Høydahl            return lst[0]
52287c131baSJan Høydahl        else:
52387c131baSJan Høydahl            return None
52487c131baSJan Høydahl
52587c131baSJan Høydahl    def get_todo_by_id(self, id):
52687c131baSJan Høydahl        lst = list(filter(lambda x: x.id == id, self.todos.values()))
52787c131baSJan Høydahl        if len(lst) == 1:
52887c131baSJan Høydahl            return lst[0]
52987c131baSJan Høydahl        else:
53087c131baSJan Høydahl            return None
53187c131baSJan Høydahl
53287c131baSJan Høydahl    def get_todo_state_by_id(self, id):
53387c131baSJan Høydahl        lst = list(filter(lambda x: x.id == id, self.todos.values()))
53487c131baSJan Høydahl        if len(lst) == 1:
53587c131baSJan Høydahl            return lst[0].state
53687c131baSJan Høydahl        else:
53787c131baSJan Høydahl            return {}
53887c131baSJan Høydahl
53987c131baSJan Høydahl    def get_release_folder(self):
54087c131baSJan Høydahl        folder = os.path.join(self.config_path, self.release_version)
54187c131baSJan Høydahl        if not os.path.exists(folder):
54287c131baSJan Høydahl            print("Creating folder %s" % folder)
54387c131baSJan Høydahl            os.makedirs(folder)
54487c131baSJan Høydahl        return folder
54587c131baSJan Høydahl
54687c131baSJan Høydahl    def get_rc_folder(self):
54787c131baSJan Høydahl        folder = os.path.join(self.get_release_folder(), "RC%d" % self.rc_number)
54887c131baSJan Høydahl        if not os.path.exists(folder):
54987c131baSJan Høydahl            print("Creating folder %s" % folder)
55087c131baSJan Høydahl            os.makedirs(folder)
55187c131baSJan Høydahl        return folder
55287c131baSJan Høydahl
55387c131baSJan Høydahl    def get_dist_folder(self):
55487c131baSJan Høydahl        folder = os.path.join(self.get_rc_folder(), "dist")
55587c131baSJan Høydahl        return folder
55687c131baSJan Høydahl
55787c131baSJan Høydahl    def get_git_checkout_folder(self):
55887c131baSJan Høydahl        folder = os.path.join(self.get_release_folder(), "lucene-solr")
55987c131baSJan Høydahl        return folder
56087c131baSJan Høydahl
561329e7c7bSJan Høydahl    def get_website_git_folder(self):
562329e7c7bSJan Høydahl        folder = os.path.join(self.get_release_folder(), "lucene-site")
563329e7c7bSJan Høydahl        return folder
564329e7c7bSJan Høydahl
56587c131baSJan Høydahl    def get_minor_branch_name(self):
56687c131baSJan Høydahl        latest = state.get_latest_version()
56787c131baSJan Høydahl        if latest is not None:
56887c131baSJan Høydahl          v = Version.parse(latest)
56987c131baSJan Høydahl          return "branch_%s_%s" % (v.major, v.minor)
57087c131baSJan Høydahl        else:
57187c131baSJan Høydahl            raise Exception("Cannot find latest version")
57287c131baSJan Høydahl
57387c131baSJan Høydahl    def get_stable_branch_name(self):
574*3134f10aSMike Drob        if self.release_type == 'major':
575*3134f10aSMike Drob            v = Version.parse(self.get_master_version())
576*3134f10aSMike Drob        else:
57787c131baSJan Høydahl            v = Version.parse(self.get_latest_version())
57887c131baSJan Høydahl        return "branch_%sx" % v.major
57987c131baSJan Høydahl
58087c131baSJan Høydahl    def get_next_version(self):
58187c131baSJan Høydahl        if self.release_type == 'major':
582*3134f10aSMike Drob            return "%s.0.0" % (self.release_version_major + 1)
58387c131baSJan Høydahl        if self.release_type == 'minor':
584*3134f10aSMike Drob            return "%s.%s.0" % (self.release_version_major, self.release_version_minor + 1)
58587c131baSJan Høydahl        if self.release_type == 'bugfix':
58687c131baSJan Høydahl            return "%s.%s.%s" % (self.release_version_major, self.release_version_minor, self.release_version_bugfix + 1)
58787c131baSJan Høydahl
5880f06eff4SHouston Putman    def get_refguide_release(self):
5890f06eff4SHouston Putman        return "%s_%s" % (self.release_version_major, self.release_version_minor)
5900f06eff4SHouston Putman
59187c131baSJan Høydahl    def get_java_home(self):
59287c131baSJan Høydahl        return self.get_java_home_for_version(self.release_version)
59387c131baSJan Høydahl
59487c131baSJan Høydahl    def get_java_home_for_version(self, version):
59587c131baSJan Høydahl        v = Version.parse(version)
59687c131baSJan Høydahl        java_ver = java_versions[v.major]
59787c131baSJan Høydahl        java_home_var = "JAVA%s_HOME" % java_ver
59887c131baSJan Høydahl        if java_home_var in os.environ:
59987c131baSJan Høydahl            return os.environ.get(java_home_var)
60087c131baSJan Høydahl        else:
60187c131baSJan Høydahl            raise Exception("Script needs environment variable %s" % java_home_var )
60287c131baSJan Høydahl
60387c131baSJan Høydahl    def get_java_cmd_for_version(self, version):
60487c131baSJan Høydahl        return os.path.join(self.get_java_home_for_version(version), "bin", "java")
60587c131baSJan Høydahl
60687c131baSJan Høydahl    def get_java_cmd(self):
60787c131baSJan Høydahl        return os.path.join(self.get_java_home(), "bin", "java")
60887c131baSJan Høydahl
60987c131baSJan Høydahl    def get_todo_states(self):
61087c131baSJan Høydahl        states = {}
61187c131baSJan Høydahl        if self.todos:
61287c131baSJan Høydahl            for todo_id in self.todos:
61387c131baSJan Høydahl                t = self.todos[todo_id]
61487c131baSJan Høydahl                states[todo_id] = copy.deepcopy(t.state)
61587c131baSJan Høydahl        return states
61687c131baSJan Høydahl
61787c131baSJan Høydahl    def init_todos(self, groups):
61887c131baSJan Høydahl        self.todo_groups = groups
61987c131baSJan Høydahl        self.todos = {}
62087c131baSJan Høydahl        for g in self.todo_groups:
62187c131baSJan Høydahl            for t in g.get_todos():
62287c131baSJan Høydahl                self.todos[t.id] = t
62387c131baSJan Høydahl
62487c131baSJan Høydahl
62587c131baSJan Høydahlclass TodoGroup(SecretYamlObject):
62687c131baSJan Høydahl    yaml_tag = u'!TodoGroup'
62787c131baSJan Høydahl    hidden_fields = []
62887c131baSJan Høydahl    def __init__(self, id, title, description, todos, is_in_rc_loop=None, depends=None):
62987c131baSJan Høydahl        self.id = id
63087c131baSJan Høydahl        self.title = title
63187c131baSJan Høydahl        self.description = description
63287c131baSJan Høydahl        self.depends = depends
63387c131baSJan Høydahl        self.is_in_rc_loop = is_in_rc_loop
63487c131baSJan Høydahl        self.todos = todos
63587c131baSJan Høydahl
63687c131baSJan Høydahl    @classmethod
63787c131baSJan Høydahl    def from_yaml(cls, loader, node):
63887c131baSJan Høydahl        fields = loader.construct_mapping(node, deep = True)
63987c131baSJan Høydahl        return TodoGroup(**fields)
64087c131baSJan Høydahl
64187c131baSJan Høydahl    def num_done(self):
64287c131baSJan Høydahl        return sum(1 for x in self.todos if x.is_done() > 0)
64387c131baSJan Høydahl
64487c131baSJan Høydahl    def num_applies(self):
64587c131baSJan Høydahl        count = sum(1 for x in self.todos if x.applies(state.release_type))
64687c131baSJan Høydahl        # print("num_applies=%s" % count)
64787c131baSJan Høydahl        return count
64887c131baSJan Høydahl
64987c131baSJan Høydahl    def is_done(self):
65087c131baSJan Høydahl        # print("Done=%s, applies=%s" % (self.num_done(), self.num_applies()))
65187c131baSJan Høydahl        return self.num_done() >= self.num_applies()
65287c131baSJan Høydahl
65387c131baSJan Høydahl    def get_title(self):
65487c131baSJan Høydahl        # print("get_title: %s" % self.is_done())
65587c131baSJan Høydahl        prefix = ""
65687c131baSJan Høydahl        if self.is_done():
65787c131baSJan Høydahl            prefix = "✓ "
65887c131baSJan Høydahl        return "%s%s (%d/%d)" % (prefix, self.title, self.num_done(), self.num_applies())
65987c131baSJan Høydahl
66087c131baSJan Høydahl    def get_submenu(self):
66187c131baSJan Høydahl        menu = UpdatableConsoleMenu(title=self.title, subtitle=self.get_subtitle, prologue_text=self.get_description(),
66287c131baSJan Høydahl                           screen=MyScreen())
66387c131baSJan Høydahl        menu.exit_item = CustomExitItem("Return")
66487c131baSJan Høydahl        for todo in self.get_todos():
66587c131baSJan Høydahl            if todo.applies(state.release_type):
66687c131baSJan Høydahl                menu.append_item(todo.get_menu_item())
66787c131baSJan Høydahl        return menu
66887c131baSJan Høydahl
66987c131baSJan Høydahl    def get_menu_item(self):
67087c131baSJan Høydahl        item = UpdatableSubmenuItem(self.get_title, self.get_submenu())
67187c131baSJan Høydahl        return item
67287c131baSJan Høydahl
67387c131baSJan Høydahl    def get_todos(self):
67487c131baSJan Høydahl        return self.todos
67587c131baSJan Høydahl
67687c131baSJan Høydahl    def in_rc_loop(self):
67787c131baSJan Høydahl        return self.is_in_rc_loop is True
67887c131baSJan Høydahl
67987c131baSJan Høydahl    def get_description(self):
68087c131baSJan Høydahl        desc = self.description
68187c131baSJan Høydahl        if desc:
68287c131baSJan Høydahl            return expand_jinja(desc)
68387c131baSJan Høydahl        else:
68487c131baSJan Høydahl            return None
68587c131baSJan Høydahl
68687c131baSJan Høydahl    def get_subtitle(self):
68787c131baSJan Høydahl        if self.depends:
68887c131baSJan Høydahl            ret_str = ""
68987c131baSJan Høydahl            for dep in ensure_list(self.depends):
69087c131baSJan Høydahl                g = state.get_group_by_id(dep)
69187c131baSJan Høydahl                if not g:
69287c131baSJan Høydahl                    g = state.get_todo_by_id(dep)
69387c131baSJan Høydahl                if g and not g.is_done():
69487c131baSJan Høydahl                    ret_str += "NOTE: Please first complete '%s'\n" % g.title
69587c131baSJan Høydahl                    return ret_str.strip()
69687c131baSJan Høydahl        return None
69787c131baSJan Høydahl
69887c131baSJan Høydahl
69987c131baSJan Høydahlclass Todo(SecretYamlObject):
70087c131baSJan Høydahl    yaml_tag = u'!Todo'
70187c131baSJan Høydahl    hidden_fields = ['state']
70287c131baSJan Høydahl    def __init__(self, id, title, description=None, post_description=None, done=None, types=None, links=None,
70387c131baSJan Høydahl                 commands=None, user_input=None, depends=None, vars=None, asciidoc=None, persist_vars=None,
70487c131baSJan Høydahl                 function=None):
70587c131baSJan Høydahl        self.id = id
70687c131baSJan Høydahl        self.title = title
70787c131baSJan Høydahl        self.description = description
70887c131baSJan Høydahl        self.asciidoc = asciidoc
70987c131baSJan Høydahl        self.types = types
71087c131baSJan Høydahl        self.depends = depends
71187c131baSJan Høydahl        self.vars = vars
71287c131baSJan Høydahl        self.persist_vars = persist_vars
71387c131baSJan Høydahl        self.function = function
71487c131baSJan Høydahl        self.user_input = user_input
71587c131baSJan Høydahl        self.commands = commands
71687c131baSJan Høydahl        self.post_description = post_description
71787c131baSJan Høydahl        self.links = links
71887c131baSJan Høydahl        self.state = {}
71987c131baSJan Høydahl
72087c131baSJan Høydahl        self.set_done(done)
72187c131baSJan Høydahl        if self.types:
72287c131baSJan Høydahl            self.types = ensure_list(self.types)
72387c131baSJan Høydahl            for t in self.types:
72487c131baSJan Høydahl                if not t in ['minor', 'major', 'bugfix']:
72587c131baSJan Høydahl                    sys.exit("Wrong Todo config for '%s'. Type needs to be either 'minor', 'major' or 'bugfix'" % self.id)
72687c131baSJan Høydahl        if commands:
72787c131baSJan Høydahl            self.commands.todo_id = self.id
72887c131baSJan Høydahl            for c in commands.commands:
72987c131baSJan Høydahl                c.todo_id = self.id
73087c131baSJan Høydahl
73187c131baSJan Høydahl    @classmethod
73287c131baSJan Høydahl    def from_yaml(cls, loader, node):
73387c131baSJan Høydahl        fields = loader.construct_mapping(node, deep = True)
73487c131baSJan Høydahl        return Todo(**fields)
73587c131baSJan Høydahl
73687c131baSJan Høydahl    def get_vars(self):
73787c131baSJan Høydahl        myvars = {}
73887c131baSJan Høydahl        if self.vars:
73987c131baSJan Høydahl            for k in self.vars:
74087c131baSJan Høydahl                val = self.vars[k]
74187c131baSJan Høydahl                if callable(val):
74287c131baSJan Høydahl                    myvars[k] = expand_jinja(val(), vars=myvars)
74387c131baSJan Høydahl                else:
74487c131baSJan Høydahl                    myvars[k] = expand_jinja(val, vars=myvars)
74587c131baSJan Høydahl        return myvars
74687c131baSJan Høydahl
74787c131baSJan Høydahl    def set_done(self, is_done):
74887c131baSJan Høydahl        if is_done:
74987c131baSJan Høydahl            self.state['done_date'] = unix_time_millis(datetime.utcnow())
75087c131baSJan Høydahl            if self.persist_vars:
75187c131baSJan Høydahl                for k in self.persist_vars:
75287c131baSJan Høydahl                    self.state[k] = self.get_vars()[k]
75387c131baSJan Høydahl        else:
75487c131baSJan Høydahl            self.state.clear()
75587c131baSJan Høydahl        self.state['done'] = is_done
75687c131baSJan Høydahl
75787c131baSJan Høydahl    def applies(self, type):
75887c131baSJan Høydahl        if self.types:
75987c131baSJan Høydahl            return type in self.types
76087c131baSJan Høydahl        return True
76187c131baSJan Høydahl
76287c131baSJan Høydahl    def is_done(self):
76387c131baSJan Høydahl        return 'done' in self.state and self.state['done'] is True
76487c131baSJan Høydahl
76587c131baSJan Høydahl    def get_title(self):
76687c131baSJan Høydahl        prefix = ""
76787c131baSJan Høydahl        if self.is_done():
76887c131baSJan Høydahl            prefix = "✓ "
76987c131baSJan Høydahl        return expand_jinja("%s%s" % (prefix, self.title), self.get_vars_and_state())
77087c131baSJan Høydahl
77187c131baSJan Høydahl    def display_and_confirm(self):
77287c131baSJan Høydahl        try:
77387c131baSJan Høydahl            if self.depends:
77487c131baSJan Høydahl                ret_str = ""
77587c131baSJan Høydahl                for dep in ensure_list(self.depends):
77687c131baSJan Høydahl                    g = state.get_group_by_id(dep)
77787c131baSJan Høydahl                    if not g:
77887c131baSJan Høydahl                        g = state.get_todo_by_id(dep)
77987c131baSJan Høydahl                    if not g.is_done():
78087c131baSJan Høydahl                        print("This step depends on '%s'. Please complete that first\n" % g.title)
78187c131baSJan Høydahl                        return
78287c131baSJan Høydahl            desc = self.get_description()
78387c131baSJan Høydahl            if desc:
78487c131baSJan Høydahl                print("%s" % desc)
78587c131baSJan Høydahl            try:
78687c131baSJan Høydahl                if self.function and not self.is_done():
78787c131baSJan Høydahl                    if not eval(self.function)(self):
78887c131baSJan Høydahl                        return
78987c131baSJan Høydahl            except Exception as e:
79087c131baSJan Høydahl                print("Function call to %s for todo %s failed: %s" % (self.function, self.id, e))
79187c131baSJan Høydahl                raise e
79287c131baSJan Høydahl            if self.user_input and not self.is_done():
79387c131baSJan Høydahl                ui_list = ensure_list(self.user_input)
79487c131baSJan Høydahl                for ui in ui_list:
79587c131baSJan Høydahl                    ui.run(self.state)
79687c131baSJan Høydahl                print()
79787c131baSJan Høydahl            if self.links:
79887c131baSJan Høydahl                print("\nLinks:\n")
79987c131baSJan Høydahl                for link in self.links:
80087c131baSJan Høydahl                    print("- %s" % expand_jinja(link, self.get_vars_and_state()))
80187c131baSJan Høydahl                print()
80287c131baSJan Høydahl            cmds = self.get_commands()
80387c131baSJan Høydahl            if cmds:
80487c131baSJan Høydahl                if not self.is_done():
80587c131baSJan Høydahl                    if not cmds.logs_prefix:
80687c131baSJan Høydahl                        cmds.logs_prefix = self.id
80787c131baSJan Høydahl                    cmds.run()
80887c131baSJan Høydahl                else:
80987c131baSJan Høydahl                    print("This step is already completed. You have to first set it to 'not completed' in order to execute commands again.")
81087c131baSJan Høydahl                print()
81187c131baSJan Høydahl            if self.post_description:
81287c131baSJan Høydahl                print("%s" % self.get_post_description())
81387c131baSJan Høydahl            todostate = self.get_state()
81487c131baSJan Høydahl            if self.is_done() and len(todostate) > 2:
81587c131baSJan Høydahl                print("Variables registered\n")
81687c131baSJan Høydahl                for k in todostate:
81787c131baSJan Høydahl                    if k == 'done' or k == 'done_date':
81887c131baSJan Høydahl                        continue
81987c131baSJan Høydahl                    print("* %s = %s" % (k, todostate[k]))
82087c131baSJan Høydahl                print()
82187c131baSJan Høydahl            completed = ask_yes_no("Mark task '%s' as completed?" % self.get_title())
82287c131baSJan Høydahl            self.set_done(completed)
82387c131baSJan Høydahl            state.save()
82487c131baSJan Høydahl        except Exception as e:
82587c131baSJan Høydahl            print("ERROR while executing todo %s (%s)" % (self.get_title(), e))
82687c131baSJan Høydahl
82787c131baSJan Høydahl    def get_menu_item(self):
82887c131baSJan Høydahl        return UpdatableFunctionItem(self.get_title, self.display_and_confirm)
82987c131baSJan Høydahl
83087c131baSJan Høydahl    def clone(self):
83187c131baSJan Høydahl        clone = Todo(self.id, self.title, description=self.description)
83287c131baSJan Høydahl        clone.state = copy.deepcopy(self.state)
83387c131baSJan Høydahl        return clone
83487c131baSJan Høydahl
83587c131baSJan Høydahl    def clear(self):
83687c131baSJan Høydahl        self.state.clear()
83787c131baSJan Høydahl        self.set_done(False)
83887c131baSJan Høydahl
83987c131baSJan Høydahl    def get_state(self):
84087c131baSJan Høydahl        return self.state
84187c131baSJan Høydahl
84287c131baSJan Høydahl    def get_description(self):
84387c131baSJan Høydahl        desc = self.description
84487c131baSJan Høydahl        if desc:
84587c131baSJan Høydahl            return expand_jinja(desc, vars=self.get_vars_and_state())
84687c131baSJan Høydahl        else:
84787c131baSJan Høydahl            return None
84887c131baSJan Høydahl
84987c131baSJan Høydahl    def get_post_description(self):
85087c131baSJan Høydahl        if self.post_description:
85187c131baSJan Høydahl            return expand_jinja(self.post_description, vars=self.get_vars_and_state())
85287c131baSJan Høydahl        else:
85387c131baSJan Høydahl            return None
85487c131baSJan Høydahl
85587c131baSJan Høydahl    def get_commands(self):
85687c131baSJan Høydahl        cmds = self.commands
85787c131baSJan Høydahl        return cmds
85887c131baSJan Høydahl
85987c131baSJan Høydahl    def get_asciidoc(self):
86087c131baSJan Høydahl        if self.asciidoc:
86187c131baSJan Høydahl            return expand_jinja(self.asciidoc, vars=self.get_vars_and_state())
86287c131baSJan Høydahl        else:
86387c131baSJan Høydahl            return None
86487c131baSJan Høydahl
86587c131baSJan Høydahl    def get_vars_and_state(self):
86687c131baSJan Høydahl        d = self.get_vars().copy()
86787c131baSJan Høydahl        d.update(self.get_state())
86887c131baSJan Høydahl        return d
86987c131baSJan Høydahl
87087c131baSJan Høydahl
87187c131baSJan Høydahldef get_release_version():
87287c131baSJan Høydahl    v = str(input("Which version are you releasing? (x.y.z) "))
87387c131baSJan Høydahl    try:
87487c131baSJan Høydahl        version = Version.parse(v)
87587c131baSJan Høydahl    except:
87687c131baSJan Høydahl        print("Not a valid version %s" % v)
87787c131baSJan Høydahl        return get_release_version()
87887c131baSJan Høydahl
87987c131baSJan Høydahl    return str(version)
88087c131baSJan Høydahl
88187c131baSJan Høydahl
88287c131baSJan Høydahldef get_subtitle():
88387c131baSJan Høydahl    applying_groups = list(filter(lambda x: x.num_applies() > 0, state.todo_groups))
88487c131baSJan Høydahl    done_groups = sum(1 for x in applying_groups if x.is_done())
88587c131baSJan Høydahl    return "Please complete the below checklist (Complete: %s/%s)" % (done_groups, len(applying_groups))
88687c131baSJan Høydahl
88787c131baSJan Høydahl
88887c131baSJan Høydahldef get_todo_menuitem_title():
88987c131baSJan Høydahl    return "Go to checklist (RC%d)" % (state.rc_number)
89087c131baSJan Høydahl
89187c131baSJan Høydahl
89287c131baSJan Høydahldef get_releasing_text():
89387c131baSJan Høydahl    return "Releasing Lucene/Solr %s RC%d" % (state.release_version, state.rc_number)
89487c131baSJan Høydahl
89587c131baSJan Høydahl
89687c131baSJan Høydahldef get_start_new_rc_menu_title():
89787c131baSJan Høydahl    return "Abort RC%d and start a new RC%d" % (state.rc_number, state.rc_number + 1)
89887c131baSJan Høydahl
89987c131baSJan Høydahl
90087c131baSJan Høydahldef start_new_rc():
90187c131baSJan Høydahl    state.new_rc()
90287c131baSJan Høydahl    print("Started RC%d" % state.rc_number)
90387c131baSJan Høydahl
90487c131baSJan Høydahl
90587c131baSJan Høydahldef reset_state():
90687c131baSJan Høydahl    global state
90787c131baSJan Høydahl    if ask_yes_no("Are you sure? This will erase all current progress"):
90887c131baSJan Høydahl        maybe_remove_rc_from_svn()
90987c131baSJan Høydahl        shutil.rmtree(os.path.join(state.config_path, state.release_version))
91087c131baSJan Høydahl        state.clear()
91187c131baSJan Høydahl
91287c131baSJan Høydahl
91387c131baSJan Høydahldef template(name, vars=None):
91487c131baSJan Høydahl    return expand_jinja(templates[name], vars=vars)
91587c131baSJan Høydahl
91687c131baSJan Høydahl
91787c131baSJan Høydahldef help():
91887c131baSJan Høydahl    print(template('help'))
91987c131baSJan Høydahl    pause()
92087c131baSJan Høydahl
92187c131baSJan Høydahl
92287c131baSJan Høydahldef ensure_list(o):
92387c131baSJan Høydahl    if o is None:
92487c131baSJan Høydahl        return []
92587c131baSJan Høydahl    if not isinstance(o, list):
92687c131baSJan Høydahl        return [o]
92787c131baSJan Høydahl    else:
92887c131baSJan Høydahl        return o
92987c131baSJan Høydahl
93087c131baSJan Høydahl
93187c131baSJan Høydahldef open_file(filename):
93287c131baSJan Høydahl    print("Opening file %s" % filename)
93387c131baSJan Høydahl    if platform.system().startswith("Win"):
93487c131baSJan Høydahl        run("start %s" % filename)
93587c131baSJan Høydahl    else:
93687c131baSJan Høydahl        run("open %s" % filename)
93787c131baSJan Høydahl
93887c131baSJan Høydahl
93987c131baSJan Høydahldef expand_multiline(cmd_txt, indent=0):
94087c131baSJan Høydahl    return re.sub(r'  +', " %s\n    %s" % (Commands.cmd_continuation_char, " "*indent), cmd_txt)
94187c131baSJan Høydahl
94287c131baSJan Høydahl
94387c131baSJan Høydahldef unix_to_datetime(unix_stamp):
94487c131baSJan Høydahl    return datetime.utcfromtimestamp(unix_stamp / 1000)
94587c131baSJan Høydahl
94687c131baSJan Høydahl
94787c131baSJan Høydahldef generate_asciidoc():
94887c131baSJan Høydahl    base_filename = os.path.join(state.get_release_folder(),
94987c131baSJan Høydahl                                 "lucene_solr_release_%s"
95087c131baSJan Høydahl                                 % (state.release_version.replace("\.", "_")))
95187c131baSJan Høydahl
95287c131baSJan Høydahl    filename_adoc = "%s.adoc" % base_filename
95387c131baSJan Høydahl    filename_html = "%s.html" % base_filename
95487c131baSJan Høydahl    fh = open(filename_adoc, "w")
95587c131baSJan Høydahl
95687c131baSJan Høydahl    fh.write("= Lucene/Solr Release %s\n\n" % state.release_version)
95787c131baSJan Høydahl    fh.write("(_Generated by releaseWizard.py v%s ALPHA at %s_)\n\n"
95887c131baSJan Høydahl             % (getScriptVersion(), datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")))
95987c131baSJan Høydahl    fh.write(":numbered:\n\n")
96087c131baSJan Høydahl    fh.write("%s\n\n" % template('help'))
96187c131baSJan Høydahl    for group in state.todo_groups:
96287c131baSJan Høydahl        if group.num_applies() == 0:
96387c131baSJan Høydahl            continue
96487c131baSJan Høydahl        fh.write("== %s\n\n" % group.get_title())
96587c131baSJan Høydahl        fh.write("%s\n\n" % group.get_description())
96687c131baSJan Høydahl        for todo in group.get_todos():
96787c131baSJan Høydahl            if not todo.applies(state.release_type):
96887c131baSJan Høydahl                continue
96987c131baSJan Høydahl            fh.write("=== %s\n\n" % todo.get_title())
97087c131baSJan Høydahl            if todo.is_done():
97187c131baSJan Høydahl                fh.write("_Completed %s_\n\n" % unix_to_datetime(todo.state['done_date']).strftime(
97287c131baSJan Høydahl                    "%Y-%m-%d %H:%M UTC"))
97387c131baSJan Høydahl            if todo.get_asciidoc():
97487c131baSJan Høydahl                fh.write("%s\n\n" % todo.get_asciidoc())
97587c131baSJan Høydahl            else:
97687c131baSJan Høydahl                desc = todo.get_description()
97787c131baSJan Høydahl                if desc:
97887c131baSJan Høydahl                    fh.write("%s\n\n" % desc)
97987c131baSJan Høydahl            state_copy = copy.deepcopy(todo.state)
98087c131baSJan Høydahl            state_copy.pop('done', None)
98187c131baSJan Høydahl            state_copy.pop('done_date', None)
98287c131baSJan Høydahl            if len(state_copy) > 0 or todo.user_input is not None:
98387c131baSJan Høydahl                fh.write(".Variables collected in this step\n")
98487c131baSJan Høydahl                fh.write("|===\n")
98587c131baSJan Høydahl                fh.write("|Variable |Value\n")
98687c131baSJan Høydahl                mykeys = set()
98787c131baSJan Høydahl                for e in ensure_list(todo.user_input):
98887c131baSJan Høydahl                    mykeys.add(e.name)
98987c131baSJan Høydahl                for e in state_copy.keys():
99087c131baSJan Høydahl                    mykeys.add(e)
99187c131baSJan Høydahl                for key in mykeys:
99287c131baSJan Høydahl                    val = "(not set)"
99387c131baSJan Høydahl                    if key in state_copy:
99487c131baSJan Høydahl                        val = state_copy[key]
99587c131baSJan Høydahl                    fh.write("\n|%s\n|%s\n" % (key, val))
99687c131baSJan Høydahl                fh.write("|===\n\n")
99787c131baSJan Høydahl            cmds = todo.get_commands()
99887c131baSJan Høydahl            if cmds:
99987c131baSJan Høydahl                if cmds.commands_text:
100087c131baSJan Høydahl                    fh.write("%s\n\n" % cmds.get_commands_text())
100187c131baSJan Høydahl                fh.write("[source,sh]\n----\n")
100287c131baSJan Høydahl                if cmds.env:
100387c131baSJan Høydahl                    for key in cmds.env:
100487c131baSJan Høydahl                        val = cmds.env[key]
100587c131baSJan Høydahl                        if is_windows():
100687c131baSJan Høydahl                            fh.write("SET %s=%s\n" % (key, val))
100787c131baSJan Høydahl                        else:
100887c131baSJan Høydahl                            fh.write("export %s=%s\n" % (key, val))
100987c131baSJan Høydahl                fh.write(abbreviate_homedir("cd %s\n" % cmds.get_root_folder()))
101087c131baSJan Høydahl                cmds2 = ensure_list(cmds.commands)
101187c131baSJan Høydahl                for c in cmds2:
101287c131baSJan Høydahl                    for line in c.display_cmd():
101387c131baSJan Høydahl                        fh.write("%s\n" % line)
101487c131baSJan Høydahl                fh.write("----\n\n")
101587c131baSJan Høydahl            if todo.post_description and not todo.get_asciidoc():
101687c131baSJan Høydahl                fh.write("\n%s\n\n" % todo.get_post_description())
101787c131baSJan Høydahl            if todo.links:
101887c131baSJan Høydahl                fh.write("Links:\n\n")
101987c131baSJan Høydahl                for l in todo.links:
102087c131baSJan Høydahl                    fh.write("* %s\n" % expand_jinja(l))
102187c131baSJan Høydahl                fh.write("\n")
102287c131baSJan Høydahl
102387c131baSJan Høydahl    fh.close()
102487c131baSJan Høydahl    print("Wrote file %s" % os.path.join(state.get_release_folder(), filename_adoc))
102587c131baSJan Høydahl    print("Running command 'asciidoctor %s'" % filename_adoc)
102687c131baSJan Høydahl    run_follow("asciidoctor %s" % filename_adoc)
102787c131baSJan Høydahl    if os.path.exists(filename_html):
102887c131baSJan Høydahl        open_file(filename_html)
102987c131baSJan Høydahl    else:
103087c131baSJan Høydahl        print("Failed generating HTML version, please install asciidoctor")
103187c131baSJan Høydahl    pause()
103287c131baSJan Høydahl
103387c131baSJan Høydahl
103487c131baSJan Høydahldef load_rc():
103587c131baSJan Høydahl    lucenerc = os.path.expanduser("~/.lucenerc")
103687c131baSJan Høydahl    try:
103787c131baSJan Høydahl        with open(lucenerc, 'r') as fp:
103887c131baSJan Høydahl            return json.load(fp)
103987c131baSJan Høydahl    except:
104087c131baSJan Høydahl        return None
104187c131baSJan Høydahl
104287c131baSJan Høydahl
104387c131baSJan Høydahldef store_rc(release_root, release_version=None):
104487c131baSJan Høydahl    lucenerc = os.path.expanduser("~/.lucenerc")
104587c131baSJan Høydahl    dict = {}
104687c131baSJan Høydahl    dict['root'] = release_root
104787c131baSJan Høydahl    if release_version:
104887c131baSJan Høydahl        dict['release_version'] = release_version
104987c131baSJan Høydahl    with open(lucenerc, "w") as fp:
105087c131baSJan Høydahl        json.dump(dict, fp, indent=2)
105187c131baSJan Høydahl
105287c131baSJan Høydahl
105387c131baSJan Høydahldef release_other_version():
105487c131baSJan Høydahl    if not state.is_released():
105587c131baSJan Høydahl        maybe_remove_rc_from_svn()
105687c131baSJan Høydahl    store_rc(state.config_path, None)
105787c131baSJan Høydahl    print("Please restart the wizard")
105887c131baSJan Høydahl    sys.exit(0)
105987c131baSJan Høydahl
106087c131baSJan Høydahldef file_to_string(filename):
106187c131baSJan Høydahl    with open(filename, encoding='utf8') as f:
106287c131baSJan Høydahl        return f.read().strip()
106387c131baSJan Høydahl
106487c131baSJan Høydahldef download_keys():
106587c131baSJan Høydahl    download('KEYS', "https://archive.apache.org/dist/lucene/KEYS", state.config_path)
106687c131baSJan Høydahl
106787c131baSJan Høydahldef keys_downloaded():
106887c131baSJan Høydahl    return os.path.exists(os.path.join(state.config_path, "KEYS"))
106987c131baSJan Høydahl
107087c131baSJan Høydahl
107187c131baSJan Høydahldef dump_yaml():
107287c131baSJan Høydahl    file = open(os.path.join(script_path, "releaseWizard.yaml"), "w")
107387c131baSJan Høydahl    yaml.add_representer(str, str_presenter)
107487c131baSJan Høydahl    yaml.Dumper.ignore_aliases = lambda *args : True
107587c131baSJan Høydahl    dump_obj = {'templates': templates,
107687c131baSJan Høydahl                'groups': state.todo_groups}
107787c131baSJan Høydahl    yaml.dump(dump_obj, width=180, stream=file, sort_keys=False, default_flow_style=False)
107887c131baSJan Høydahl
107987c131baSJan Høydahl
108087c131baSJan Høydahldef parse_config():
108187c131baSJan Høydahl    description = 'Script to guide a RM through the whole release process'
108287c131baSJan Høydahl    parser = argparse.ArgumentParser(description=description, epilog="Go push that release!",
108387c131baSJan Høydahl                                     formatter_class=argparse.RawDescriptionHelpFormatter)
108487c131baSJan Høydahl    parser.add_argument('--dry-run', dest='dry', action='store_true', default=False,
108587c131baSJan Høydahl                        help='Do not execute any commands, but echo them instead. Display extra debug info')
108687c131baSJan Høydahl    parser.add_argument('--init', action='store_true', default=False,
108787c131baSJan Høydahl                        help='Re-initialize root and version')
108887c131baSJan Høydahl    config = parser.parse_args()
108987c131baSJan Høydahl
109087c131baSJan Høydahl    return config
109187c131baSJan Høydahl
109287c131baSJan Høydahl
109387c131baSJan Høydahldef load(urlString, encoding="utf-8"):
109487c131baSJan Høydahl    try:
109587c131baSJan Høydahl        content = urllib.request.urlopen(urlString).read().decode(encoding)
109687c131baSJan Høydahl    except Exception as e:
109787c131baSJan Høydahl        print('Retrying download of url %s after exception: %s' % (urlString, e))
109887c131baSJan Høydahl        content = urllib.request.urlopen(urlString).read().decode(encoding)
109987c131baSJan Høydahl    return content
110087c131baSJan Høydahl
110187c131baSJan Høydahl
110287c131baSJan Høydahldef configure_pgp(gpg_todo):
110387c131baSJan Høydahl    print("Based on your Apache ID we'll lookup your key online\n"
110487c131baSJan Høydahl          "and through this complete the 'gpg' prerequisite task.\n")
110587c131baSJan Høydahl    gpg_state = gpg_todo.get_state()
110687c131baSJan Høydahl    id = str(input("Please enter your Apache id: (ENTER=skip) "))
110787c131baSJan Høydahl    if id.strip() == '':
110887c131baSJan Høydahl        return False
1109daf14981SAlan Woodward    all_keys = load('https://home.apache.org/keys/group/lucene.asc')
111087c131baSJan Høydahl    lines = all_keys.splitlines()
111187c131baSJan Høydahl    keyid_linenum = None
111287c131baSJan Høydahl    for idx, line in enumerate(lines):
111387c131baSJan Høydahl        if line == 'ASF ID: %s' % id:
111487c131baSJan Høydahl            keyid_linenum = idx+1
111587c131baSJan Høydahl            break
111687c131baSJan Høydahl    if keyid_linenum:
111787c131baSJan Høydahl        keyid_line = lines[keyid_linenum]
111887c131baSJan Høydahl        assert keyid_line.startswith('LDAP PGP key: ')
111987c131baSJan Høydahl        gpg_id = keyid_line[14:].replace(" ", "")[-8:]
1120daf14981SAlan Woodward        print("Found gpg key id %s on file at Apache (https://home.apache.org/keys/group/lucene.asc)" % gpg_id)
112187c131baSJan Høydahl    else:
112287c131baSJan Høydahl        print(textwrap.dedent("""\
112387c131baSJan Høydahl            Could not find your GPG key from Apache servers.
112487c131baSJan Høydahl            Please make sure you have registered your key ID in
112587c131baSJan Høydahl            id.apache.org, see links for more info."""))
112687c131baSJan Høydahl        gpg_id = str(input("Enter your key ID manually, 8 last characters (ENTER=skip): "))
112787c131baSJan Høydahl        if gpg_id.strip() == '':
112887c131baSJan Høydahl            return False
112987c131baSJan Høydahl        elif len(gpg_id) != 8:
113087c131baSJan Høydahl            print("gpg id must be the last 8 characters of your key id")
113187c131baSJan Høydahl        gpg_id = gpg_id.upper()
113287c131baSJan Høydahl    try:
113387c131baSJan Høydahl        res = run("gpg --list-secret-keys %s" % gpg_id)
113487c131baSJan Høydahl        print("Found key %s on your private gpg keychain" % gpg_id)
113587c131baSJan Høydahl        # Check rsa and key length >= 4096
113687c131baSJan Høydahl        match = re.search(r'^sec +((rsa|dsa)(\d{4})) ', res)
113787c131baSJan Høydahl        type = "(unknown)"
113887c131baSJan Høydahl        length = -1
113987c131baSJan Høydahl        if match:
114087c131baSJan Høydahl            type = match.group(2)
114187c131baSJan Høydahl            length = int(match.group(3))
114287c131baSJan Høydahl        else:
114387c131baSJan Høydahl            match = re.search(r'^sec +((\d{4})([DR])/.*?) ', res)
114487c131baSJan Høydahl            if match:
114587c131baSJan Høydahl                type = 'rsa' if match.group(3) == 'R' else 'dsa'
114687c131baSJan Høydahl                length = int(match.group(2))
114787c131baSJan Høydahl            else:
114887c131baSJan Høydahl                print("Could not determine type and key size for your key")
114987c131baSJan Høydahl                print("%s" % res)
115087c131baSJan Høydahl                if not ask_yes_no("Is your key of type RSA and size >= 2048 (ideally 4096)? "):
115187c131baSJan Høydahl                    print("Sorry, please generate a new key, add to KEYS and register with id.apache.org")
115287c131baSJan Høydahl                    return False
115387c131baSJan Høydahl        if not type == 'rsa':
115487c131baSJan Høydahl            print("We strongly recommend RSA type key, your is '%s'. Consider generating a new key." % type.upper())
115587c131baSJan Høydahl        if length < 2048:
115687c131baSJan Høydahl            print("Your key has key length of %s. Cannot use < 2048, please generate a new key before doing the release" % length)
115787c131baSJan Høydahl            return False
115887c131baSJan Høydahl        if length < 4096:
115987c131baSJan Høydahl            print("Your key length is < 4096, Please generate a stronger key.")
116087c131baSJan Høydahl            print("Alternatively, follow instructions in http://www.apache.org/dev/release-signing.html#note")
116187c131baSJan Høydahl            if not ask_yes_no("Have you configured your gpg to avoid SHA-1?"):
116287c131baSJan Høydahl                print("Please either generate a strong key or reconfigure your client")
116387c131baSJan Høydahl                return False
116487c131baSJan Høydahl        print("Validated that your key is of type RSA and has a length >= 2048 (%s)" % length)
116587c131baSJan Høydahl    except:
116687c131baSJan Høydahl        print(textwrap.dedent("""\
116787c131baSJan Høydahl            Key not found on your private gpg keychain. In order to sign the release you'll
116887c131baSJan Høydahl            need to fix this, then try again"""))
116987c131baSJan Høydahl        return False
117087c131baSJan Høydahl    try:
117187c131baSJan Høydahl        lines = run("gpg --check-signatures %s" % gpg_id).splitlines()
117287c131baSJan Høydahl        sigs = 0
117387c131baSJan Høydahl        apache_sigs = 0
117487c131baSJan Høydahl        for line in lines:
117587c131baSJan Høydahl            if line.startswith("sig") and not gpg_id in line:
117687c131baSJan Høydahl                sigs += 1
117787c131baSJan Høydahl                if '@apache.org' in line:
117887c131baSJan Høydahl                    apache_sigs += 1
117987c131baSJan Høydahl        print("Your key has %s signatures, of which %s are by committers (@apache.org address)" % (sigs, apache_sigs))
118087c131baSJan Høydahl        if apache_sigs < 1:
118187c131baSJan Høydahl            print(textwrap.dedent("""\
118287c131baSJan Høydahl                Your key is not signed by any other committer.
118387c131baSJan Høydahl                Please review http://www.apache.org/dev/openpgp.html#apache-wot
118487c131baSJan Høydahl                and make sure to get your key signed until next time.
118587c131baSJan Høydahl                You may want to run 'gpg --refresh-keys' to refresh your keychain."""))
118687c131baSJan Høydahl        uses_apacheid = is_code_signing_key = False
118787c131baSJan Høydahl        for line in lines:
118887c131baSJan Høydahl            if line.startswith("uid") and "%s@apache" % id in line:
118987c131baSJan Høydahl                uses_apacheid = True
119087c131baSJan Høydahl                if 'CODE SIGNING KEY' in line.upper():
119187c131baSJan Høydahl                    is_code_signing_key = True
119287c131baSJan Høydahl        if not uses_apacheid:
119387c131baSJan Høydahl            print("WARNING: Your key should use your apache-id email address, see http://www.apache.org/dev/release-signing.html#user-id")
119487c131baSJan Høydahl        if not is_code_signing_key:
119587c131baSJan Høydahl            print("WARNING: You code signing key should be labeled 'CODE SIGNING KEY', see http://www.apache.org/dev/release-signing.html#key-comment")
119687c131baSJan Høydahl    except Exception as e:
119787c131baSJan Høydahl        print("Could not check signatures of your key: %s" % e)
119887c131baSJan Høydahl
119987c131baSJan Høydahl    download_keys()
120087c131baSJan Høydahl    keys_text = file_to_string(os.path.join(state.config_path, "KEYS"))
120187c131baSJan Høydahl    if gpg_id in keys_text or "%s %s" % (gpg_id[:4], gpg_id[-4:]) in keys_text:
120287c131baSJan Høydahl        print("Found your key ID in official KEYS file. KEYS file is not cached locally.")
120387c131baSJan Høydahl    else:
120487c131baSJan Høydahl        print(textwrap.dedent("""\
120587c131baSJan Høydahl            Could not find your key ID in official KEYS file.
120687c131baSJan Høydahl            Please make sure it is added to https://dist.apache.org/repos/dist/release/lucene/KEYS
120787c131baSJan Høydahl            and committed to svn. Then re-try this initialization"""))
120887c131baSJan Høydahl        if not ask_yes_no("Do you want to continue without fixing KEYS file? (not recommended) "):
120987c131baSJan Høydahl            return False
121087c131baSJan Høydahl
121187c131baSJan Høydahl    gpg_state['apache_id'] = id
121287c131baSJan Høydahl    gpg_state['gpg_key'] = gpg_id
121387c131baSJan Høydahl    return True
121487c131baSJan Høydahl
121587c131baSJan Høydahl
121687c131baSJan Høydahldef pause(fun=None):
121787c131baSJan Høydahl    if fun:
121887c131baSJan Høydahl        fun()
121987c131baSJan Høydahl    input("\nPress ENTER to continue...")
122087c131baSJan Høydahl
122187c131baSJan Høydahl
122287c131baSJan Høydahl# Custom classes for ConsoleMenu, to make menu texts dynamic
122387c131baSJan Høydahl# Needed until https://github.com/aegirhall/console-menu/pull/25 is released
122487c131baSJan Høydahl# See https://pypi.org/project/console-menu/ for other docs
122587c131baSJan Høydahl
122687c131baSJan Høydahlclass UpdatableConsoleMenu(ConsoleMenu):
122787c131baSJan Høydahl
122887c131baSJan Høydahl    def __repr__(self):
122987c131baSJan Høydahl        return "%s: %s. %d items" % (self.get_title(), self.get_subtitle(), len(self.items))
123087c131baSJan Høydahl
123187c131baSJan Høydahl    def draw(self):
123287c131baSJan Høydahl        """
123387c131baSJan Høydahl        Refreshes the screen and redraws the menu. Should be called whenever something changes that needs to be redrawn.
123487c131baSJan Høydahl        """
123587c131baSJan Høydahl        self.screen.printf(self.formatter.format(title=self.get_title(), subtitle=self.get_subtitle(), items=self.items,
123687c131baSJan Høydahl                                                 prologue_text=self.get_prologue_text(), epilogue_text=self.get_epilogue_text()))
123787c131baSJan Høydahl
123887c131baSJan Høydahl    # Getters to get text in case method reference
123987c131baSJan Høydahl    def get_title(self):
124087c131baSJan Høydahl        return self.title() if callable(self.title) else self.title
124187c131baSJan Høydahl
124287c131baSJan Høydahl    def get_subtitle(self):
124387c131baSJan Høydahl        return self.subtitle() if callable(self.subtitle) else self.subtitle
124487c131baSJan Høydahl
124587c131baSJan Høydahl    def get_prologue_text(self):
124687c131baSJan Høydahl        return self.prologue_text() if callable(self.prologue_text) else self.prologue_text
124787c131baSJan Høydahl
124887c131baSJan Høydahl    def get_epilogue_text(self):
124987c131baSJan Høydahl        return self.epilogue_text() if callable(self.epilogue_text) else self.epilogue_text
125087c131baSJan Høydahl
125187c131baSJan Høydahl
125287c131baSJan Høydahlclass UpdatableSubmenuItem(SubmenuItem):
125387c131baSJan Høydahl    def __init__(self, text, submenu, menu=None, should_exit=False):
125487c131baSJan Høydahl        """
125587c131baSJan Høydahl        :ivar ConsoleMenu self.submenu: The submenu to be opened when this item is selected
125687c131baSJan Høydahl        """
125787c131baSJan Høydahl        super(SubmenuItem, self).__init__(text=text, menu=menu, should_exit=should_exit)
125887c131baSJan Høydahl
125987c131baSJan Høydahl        self.submenu = submenu
126087c131baSJan Høydahl        if menu:
126187c131baSJan Høydahl            self.get_submenu().parent = menu
126287c131baSJan Høydahl
126387c131baSJan Høydahl    def show(self, index):
126487c131baSJan Høydahl        return "%2d - %s" % (index + 1, self.get_text())
126587c131baSJan Høydahl
126687c131baSJan Høydahl    # Getters to get text in case method reference
126787c131baSJan Høydahl    def get_text(self):
126887c131baSJan Høydahl        return self.text() if callable(self.text) else self.text
126987c131baSJan Høydahl
127087c131baSJan Høydahl    def set_menu(self, menu):
127187c131baSJan Høydahl        """
127287c131baSJan Høydahl        Sets the menu of this item.
127387c131baSJan Høydahl        Should be used instead of directly accessing the menu attribute for this class.
127487c131baSJan Høydahl
127587c131baSJan Høydahl        :param ConsoleMenu menu: the menu
127687c131baSJan Høydahl        """
127787c131baSJan Høydahl        self.menu = menu
127887c131baSJan Høydahl        self.get_submenu().parent = menu
127987c131baSJan Høydahl
128087c131baSJan Høydahl    def action(self):
128187c131baSJan Høydahl        """
128287c131baSJan Høydahl        This class overrides this method
128387c131baSJan Høydahl        """
128487c131baSJan Høydahl        self.get_submenu().start()
128587c131baSJan Høydahl
128687c131baSJan Høydahl    def clean_up(self):
128787c131baSJan Høydahl        """
128887c131baSJan Høydahl        This class overrides this method
128987c131baSJan Høydahl        """
129087c131baSJan Høydahl        self.get_submenu().join()
129187c131baSJan Høydahl        self.menu.clear_screen()
129287c131baSJan Høydahl        self.menu.resume()
129387c131baSJan Høydahl
129487c131baSJan Høydahl    def get_return(self):
129587c131baSJan Høydahl        """
129687c131baSJan Høydahl        :return: The returned value in the submenu
129787c131baSJan Høydahl        """
129887c131baSJan Høydahl        return self.get_submenu().returned_value
129987c131baSJan Høydahl
130087c131baSJan Høydahl    def get_submenu(self):
130187c131baSJan Høydahl        """
130287c131baSJan Høydahl        We unwrap the submenu variable in case it is a reference to a method that returns a submenu
130387c131baSJan Høydahl        """
130487c131baSJan Høydahl        return self.submenu if not callable(self.submenu) else self.submenu()
130587c131baSJan Høydahl
130687c131baSJan Høydahl
130787c131baSJan Høydahlclass UpdatableFunctionItem(FunctionItem):
130887c131baSJan Høydahl    def show(self, index):
130987c131baSJan Høydahl        return "%2d - %s" % (index + 1, self.get_text())
131087c131baSJan Høydahl
131187c131baSJan Høydahl    # Getters to get text in case method reference
131287c131baSJan Høydahl    def get_text(self):
131387c131baSJan Høydahl        return self.text() if callable(self.text) else self.text
131487c131baSJan Høydahl
131587c131baSJan Høydahl
131687c131baSJan Høydahlclass MyScreen(Screen):
131787c131baSJan Høydahl    def clear(self):
131887c131baSJan Høydahl        return
131987c131baSJan Høydahl
132087c131baSJan Høydahl
132187c131baSJan Høydahlclass CustomExitItem(ExitItem):
132287c131baSJan Høydahl    def show(self, index):
132387c131baSJan Høydahl        return super(ExitItem, self).show(index)
132487c131baSJan Høydahl
132587c131baSJan Høydahl    def get_return(self):
132687c131baSJan Høydahl        return ""
132787c131baSJan Høydahl
132887c131baSJan Høydahl
132987c131baSJan Høydahldef main():
133087c131baSJan Høydahl    global state
133187c131baSJan Høydahl    global dry_run
133287c131baSJan Høydahl    global templates
133387c131baSJan Høydahl
133487c131baSJan Høydahl    print("Lucene/Solr releaseWizard v%s" % getScriptVersion())
133587c131baSJan Høydahl    c = parse_config()
133687c131baSJan Høydahl
133787c131baSJan Høydahl    if c.dry:
133887c131baSJan Høydahl        print("Entering dry-run mode where all commands will be echoed instead of executed")
133987c131baSJan Høydahl        dry_run = True
134087c131baSJan Høydahl
134187c131baSJan Høydahl    release_root = os.path.expanduser("~/.lucene-releases")
134287c131baSJan Høydahl    if not load_rc() or c.init:
134387c131baSJan Høydahl        print("Initializing")
134487c131baSJan Høydahl        dir_ok = False
134587c131baSJan Høydahl        root = str(input("Choose root folder: [~/.lucene-releases] "))
134687c131baSJan Høydahl        if os.path.exists(root) and (not os.path.isdir(root) or not os.access(root, os.W_OK)):
134787c131baSJan Høydahl            sys.exit("Root %s exists but is not a directory or is not writable" % root)
134887c131baSJan Høydahl        if not root == '':
134987c131baSJan Høydahl            if root.startswith("~/"):
135087c131baSJan Høydahl                release_root = os.path.expanduser(root)
135187c131baSJan Høydahl            else:
135287c131baSJan Høydahl                release_root = os.path.abspath(root)
135387c131baSJan Høydahl        if not os.path.exists(release_root):
135487c131baSJan Høydahl            try:
135587c131baSJan Høydahl                print("Creating release root %s" % release_root)
135687c131baSJan Høydahl                os.makedirs(release_root)
135787c131baSJan Høydahl            except Exception as e:
135887c131baSJan Høydahl                sys.exit("Error while creating %s: %s" % (release_root, e))
135987c131baSJan Høydahl        release_version = get_release_version()
136087c131baSJan Høydahl    else:
136187c131baSJan Høydahl        conf = load_rc()
136287c131baSJan Høydahl        release_root = conf['root']
136387c131baSJan Høydahl        if 'release_version' in conf:
136487c131baSJan Høydahl            release_version = conf['release_version']
136587c131baSJan Høydahl        else:
136687c131baSJan Høydahl            release_version = get_release_version()
136787c131baSJan Høydahl    store_rc(release_root, release_version)
136887c131baSJan Høydahl
136987c131baSJan Høydahl    check_prerequisites()
137087c131baSJan Høydahl
137187c131baSJan Høydahl    try:
137287c131baSJan Høydahl        y = yaml.load(open(os.path.join(script_path, "releaseWizard.yaml"), "r"), Loader=yaml.Loader)
137387c131baSJan Høydahl        templates = y.get('templates')
137487c131baSJan Høydahl        todo_list = y.get('groups')
137587c131baSJan Høydahl        state = ReleaseState(release_root, release_version, getScriptVersion())
137687c131baSJan Høydahl        state.init_todos(bootstrap_todos(todo_list))
137787c131baSJan Høydahl        state.load()
137887c131baSJan Høydahl    except Exception as e:
137987c131baSJan Høydahl        sys.exit("Failed initializing. %s" % e)
138087c131baSJan Høydahl
138187c131baSJan Høydahl    state.save()
138287c131baSJan Høydahl
1383*3134f10aSMike Drob    # Smoketester requires JAVA11_HOME to point to Java11
138487c131baSJan Høydahl    os.environ['JAVA_HOME'] = state.get_java_home()
138587c131baSJan Høydahl    os.environ['JAVACMD'] = state.get_java_cmd()
138687c131baSJan Høydahl
138787c131baSJan Høydahl    global lucene_news_file
138887c131baSJan Høydahl    global solr_news_file
1389329e7c7bSJan Høydahl    lucene_news_file = os.path.join(state.get_website_git_folder(), 'content', 'core', 'core_news',
1390329e7c7bSJan Høydahl      "%s-%s-available.md" % (state.get_release_date_iso(), state.release_version.replace(".", "-")))
1391329e7c7bSJan Høydahl    solr_news_file = os.path.join(state.get_website_git_folder(), 'content', 'solr', 'solr_news',
1392329e7c7bSJan Høydahl      "%s-%s-available.md" % (state.get_release_date_iso(), state.release_version.replace(".", "-")))
1393329e7c7bSJan Høydahl    website_folder = state.get_website_git_folder()
139487c131baSJan Høydahl
139587c131baSJan Høydahl    main_menu = UpdatableConsoleMenu(title="Lucene/Solr ReleaseWizard",
139687c131baSJan Høydahl                            subtitle=get_releasing_text,
139787c131baSJan Høydahl                            prologue_text="Welcome to the release wizard. From here you can manage the process including creating new RCs. "
139887c131baSJan Høydahl                                          "All changes are persisted, so you can exit any time and continue later. Make sure to read the Help section.",
1399*3134f10aSMike Drob                            epilogue_text="® 2020 The Lucene/Solr project. Licensed under the Apache License 2.0\nScript version v%s ALPHA)" % getScriptVersion(),
140087c131baSJan Høydahl                            screen=MyScreen())
140187c131baSJan Høydahl
140287c131baSJan Høydahl    todo_menu = UpdatableConsoleMenu(title=get_releasing_text,
140387c131baSJan Høydahl                            subtitle=get_subtitle,
140487c131baSJan Høydahl                            prologue_text=None,
140587c131baSJan Høydahl                            epilogue_text=None,
140687c131baSJan Høydahl                            screen=MyScreen())
140787c131baSJan Høydahl    todo_menu.exit_item = CustomExitItem("Return")
140887c131baSJan Høydahl
140987c131baSJan Høydahl    for todo_group in state.todo_groups:
141087c131baSJan Høydahl        if todo_group.num_applies() >= 0:
141187c131baSJan Høydahl            menu_item = todo_group.get_menu_item()
141287c131baSJan Høydahl            menu_item.set_menu(todo_menu)
141387c131baSJan Høydahl            todo_menu.append_item(menu_item)
141487c131baSJan Høydahl
141587c131baSJan Høydahl    main_menu.append_item(UpdatableSubmenuItem(get_todo_menuitem_title, todo_menu, menu=main_menu))
141687c131baSJan Høydahl    main_menu.append_item(UpdatableFunctionItem(get_start_new_rc_menu_title, start_new_rc))
141787c131baSJan Høydahl    main_menu.append_item(UpdatableFunctionItem('Clear and restart current RC', state.clear_rc))
141887c131baSJan Høydahl    main_menu.append_item(UpdatableFunctionItem("Clear all state, restart the %s release" % state.release_version, reset_state))
141987c131baSJan Høydahl    main_menu.append_item(UpdatableFunctionItem('Start release for a different version', release_other_version))
142087c131baSJan Høydahl    main_menu.append_item(UpdatableFunctionItem('Generate Asciidoc guide for this release', generate_asciidoc))
142187c131baSJan Høydahl    # main_menu.append_item(UpdatableFunctionItem('Dump YAML', dump_yaml))
142287c131baSJan Høydahl    main_menu.append_item(UpdatableFunctionItem('Help', help))
142387c131baSJan Høydahl
142487c131baSJan Høydahl    main_menu.show()
142587c131baSJan Høydahl
142687c131baSJan Høydahl
142787c131baSJan Høydahlsys.path.append(os.path.dirname(__file__))
142887c131baSJan Høydahlcurrent_git_root = os.path.abspath(
142987c131baSJan Høydahl    os.path.join(os.path.abspath(os.path.dirname(__file__)), os.path.pardir, os.path.pardir))
143087c131baSJan Høydahl
143187c131baSJan Høydahldry_run = False
143287c131baSJan Høydahl
143387c131baSJan Høydahlmajor_minor = ['major', 'minor']
143487c131baSJan Høydahlscript_path = os.path.dirname(os.path.realpath(__file__))
143587c131baSJan Høydahlos.chdir(script_path)
143687c131baSJan Høydahl
143787c131baSJan Høydahl
143887c131baSJan Høydahldef git_checkout_folder():
143987c131baSJan Høydahl    return state.get_git_checkout_folder()
144087c131baSJan Høydahl
144187c131baSJan Høydahl
144287c131baSJan Høydahldef tail_file(file, lines):
144387c131baSJan Høydahl    bufsize = 8192
144487c131baSJan Høydahl    fsize = os.stat(file).st_size
144587c131baSJan Høydahl    with open(file) as f:
144687c131baSJan Høydahl        if bufsize >= fsize:
144787c131baSJan Høydahl            bufsize = fsize
144887c131baSJan Høydahl        idx = 0
144987c131baSJan Høydahl        while True:
145087c131baSJan Høydahl            idx += 1
145187c131baSJan Høydahl            seek_pos = fsize - bufsize * idx
145287c131baSJan Høydahl            if seek_pos < 0:
145387c131baSJan Høydahl                seek_pos = 0
145487c131baSJan Høydahl            f.seek(seek_pos)
145587c131baSJan Høydahl            data = []
145687c131baSJan Høydahl            data.extend(f.readlines())
145787c131baSJan Høydahl            if len(data) >= lines or f.tell() == 0 or seek_pos == 0:
145887c131baSJan Høydahl                if not seek_pos == 0:
145987c131baSJan Høydahl                    print("Tailing last %d lines of file %s" % (lines, file))
146087c131baSJan Høydahl                print(''.join(data[-lines:]))
146187c131baSJan Høydahl                break
146287c131baSJan Høydahl
146387c131baSJan Høydahl
146487c131baSJan Høydahldef run_with_log_tail(command, cwd, logfile=None, tail_lines=10, tee=False, live=False, shell=None):
146587c131baSJan Høydahl    fh = sys.stdout
146687c131baSJan Høydahl    if logfile:
146787c131baSJan Høydahl        logdir = os.path.dirname(logfile)
146887c131baSJan Høydahl        if not os.path.exists(logdir):
146987c131baSJan Høydahl            os.makedirs(logdir)
147087c131baSJan Høydahl        fh = open(logfile, 'w')
147187c131baSJan Høydahl    rc = run_follow(command, cwd, fh=fh, tee=tee, live=live, shell=shell)
147287c131baSJan Høydahl    if logfile:
147387c131baSJan Høydahl        fh.close()
147487c131baSJan Høydahl        if not tee and tail_lines and tail_lines > 0:
147587c131baSJan Høydahl            tail_file(logfile, tail_lines)
147687c131baSJan Høydahl    return rc
147787c131baSJan Høydahl
147887c131baSJan Høydahl
147987c131baSJan Høydahldef ask_yes_no(text):
148087c131baSJan Høydahl    answer = None
148187c131baSJan Høydahl    while answer not in ['y', 'n']:
148287c131baSJan Høydahl        answer = str(input("\nQ: %s (y/n): " % text))
148387c131baSJan Høydahl    print("\n")
148487c131baSJan Høydahl    return answer == 'y'
148587c131baSJan Høydahl
148687c131baSJan Høydahl
148787c131baSJan Høydahldef abbreviate_line(line, width):
148887c131baSJan Høydahl    line = line.rstrip()
148987c131baSJan Høydahl    if len(line) > width:
149087c131baSJan Høydahl        line = "%s.....%s" % (line[:(width / 2 - 5)], line[-(width / 2):])
149187c131baSJan Høydahl    else:
149287c131baSJan Høydahl        line = "%s%s" % (line, " " * (width - len(line) + 2))
149387c131baSJan Høydahl    return line
149487c131baSJan Høydahl
149587c131baSJan Høydahl
149687c131baSJan Høydahldef print_line_cr(line, linenum, stdout=True, tee=False):
149787c131baSJan Høydahl    if not tee:
149887c131baSJan Høydahl        if not stdout:
149987c131baSJan Høydahl            print("[line %s] %s" % (linenum, abbreviate_line(line, 80)), end='\r')
150087c131baSJan Høydahl    else:
150187c131baSJan Høydahl        if line.endswith("\r"):
150287c131baSJan Høydahl            print(line.rstrip(), end='\r')
150387c131baSJan Høydahl        else:
150487c131baSJan Høydahl            print(line.rstrip())
150587c131baSJan Høydahl
150687c131baSJan Høydahl
150787c131baSJan Høydahldef run_follow(command, cwd=None, fh=sys.stdout, tee=False, live=False, shell=None):
150887c131baSJan Høydahl    doShell = '&&' in command or '&' in command or shell is not None
150987c131baSJan Høydahl    if not doShell and not isinstance(command, list):
151087c131baSJan Høydahl        command = shlex.split(command)
151187c131baSJan Høydahl    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd,
151287c131baSJan Høydahl                               universal_newlines=True, bufsize=0, close_fds=True, shell=doShell)
151387c131baSJan Høydahl    lines_written = 0
151487c131baSJan Høydahl
151587c131baSJan Høydahl    fl = fcntl.fcntl(process.stdout, fcntl.F_GETFL)
151687c131baSJan Høydahl    fcntl.fcntl(process.stdout, fcntl.F_SETFL, fl | os.O_NONBLOCK)
151787c131baSJan Høydahl
151887c131baSJan Høydahl    flerr = fcntl.fcntl(process.stderr, fcntl.F_GETFL)
151987c131baSJan Høydahl    fcntl.fcntl(process.stderr, fcntl.F_SETFL, flerr | os.O_NONBLOCK)
152087c131baSJan Høydahl
152187c131baSJan Høydahl    endstdout = endstderr = False
152287c131baSJan Høydahl    errlines = []
152387c131baSJan Høydahl    while not (endstderr and endstdout):
152487c131baSJan Høydahl        lines_before = lines_written
152587c131baSJan Høydahl        if not endstdout:
152687c131baSJan Høydahl            try:
152787c131baSJan Høydahl                if live:
152887c131baSJan Høydahl                    chars = process.stdout.read()
152987c131baSJan Høydahl                    if chars == '' and process.poll() is not None:
153087c131baSJan Høydahl                        endstdout = True
153187c131baSJan Høydahl                    else:
153287c131baSJan Høydahl                        fh.write(chars)
153387c131baSJan Høydahl                        fh.flush()
153487c131baSJan Høydahl                        if '\n' in chars:
153587c131baSJan Høydahl                            lines_written += 1
153687c131baSJan Høydahl                else:
153787c131baSJan Høydahl                    line = process.stdout.readline()
153887c131baSJan Høydahl                    if line == '' and process.poll() is not None:
153987c131baSJan Høydahl                        endstdout = True
154087c131baSJan Høydahl                    else:
154187c131baSJan Høydahl                        fh.write("%s\n" % line.rstrip())
154287c131baSJan Høydahl                        fh.flush()
154387c131baSJan Høydahl                        lines_written += 1
154487c131baSJan Høydahl                        print_line_cr(line, lines_written, stdout=(fh == sys.stdout), tee=tee)
154587c131baSJan Høydahl
154687c131baSJan Høydahl            except Exception as ioe:
154787c131baSJan Høydahl                pass
154887c131baSJan Høydahl        if not endstderr:
154987c131baSJan Høydahl            try:
155087c131baSJan Høydahl                if live:
155187c131baSJan Høydahl                    chars = process.stderr.read()
155287c131baSJan Høydahl                    if chars == '' and process.poll() is not None:
155387c131baSJan Høydahl                        endstderr = True
155487c131baSJan Høydahl                    else:
155587c131baSJan Høydahl                        fh.write(chars)
155687c131baSJan Høydahl                        fh.flush()
155787c131baSJan Høydahl                        if '\n' in chars:
155887c131baSJan Høydahl                            lines_written += 1
155987c131baSJan Høydahl                else:
156087c131baSJan Høydahl                    line = process.stderr.readline()
156187c131baSJan Høydahl                    if line == '' and process.poll() is not None:
156287c131baSJan Høydahl                        endstderr = True
156387c131baSJan Høydahl                    else:
156487c131baSJan Høydahl                        errlines.append("%s\n" % line.rstrip())
156587c131baSJan Høydahl                        lines_written += 1
156687c131baSJan Høydahl                        print_line_cr(line, lines_written, stdout=(fh == sys.stdout), tee=tee)
156787c131baSJan Høydahl            except Exception as e:
156887c131baSJan Høydahl                pass
156987c131baSJan Høydahl
157087c131baSJan Høydahl        if not lines_written > lines_before:
157187c131baSJan Høydahl            # if no output then sleep a bit before checking again
157287c131baSJan Høydahl            time.sleep(0.1)
157387c131baSJan Høydahl
157487c131baSJan Høydahl    print(" " * 80)
157587c131baSJan Høydahl    rc = process.poll()
157687c131baSJan Høydahl    if len(errlines) > 0:
157787c131baSJan Høydahl        for line in errlines:
157887c131baSJan Høydahl            fh.write("%s\n" % line.rstrip())
157987c131baSJan Høydahl            fh.flush()
158087c131baSJan Høydahl    return rc
158187c131baSJan Høydahl
158287c131baSJan Høydahl
158387c131baSJan Høydahldef is_windows():
158487c131baSJan Høydahl    return platform.system().startswith("Win")
158587c131baSJan Høydahl
1586329e7c7bSJan Høydahldef is_mac():
1587329e7c7bSJan Høydahl    return platform.system().startswith("Darwin")
1588329e7c7bSJan Høydahl
1589329e7c7bSJan Høydahldef is_linux():
1590329e7c7bSJan Høydahl    return platform.system().startswith("Linux")
159187c131baSJan Høydahl
159287c131baSJan Høydahlclass Commands(SecretYamlObject):
159387c131baSJan Høydahl    yaml_tag = u'!Commands'
159487c131baSJan Høydahl    hidden_fields = ['todo_id']
159587c131baSJan Høydahl    cmd_continuation_char = "^" if is_windows() else "\\"
159687c131baSJan Høydahl    def __init__(self, root_folder, commands_text=None, commands=None, logs_prefix=None, run_text=None, enable_execute=None,
159787c131baSJan Høydahl                 confirm_each_command=None, env=None, vars=None, todo_id=None, remove_files=None):
159887c131baSJan Høydahl        self.root_folder = root_folder
159987c131baSJan Høydahl        self.commands_text = commands_text
160087c131baSJan Høydahl        self.vars = vars
160187c131baSJan Høydahl        self.env = env
160287c131baSJan Høydahl        self.run_text = run_text
160387c131baSJan Høydahl        self.remove_files = remove_files
160487c131baSJan Høydahl        self.todo_id = todo_id
160587c131baSJan Høydahl        self.logs_prefix = logs_prefix
160687c131baSJan Høydahl        self.enable_execute = enable_execute
160787c131baSJan Høydahl        self.confirm_each_command = confirm_each_command
160887c131baSJan Høydahl        self.commands = commands
160987c131baSJan Høydahl        for c in self.commands:
161087c131baSJan Høydahl            c.todo_id = todo_id
161187c131baSJan Høydahl
161287c131baSJan Høydahl    @classmethod
161387c131baSJan Høydahl    def from_yaml(cls, loader, node):
161487c131baSJan Høydahl        fields = loader.construct_mapping(node, deep = True)
161587c131baSJan Høydahl        return Commands(**fields)
161687c131baSJan Høydahl
161787c131baSJan Høydahl    def run(self):
161887c131baSJan Høydahl        root = self.get_root_folder()
161987c131baSJan Høydahl
162087c131baSJan Høydahl        if self.commands_text:
162187c131baSJan Høydahl            print(self.get_commands_text())
162287c131baSJan Høydahl        if self.env:
162387c131baSJan Høydahl            for key in self.env:
162487c131baSJan Høydahl                val = self.jinjaify(self.env[key])
162587c131baSJan Høydahl                os.environ[key] = val
162687c131baSJan Høydahl                if is_windows():
162787c131baSJan Høydahl                    print("\n  SET %s=%s" % (key, val))
162887c131baSJan Høydahl                else:
162987c131baSJan Høydahl                    print("\n  export %s=%s" % (key, val))
163087c131baSJan Høydahl        print(abbreviate_homedir("\n  cd %s" % root))
163187c131baSJan Høydahl        commands = ensure_list(self.commands)
163287c131baSJan Høydahl        for cmd in commands:
163387c131baSJan Høydahl            for line in cmd.display_cmd():
163487c131baSJan Høydahl                print("  %s" % line)
163587c131baSJan Høydahl        print()
163687c131baSJan Høydahl        if not self.enable_execute is False:
163787c131baSJan Høydahl            if self.run_text:
163887c131baSJan Høydahl                print("\n%s\n" % self.get_run_text())
163987c131baSJan Høydahl            if len(commands) > 1:
164087c131baSJan Høydahl                if not self.confirm_each_command is False:
164187c131baSJan Høydahl                    print("You will get prompted before running each individual command.")
164287c131baSJan Høydahl                else:
164387c131baSJan Høydahl                    print(
164487c131baSJan Høydahl                        "You will not be prompted for each command but will see the ouput of each. If one command fails the execution will stop.")
164587c131baSJan Høydahl            success = True
164687c131baSJan Høydahl            if ask_yes_no("Do you want me to run these commands now?"):
164787c131baSJan Høydahl                if self.remove_files:
164887c131baSJan Høydahl                    for _f in ensure_list(self.get_remove_files()):
164987c131baSJan Høydahl                        f = os.path.join(root, _f)
165087c131baSJan Høydahl                        if os.path.exists(f):
165187c131baSJan Høydahl                            filefolder = "File" if os.path.isfile(f) else "Folder"
165287c131baSJan Høydahl                            if ask_yes_no("%s %s already exists. Shall I remove it now?" % (filefolder, f)) and not dry_run:
165387c131baSJan Høydahl                                if os.path.isdir(f):
165487c131baSJan Høydahl                                    shutil.rmtree(f)
165587c131baSJan Høydahl                                else:
165687c131baSJan Høydahl                                    os.remove(f)
165787c131baSJan Høydahl                index = 0
165887c131baSJan Høydahl                log_folder = self.logs_prefix if len(commands) > 1 else None
165987c131baSJan Høydahl                for cmd in commands:
166087c131baSJan Høydahl                    index += 1
166187c131baSJan Høydahl                    if len(commands) > 1:
166287c131baSJan Høydahl                        log_prefix = "%02d_" % index
166387c131baSJan Høydahl                    else:
166487c131baSJan Høydahl                        log_prefix = self.logs_prefix if self.logs_prefix else ''
166587c131baSJan Høydahl                    if not log_prefix[-1:] == '_':
166687c131baSJan Høydahl                        log_prefix += "_"
166787c131baSJan Høydahl                    cwd = root
166887c131baSJan Høydahl                    if cmd.cwd:
166987c131baSJan Høydahl                        cwd = os.path.join(root, cmd.cwd)
167087c131baSJan Høydahl                    folder_prefix = ''
167187c131baSJan Høydahl                    if cmd.cwd:
167287c131baSJan Høydahl                        folder_prefix = cmd.cwd + "_"
167387c131baSJan Høydahl                    if self.confirm_each_command is False or len(commands) == 1 or ask_yes_no("Shall I run '%s' in folder '%s'" % (cmd, cwd)):
167487c131baSJan Høydahl                        if self.confirm_each_command is False:
167587c131baSJan Høydahl                            print("------------\nRunning '%s' in folder '%s'" % (cmd, cwd))
167687c131baSJan Høydahl                        logfilename = cmd.logfile
167787c131baSJan Høydahl                        logfile = None
167887c131baSJan Høydahl                        cmd_to_run = "%s%s" % ("echo Dry run, command is: " if dry_run else "", cmd.get_cmd())
167987c131baSJan Høydahl                        if cmd.redirect:
168087c131baSJan Høydahl                            try:
168187c131baSJan Høydahl                                out = run(cmd_to_run, cwd=cwd)
168287c131baSJan Høydahl                                mode = 'a' if cmd.redirect_append is True else 'w'
168387c131baSJan Høydahl                                with open(os.path.join(root, cwd, cmd.get_redirect()), mode) as outfile:
168487c131baSJan Høydahl                                    outfile.write(out)
168587c131baSJan Høydahl                                    outfile.flush()
168687c131baSJan Høydahl                                print("Wrote %s bytes to redirect file %s" % (len(out), cmd.get_redirect()))
168787c131baSJan Høydahl                            except Exception as e:
168887c131baSJan Høydahl                                print("Command %s failed: %s" % (cmd_to_run, e))
168987c131baSJan Høydahl                                success = False
169087c131baSJan Høydahl                                break
169187c131baSJan Høydahl                        else:
169287c131baSJan Høydahl                            if not cmd.stdout:
169387c131baSJan Høydahl                                if not log_folder:
169487c131baSJan Høydahl                                    log_folder = os.path.join(state.get_rc_folder(), "logs")
169587c131baSJan Høydahl                                elif not os.path.isabs(log_folder):
169687c131baSJan Høydahl                                    log_folder = os.path.join(state.get_rc_folder(), "logs", log_folder)
169787c131baSJan Høydahl                                if not logfilename:
169887c131baSJan Høydahl                                    logfilename = "%s.log" % re.sub(r"\W", "_", cmd.get_cmd())
169987c131baSJan Høydahl                                logfile = os.path.join(log_folder, "%s%s%s" % (log_prefix, folder_prefix, logfilename))
170087c131baSJan Høydahl                                if cmd.tee:
170187c131baSJan Høydahl                                    print("Output of command will be printed (logfile=%s)" % logfile)
170287c131baSJan Høydahl                                elif cmd.live:
170387c131baSJan Høydahl                                    print("Output will be shown live byte by byte")
170487c131baSJan Høydahl                                    logfile = None
170587c131baSJan Høydahl                                else:
170687c131baSJan Høydahl                                    print("Wait until command completes... Full log in %s\n" % logfile)
170787c131baSJan Høydahl                                if cmd.comment:
170887c131baSJan Høydahl                                    print("# %s\n" % cmd.get_comment())
170987c131baSJan Høydahl                            start_time = time.time()
171087c131baSJan Høydahl                            returncode = run_with_log_tail(cmd_to_run, cwd, logfile=logfile, tee=cmd.tee, tail_lines=25,
171187c131baSJan Høydahl                                                           live=cmd.live, shell=cmd.shell)
171287c131baSJan Høydahl                            elapsed = time.time() - start_time
171387c131baSJan Høydahl                            if not returncode == 0:
171487c131baSJan Høydahl                                if cmd.should_fail:
171587c131baSJan Høydahl                                    print("Command failed, which was expected")
171687c131baSJan Høydahl                                    success = True
171787c131baSJan Høydahl                                else:
171887c131baSJan Høydahl                                    print("WARN: Command %s returned with error" % cmd.get_cmd())
171987c131baSJan Høydahl                                    success = False
172087c131baSJan Høydahl                                    break
172187c131baSJan Høydahl                            else:
172287c131baSJan Høydahl                                if cmd.should_fail and not dry_run:
172387c131baSJan Høydahl                                    print("Expected command to fail, but it succeeded.")
172487c131baSJan Høydahl                                    success = False
172587c131baSJan Høydahl                                    break
172687c131baSJan Høydahl                                else:
172787c131baSJan Høydahl                                    if elapsed > 30:
172887c131baSJan Høydahl                                        print("Command completed in %s seconds" % elapsed)
172987c131baSJan Høydahl            if not success:
173087c131baSJan Høydahl                print("WARNING: One or more commands failed, you may want to check the logs")
173187c131baSJan Høydahl            return success
173287c131baSJan Høydahl
173387c131baSJan Høydahl    def get_root_folder(self):
173487c131baSJan Høydahl        return self.jinjaify(self.root_folder)
173587c131baSJan Høydahl
173687c131baSJan Høydahl    def get_commands_text(self):
173787c131baSJan Høydahl        return self.jinjaify(self.commands_text)
173887c131baSJan Høydahl
173987c131baSJan Høydahl    def get_run_text(self):
174087c131baSJan Høydahl        return self.jinjaify(self.run_text)
174187c131baSJan Høydahl
174287c131baSJan Høydahl    def get_remove_files(self):
174387c131baSJan Høydahl        return self.jinjaify(self.remove_files)
174487c131baSJan Høydahl
174587c131baSJan Høydahl    def get_vars(self):
174687c131baSJan Høydahl        myvars = {}
174787c131baSJan Høydahl        if self.vars:
174887c131baSJan Høydahl            for k in self.vars:
174987c131baSJan Høydahl                val = self.vars[k]
175087c131baSJan Høydahl                if callable(val):
175187c131baSJan Høydahl                    myvars[k] = expand_jinja(val(), vars=myvars)
175287c131baSJan Høydahl                else:
175387c131baSJan Høydahl                    myvars[k] = expand_jinja(val, vars=myvars)
175487c131baSJan Høydahl        return myvars
175587c131baSJan Høydahl
175687c131baSJan Høydahl    def jinjaify(self, data, join=False):
175787c131baSJan Høydahl        if not data:
175887c131baSJan Høydahl            return None
175987c131baSJan Høydahl        v = self.get_vars()
176087c131baSJan Høydahl        if self.todo_id:
176187c131baSJan Høydahl            v.update(state.get_todo_by_id(self.todo_id).get_vars())
176287c131baSJan Høydahl        if isinstance(data, list):
176387c131baSJan Høydahl            if join:
176487c131baSJan Høydahl                return expand_jinja(" ".join(data), v)
176587c131baSJan Høydahl            else:
176687c131baSJan Høydahl                res = []
176787c131baSJan Høydahl                for rf in data:
176887c131baSJan Høydahl                    res.append(expand_jinja(rf, v))
176987c131baSJan Høydahl                return res
177087c131baSJan Høydahl        else:
177187c131baSJan Høydahl            return expand_jinja(data, v)
177287c131baSJan Høydahl
177387c131baSJan Høydahl
177487c131baSJan Høydahldef abbreviate_homedir(line):
177587c131baSJan Høydahl    if is_windows():
177687c131baSJan Høydahl        if 'HOME' in os.environ:
177787c131baSJan Høydahl            return re.sub(r'([^/]|\b)%s' % os.path.expanduser('~'), "\\1%HOME%", line)
177887c131baSJan Høydahl        elif 'USERPROFILE' in os.environ:
177987c131baSJan Høydahl            return re.sub(r'([^/]|\b)%s' % os.path.expanduser('~'), "\\1%USERPROFILE%", line)
178087c131baSJan Høydahl    else:
178187c131baSJan Høydahl        return re.sub(r'([^/]|\b)%s' % os.path.expanduser('~'), "\\1~", line)
178287c131baSJan Høydahl
178387c131baSJan Høydahl
178487c131baSJan Høydahlclass Command(SecretYamlObject):
178587c131baSJan Høydahl    yaml_tag = u'!Command'
178687c131baSJan Høydahl    hidden_fields = ['todo_id']
178787c131baSJan Høydahl    def __init__(self, cmd, cwd=None, stdout=None, logfile=None, tee=None, live=None, comment=None, vars=None,
178887c131baSJan Høydahl                 todo_id=None, should_fail=None, redirect=None, redirect_append=None, shell=None):
178987c131baSJan Høydahl        self.cmd = cmd
179087c131baSJan Høydahl        self.cwd = cwd
179187c131baSJan Høydahl        self.comment = comment
179287c131baSJan Høydahl        self.logfile = logfile
179387c131baSJan Høydahl        self.vars = vars
179487c131baSJan Høydahl        self.tee = tee
179587c131baSJan Høydahl        self.live = live
179687c131baSJan Høydahl        self.stdout = stdout
179787c131baSJan Høydahl        self.should_fail = should_fail
179887c131baSJan Høydahl        self.shell = shell
179987c131baSJan Høydahl        self.todo_id = todo_id
180087c131baSJan Høydahl        self.redirect_append = redirect_append
180187c131baSJan Høydahl        self.redirect = redirect
180287c131baSJan Høydahl        if tee and stdout:
180387c131baSJan Høydahl            self.stdout = None
180487c131baSJan Høydahl            print("Command %s specifies 'tee' and 'stdout', using only 'tee'" % self.cmd)
180587c131baSJan Høydahl        if live and stdout:
180687c131baSJan Høydahl            self.stdout = None
180787c131baSJan Høydahl            print("Command %s specifies 'live' and 'stdout', using only 'live'" % self.cmd)
180887c131baSJan Høydahl        if live and tee:
180987c131baSJan Høydahl            self.tee = None
181087c131baSJan Høydahl            print("Command %s specifies 'tee' and 'live', using only 'live'" % self.cmd)
181187c131baSJan Høydahl        if redirect and (tee or stdout or live):
181287c131baSJan Høydahl            self.tee = self.stdout = self.live = None
181387c131baSJan Høydahl            print("Command %s specifies 'redirect' and other out options at the same time. Using redirect only" % self.cmd)
181487c131baSJan Høydahl
181587c131baSJan Høydahl    @classmethod
181687c131baSJan Høydahl    def from_yaml(cls, loader, node):
181787c131baSJan Høydahl        fields = loader.construct_mapping(node, deep = True)
181887c131baSJan Høydahl        return Command(**fields)
181987c131baSJan Høydahl
182087c131baSJan Høydahl    def get_comment(self):
182187c131baSJan Høydahl        return self.jinjaify(self.comment)
182287c131baSJan Høydahl
182387c131baSJan Høydahl    def get_redirect(self):
182487c131baSJan Høydahl        return self.jinjaify(self.redirect)
182587c131baSJan Høydahl
182687c131baSJan Høydahl    def get_cmd(self):
182787c131baSJan Høydahl        return self.jinjaify(self.cmd, join=True)
182887c131baSJan Høydahl
182987c131baSJan Høydahl    def get_vars(self):
183087c131baSJan Høydahl        myvars = {}
183187c131baSJan Høydahl        if self.vars:
183287c131baSJan Høydahl            for k in self.vars:
183387c131baSJan Høydahl                val = self.vars[k]
183487c131baSJan Høydahl                if callable(val):
183587c131baSJan Høydahl                    myvars[k] = expand_jinja(val(), vars=myvars)
183687c131baSJan Høydahl                else:
183787c131baSJan Høydahl                    myvars[k] = expand_jinja(val, vars=myvars)
183887c131baSJan Høydahl        return myvars
183987c131baSJan Høydahl
184087c131baSJan Høydahl    def __str__(self):
184187c131baSJan Høydahl        return self.get_cmd()
184287c131baSJan Høydahl
184387c131baSJan Høydahl    def jinjaify(self, data, join=False):
184487c131baSJan Høydahl        v = self.get_vars()
184587c131baSJan Høydahl        if self.todo_id:
184687c131baSJan Høydahl            v.update(state.get_todo_by_id(self.todo_id).get_vars())
184787c131baSJan Høydahl        if isinstance(data, list):
184887c131baSJan Høydahl            if join:
184987c131baSJan Høydahl                return expand_jinja(" ".join(data), v)
185087c131baSJan Høydahl            else:
185187c131baSJan Høydahl                res = []
185287c131baSJan Høydahl                for rf in data:
185387c131baSJan Høydahl                    res.append(expand_jinja(rf, v))
185487c131baSJan Høydahl                return res
185587c131baSJan Høydahl        else:
185687c131baSJan Høydahl            return expand_jinja(data, v)
185787c131baSJan Høydahl
185887c131baSJan Høydahl    def display_cmd(self):
185987c131baSJan Høydahl        lines = []
186087c131baSJan Høydahl        pre = post = ''
186187c131baSJan Høydahl        if self.comment:
186287c131baSJan Høydahl            if is_windows():
186387c131baSJan Høydahl                lines.append("REM %s" % self.get_comment())
186487c131baSJan Høydahl            else:
186587c131baSJan Høydahl                lines.append("# %s" % self.get_comment())
186687c131baSJan Høydahl        if self.cwd:
186787c131baSJan Høydahl            lines.append("pushd %s" % self.cwd)
186887c131baSJan Høydahl        redir = "" if self.redirect is None else " %s %s" % (">" if self.redirect_append is None else ">>" , self.get_redirect())
186987c131baSJan Høydahl        line = "%s%s" % (expand_multiline(self.get_cmd(), indent=2), redir)
187087c131baSJan Høydahl        # Print ~ or %HOME% rather than the full expanded homedir path
187187c131baSJan Høydahl        line = abbreviate_homedir(line)
187287c131baSJan Høydahl        lines.append(line)
187387c131baSJan Høydahl        if self.cwd:
187487c131baSJan Høydahl            lines.append("popd")
187587c131baSJan Høydahl        return lines
187687c131baSJan Høydahl
187787c131baSJan Høydahlclass UserInput(SecretYamlObject):
187887c131baSJan Høydahl    yaml_tag = u'!UserInput'
187987c131baSJan Høydahl
188087c131baSJan Høydahl    def __init__(self, name, prompt, type=None):
188187c131baSJan Høydahl        self.type = type
188287c131baSJan Høydahl        self.prompt = prompt
188387c131baSJan Høydahl        self.name = name
188487c131baSJan Høydahl
188587c131baSJan Høydahl    @classmethod
188687c131baSJan Høydahl    def from_yaml(cls, loader, node):
188787c131baSJan Høydahl        fields = loader.construct_mapping(node, deep = True)
188887c131baSJan Høydahl        return UserInput(**fields)
188987c131baSJan Høydahl
189087c131baSJan Høydahl    def run(self, dict=None):
189187c131baSJan Høydahl        correct = False
189287c131baSJan Høydahl        while not correct:
189387c131baSJan Høydahl            try:
189487c131baSJan Høydahl                result = str(input("%s : " % self.prompt))
189587c131baSJan Høydahl                if self.type and self.type == 'int':
189687c131baSJan Høydahl                    result = int(result)
189787c131baSJan Høydahl                correct = True
189887c131baSJan Høydahl            except Exception as e:
189987c131baSJan Høydahl                print("Incorrect input: %s, try again" % e)
190087c131baSJan Høydahl                continue
190187c131baSJan Høydahl            if dict:
190287c131baSJan Høydahl                dict[self.name] = result
190387c131baSJan Høydahl            return result
190487c131baSJan Høydahl
190587c131baSJan Høydahl
190687c131baSJan Høydahldef create_ical(todo):
190787c131baSJan Høydahl    if ask_yes_no("Do you want to add a Calendar reminder for the close vote time?"):
190887c131baSJan Høydahl        c = Calendar()
190987c131baSJan Høydahl        e = Event()
191087c131baSJan Høydahl        e.name = "Lucene/Solr %s vote ends" % state.release_version
191187c131baSJan Høydahl        e.begin = vote_close_72h_date()
191287c131baSJan Høydahl        e.description = "Remember to sum up votes and continue release :)"
191387c131baSJan Høydahl        c.events.add(e)
191487c131baSJan Høydahl        ics_file = os.path.join(state.get_rc_folder(), 'vote_end.ics')
191587c131baSJan Høydahl        with open(ics_file, 'w') as my_file:
191687c131baSJan Høydahl            my_file.writelines(c)
191787c131baSJan Høydahl        open_file(ics_file)
191887c131baSJan Høydahl    return True
191987c131baSJan Høydahl
192087c131baSJan Høydahl
192187c131baSJan Høydahltoday = datetime.utcnow().date()
1922d86b473aSJan Høydahlsundays = {(today + timedelta(days=x)): 'Sunday' for x in range(10) if (today + timedelta(days=x)).weekday() == 6}
192387c131baSJan Høydahly = datetime.utcnow().year
192487c131baSJan Høydahlyears = [y, y+1]
192587c131baSJan Høydahlnon_working = holidays.CA(years=years) + holidays.US(years=years) + holidays.England(years=years) \
1926d86b473aSJan Høydahl              + holidays.DE(years=years) + holidays.NO(years=years) + holidays.IND(years=years) + holidays.RU(years=years)
192787c131baSJan Høydahl
192887c131baSJan Høydahl
192987c131baSJan Høydahldef vote_close_72h_date():
1930d86b473aSJan Høydahl    # Voting open at least 72 hours according to ASF policy
1931d86b473aSJan Høydahl    return datetime.utcnow() + timedelta(hours=73)
1932d86b473aSJan Høydahl
1933d86b473aSJan Høydahl
1934d86b473aSJan Høydahldef vote_close_72h_holidays():
1935d86b473aSJan Høydahl    days = 0
193687c131baSJan Høydahl    day_offset = -1
1937d86b473aSJan Høydahl    holidays = []
1938d86b473aSJan Høydahl    # Warn RM about major holidays coming up that should perhaps extend the voting deadline
1939d86b473aSJan Høydahl    # Warning will be given for Sunday or a public holiday observed by 3 or more [CA, US, EN, DE, NO, IND, RU]
1940d86b473aSJan Høydahl    while days < 3:
194187c131baSJan Høydahl        day_offset += 1
194287c131baSJan Høydahl        d = today + timedelta(days=day_offset)
1943d86b473aSJan Høydahl        if not (d in sundays or (d in non_working and len(non_working[d]) >= 2)):
1944d86b473aSJan Høydahl            days += 1
1945d86b473aSJan Høydahl        else:
1946d86b473aSJan Høydahl            if d in sundays:
1947d86b473aSJan Høydahl                holidays.append("%s (Sunday)" % d)
1948d86b473aSJan Høydahl            else:
1949d86b473aSJan Høydahl                holidays.append("%s (%s)" % (d, non_working[d]))
1950d86b473aSJan Høydahl    return holidays if len(holidays) > 0 else None
195187c131baSJan Høydahl
195287c131baSJan Høydahl
1953329e7c7bSJan Høydahldef prepare_announce_lucene(todo):
1954329e7c7bSJan Høydahl    if not os.path.exists(lucene_news_file):
195587c131baSJan Høydahl        lucene_text = expand_jinja("(( template=announce_lucene ))")
1956329e7c7bSJan Høydahl        with open(lucene_news_file, 'w') as fp:
195787c131baSJan Høydahl            fp.write(lucene_text)
195887c131baSJan Høydahl        # print("Wrote Lucene announce draft to %s" % lucene_news_file)
1959329e7c7bSJan Høydahl    else:
1960329e7c7bSJan Høydahl        print("Draft already exist, not re-generating")
1961329e7c7bSJan Høydahl    return True
196287c131baSJan Høydahl
1963329e7c7bSJan Høydahldef prepare_announce_solr(todo):
1964329e7c7bSJan Høydahl    if not os.path.exists(solr_news_file):
196587c131baSJan Høydahl        solr_text = expand_jinja("(( template=announce_solr ))")
1966329e7c7bSJan Høydahl        with open(solr_news_file, 'w') as fp:
196787c131baSJan Høydahl            fp.write(solr_text)
196887c131baSJan Høydahl        # print("Wrote Solr announce draft to %s" % solr_news_file)
196987c131baSJan Høydahl    else:
1970329e7c7bSJan Høydahl        print("Draft already exist, not re-generating")
197187c131baSJan Høydahl    return True
197287c131baSJan Høydahl
197387c131baSJan Høydahl
197487c131baSJan Høydahldef set_java_home(version):
197587c131baSJan Høydahl    os.environ['JAVA_HOME'] = state.get_java_home_for_version(version)
197687c131baSJan Høydahl    os.environ['JAVACMD'] = state.get_java_cmd_for_version(version)
197787c131baSJan Høydahl
197887c131baSJan Høydahl
1979329e7c7bSJan Høydahldef load_lines(file, from_line=0):
198087c131baSJan Høydahl    if os.path.exists(file):
198187c131baSJan Høydahl        with open(file, 'r') as fp:
1982329e7c7bSJan Høydahl            return fp.readlines()[from_line:]
198387c131baSJan Høydahl    else:
1984329e7c7bSJan Høydahl        return ["<Please paste the announcement text here>\n"]
198587c131baSJan Høydahl
198687c131baSJan Høydahl
198787c131baSJan Høydahlif __name__ == '__main__':
198887c131baSJan Høydahl    try:
198987c131baSJan Høydahl        main()
199087c131baSJan Høydahl    except KeyboardInterrupt:
199187c131baSJan Høydahl        print('Keyboard interrupt...exiting')
1992