#!/usr/bin/env python # # weewx --- A simple, high-performance weather station server # # Copyright (c) 2009-2020 Tom Keffer # # See the file LICENSE.txt for your full rights. # """Customized setup file for weewx. For more debug information, set the environment variable DISTUTILS_DEBUG before running. """ from __future__ import absolute_import from __future__ import print_function from __future__ import with_statement import fnmatch import os.path import re import shutil import subprocess import sys import tempfile from distutils import log from distutils.command.install import install from distutils.command.install_data import install_data from distutils.command.install_lib import install_lib from distutils.core import setup from distutils.debug import DEBUG VERSION = "4.3.0a2" if sys.version_info < (2, 7): log.fatal('WeeWX requires Python V2.7 or greater.') log.fatal('For earlier versions of Python, use WeeWX V3.9.') sys.exit("Python version unsupported.") this_file = os.path.join(os.getcwd(), __file__) this_dir = os.path.abspath(os.path.dirname(this_file)) # ============================================================================== # install # ============================================================================== class weewx_install(install): """Specialized version of install, which adds a '--no-prompt' option, and which runs a wee_config post-install script""" # Add an option for --no-prompt. This will be passed on to wee_config user_options = install.user_options + [('no-prompt', None, 'Do not prompt for station info')] def initialize_options(self, *args, **kwargs): install.initialize_options(self, *args, **kwargs) self.no_prompt = None def finalize_options(self): # Call my superclass's version install.finalize_options(self) # Unless the --force flag has been explicitly set, default to True. This will # cause files to be installed even if they are older than their target.""" if self.force is None: self.force = 1 if self.no_prompt is None: self.no_prompt = 0 def run(self): """Specialized version of run, which runs post-install commands""" # First run the install. rv = install.run(self) # Now the post-install update_and_install_config(self.install_data, self.install_scripts, self.install_lib, self.no_prompt) return rv # ============================================================================== # install_lib # ============================================================================== class weewx_install_lib(install_lib): """Specialized version of install_lib, which saves and restores the 'user' subdirectory.""" def run(self): """Specialized version of run that saves, then restores, the 'user' subdirectory.""" # Save any existing 'user' subdirectory: user_dir = os.path.join(self.install_dir, 'user') if not self.dry_run and os.path.exists(user_dir): user_backup_dir = user_dir + ".bak" shutil.move(user_dir, user_backup_dir) else: user_backup_dir = None # Run the superclass's version. This will install a new 'user' subdirectory. install_lib.run(self) # Restore the 'user' subdirectory if user_backup_dir: # Delete the freshly installed user subdirectory shutil.rmtree(user_dir) # Replace it with our saved version. shutil.move(user_backup_dir, user_dir) # ============================================================================== # install_data # ============================================================================== class weewx_install_data(install_data): """Specialized version of install_data.""" def run(self): # If there is a skins directory already, just install what the user doesn't already have. if os.path.exists(os.path.join(self.install_dir, 'skins')): # A skins directory already exists. Build a list of skins that are missing and should # be added to it. install_files = [] for skin_name in ['Ftp', 'Mobile', 'Rsync', 'Seasons', 'Smartphone', 'Standard']: rel_name = 'skins/' + skin_name if not os.path.exists(os.path.join(self.install_dir, rel_name)): # The skin has not already been installed. Include it. install_files += [dat for dat in self.data_files if dat[0].startswith(rel_name)] # Exclude all the skins files... other_files = [dat for dat in self.data_files if not dat[0].startswith('skins')] # ... then add the needed skins back in self.data_files = other_files + install_files # Run the superclass's run(): return install_data.run(self) def copy_file(self, f, install_dir, **kwargs): # If this is the configuration file, then process it separately if f == 'weewx.conf': rv = self.process_config_file(f, install_dir, **kwargs) else: rv = install_data.copy_file(self, f, install_dir, **kwargs) return rv def process_config_file(self, f, install_dir, **kwargs): """Process the configuration file weewx.conf by inserting the proper path into WEEWX_ROOT, then install as weewx.conf.X.Y.Z, where X.Y.Z is the version number.""" # The install directory. The normalization is necessary because sometimes there # is a trailing '/'. norm_install_dir = os.path.normpath(install_dir) # The path to the destination configuration file. It will look # something like '/home/weewx/weewx.conf.4.0.0' weewx_install_path = os.path.join(norm_install_dir, os.path.basename(f) + '.' + VERSION) if self.dry_run: return weewx_install_path, 0 # This RE is for finding the assignment to WEEWX_ROOT. # It matches the assignment, plus an optional comment. For example, for the string # ' WEEWX_ROOT = foo # A comment' # it matches 3 groups: # Group 1: ' WEEWX_ROOT ' # Group 2: ' foo' # Group 3: ' # A comment' pattern = re.compile(r'(^\s*WEEWX_ROOT\s*)=(\s*\S+)(\s*#?.*)') done = 0 if self.verbose: log.info("massaging %s -> %s", f, weewx_install_path) # Massage the incoming file, assigning the right value for WEEWX_ROOT. Use a temporary # file. This will help in making the operatioin atomic. try: # Open up the incoming configuration file with open(f, mode='rt') as incoming_fd: # Open up a temporary file tmpfd, tmpfn = tempfile.mkstemp() with os.fdopen(tmpfd, 'wt') as tmpfile: # Go through the incoming template file line by line, inserting a value for # WEEWX_ROOT. There's only one per file, so stop looking after the first one. for line in incoming_fd: if not done: line, done = pattern.subn("\\1 = %s\\3" % norm_install_dir, line) tmpfile.write(line) if not self.dry_run: # If not a dry run, install the temporary file in the right spot rv = install_data.copy_file(self, tmpfn, weewx_install_path, **kwargs) # Set the permission bits: shutil.copymode(f, weewx_install_path) finally: # Get rid of the temporary file os.remove(tmpfn) return rv # ============================================================================== # utilities # ============================================================================== def find_files(directory, file_excludes=['*.pyc', "junk*"], dir_excludes=['*/__pycache__']): """Find all files under a directory, honoring some exclusions. Returns: A list of two-way tuples (directory_path, file_list), where file_list is a list of relative file paths. """ # First recursively create a list of all the directories dir_list = [] for dirpath, _, _ in os.walk(directory): # Make sure the directory name doesn't match the excluded pattern if not any(fnmatch.fnmatch(dirpath, d) for d in dir_excludes): dir_list.append(dirpath) data_files = [] # Now search each directory for all files for d_path in dir_list: file_list = [] # Find all the files in this directory for fn in os.listdir(d_path): filepath = os.path.join(d_path, fn) # Make sure it's a file, and that it's name doesn't match the excluded pattern if os.path.isfile(filepath) \ and not any(fnmatch.fnmatch(filepath, f) for f in file_excludes): file_list.append(filepath) # Add the directory and the list of files in it, to the list of all files. data_files.append((d_path, file_list)) return data_files def update_and_install_config(install_dir, install_scripts, install_lib, no_prompt=False, config_name='weewx.conf'): """Install the configuration file, weewx.conf, updating it if necessary. install_dir: the directory containing the configuration file. install_scripts: the directory containing the weewx executables. install_lib: the directory containing the weewx packages. no_prompt: Pass a '--no-prompt' flag on to wee_config. config_name: the name of the configuration file. Defaults to 'weewx.conf' """ # This is where the weewx.conf file will go config_path = os.path.join(install_dir, config_name) # Is there an existing file? if os.path.isfile(config_path): # Yes, so this is an upgrade args = [sys.executable, os.path.join(install_scripts, 'wee_config'), '--upgrade', '--config=%s' % config_path, '--dist-config=%s' % config_path + '.' + VERSION, '--output=%s' % config_path, ] else: # No existing config file, so this is a fresh install. args = [sys.executable, os.path.join(install_scripts, 'wee_config'), '--install', '--dist-config=%s' % config_path + '.' + VERSION, '--output=%s' % config_path, ] # Add the --no-prompt flag if the user requested it. if no_prompt: args += ['--no-prompt'] if DEBUG: log.info("Command used to invoke wee_config: %s" % args) proc = subprocess.Popen(args, env={'PYTHONPATH': install_lib}, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr) out, err = proc.communicate() if DEBUG and out: log.info('out=', out.decode()) if DEBUG and err: log.info('err=', err.decode()) # ============================================================================== # main entry point # ============================================================================== if __name__ == "__main__": setup(name='weewx', version=VERSION, description='The WeeWX weather software system', long_description="WeeWX interacts with a weather station to produce graphs, reports, " "and HTML pages. WeeWX can upload data to services such as the " "WeatherUnderground, PWSweather.com, or CWOP.", author='Tom Keffer', author_email='tkeffer@gmail.com', url='http://www.weewx.com', license='GPLv3', py_modules=['daemon', 'six'], package_dir={'': 'bin'}, packages=['schemas', 'user', 'weecfg', 'weedb', 'weeimport', 'weeplot', 'weeutil', 'weewx', 'weewx.drivers'], scripts=['bin/wee_config', 'bin/wee_database', 'bin/wee_debug', 'bin/wee_device', 'bin/wee_extension', 'bin/wee_import', 'bin/wee_reports', 'bin/weewxd', 'bin/wunderfixer'], data_files=[('', ['LICENSE.txt', 'README.md', 'weewx.conf']), ] + find_files('docs') + find_files('examples') + find_files('skins') + find_files('util'), cmdclass={ "install": weewx_install, "install_data": weewx_install_data, "install_lib": weewx_install_lib, }, )