Skip to content

Commit

Permalink
Merge pull request #1091 from ptomulik/fix/python_spec
Browse files Browse the repository at this point in the history
add FPM::Util.execmd and fix python_spec.rb
  • Loading branch information
jordansissel committed Apr 28, 2016
2 parents 627513d + dc7e732 commit 15497b2
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 54 deletions.
179 changes: 141 additions & 38 deletions lib/fpm/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,42 +45,158 @@ def default_shell
return shell
end

# Run a command safely in a way that gets reports useful errors.
def safesystem(*args)
# ChildProcess isn't smart enough to run a $SHELL if there's
# spaces in the first arg and there's only 1 arg.
if args.size == 1
args = [ default_shell, "-c", args[0] ]
############################################################################
# execmd([env,] cmd [,opts])
#
# Execute a command as a child process. The function allows to:
#
# - pass environment variables to child process,
# - communicate with stdin, stdout and stderr of the child process via pipes,
# - retrieve execution's status code.
#
# ---- EXAMPLE 1 (simple execution)
#
# if execmd(['which', 'python']) == 0
# p "Python is installed"
# end
#
# ---- EXAMPLE 2 (custom environment variables)
#
# execmd({:PYTHONPATH=>'/home/me/foo'}, [ 'python', '-m', 'bar'])
#
# ---- EXAMPLE 3 (communicating via stdin, stdout and stderr)
#
# script = <<PYTHON
# import sys
# sys.stdout.write("normal output\n")
# sys.stdout.write("narning or error\n")
# PYTHON
# status = execmd('python') do |stdin,stdout,stderr|
# stdin.write(script)
# stdin.close
# p "STDOUT: #{stdout.read}"
# p "STDERR: #{stderr.read}"
# end
# p "STATUS: #{status}"
#
# ---- EXAMPLE 4 (additional options)
#
# execmd(['which', 'python'], :process=>true, :stdin=>false, :stderr=>false) do |process,stdout|
# p = stdout.read.chomp
# process.wait
# if (x = process.exit_code) == 0
# p "PYTHON: #{p}"
# else
# p "ERROR: #{x}"
# end
# end
#
#
# OPTIONS:
#
# :process (default: false) -- pass process object as the first argument the to block,
# :stdin (default: true) -- pass stdin object of the child process to the block for writting,
# :stdout (default: true) -- pass stdout object of the child process to the block for reading,
# :stderr (default: true) -- pass stderr object of the child process to the block for reading,
#
def execmd(*args)
i = 0
if i < args.size
if args[i].kind_of?(Hash)
# args[0] may contain environment variables
env = args[i]
i += 1
else
env = Hash[]
end
end

if i < args.size
if args[i].kind_of?(Array)
args2 = args[i]
else
args2 = [ args[i] ]
end
program = args2[0]
i += 1
else
raise ArgumentError.new("missing argument: cmd")
end
program = args[0]

if !program_exists?(program)
if i < args.size
if args[i].kind_of?(Hash)
opts = Hash[args[i].map {|k,v| [k.to_sym, v]} ]
i += 1
end
else
opts = Hash[]
end

opts[:process] = false unless opts.include?(:process)
opts[:stdin] = true unless opts.include?(:stdin)
opts[:stdout] = true unless opts.include?(:stdout)
opts[:stderr] = true unless opts.include?(:stderr)

if !program.include?("/") and !program_in_path?(program)
raise ExecutableNotFound.new(program)
end

logger.debug("Running command", :args => args)
logger.debug("Running command", :args => args2)

# Create a pair of pipes to connect the
# invoked process to the cabin logger
stdout_r, stdout_w = IO.pipe
stderr_r, stderr_w = IO.pipe

process = ChildProcess.build(*args)
process = ChildProcess.build(*args2)
process.environment.merge!(env)

process.io.stdout = stdout_w
process.io.stderr = stderr_w

if block_given? and opts[:stdin]
process.duplex = true
end

process.start

stdout_w.close; stderr_w.close
logger.debug('Process is running', :pid => process.pid)
# Log both stdout and stderr as 'info' because nobody uses stderr for
# actually reporting errors and as a result 'stderr' is a misnomer.
logger.pipe(stdout_r => :info, stderr_r => :info)
logger.debug("Process is running", :pid => process.pid)
if block_given?
args3 = []
args3.push(process) if opts[:process]
args3.push(process.io.stdin) if opts[:stdin]
args3.push(stdout_r) if opts[:stdout]
args3.push(stderr_r) if opts[:stderr]

yield(*args3)

process.io.stdin.close if opts[:stdin] and not process.io.stdin.closed?
stdout_r.close unless stdout_r.closed?
stderr_r.close unless stderr_r.closed?
else
# Log both stdout and stderr as 'info' because nobody uses stderr for
# actually reporting errors and as a result 'stderr' is a misnomer.
logger.pipe(stdout_r => :info, stderr_r => :info)
end

process.wait if process.alive?

process.wait
success = (process.exit_code == 0)
return process.exit_code
end # def execmd

# Run a command safely in a way that gets reports useful errors.
def safesystem(*args)
# ChildProcess isn't smart enough to run a $SHELL if there's
# spaces in the first arg and there's only 1 arg.
if args.size == 1
args = [ default_shell, "-c", args[0] ]
end
program = args[0]

exit_code = execmd(args)
success = (exit_code == 0)

if !success
raise ProcessFailed.new("#{program} failed (exit code #{process.exit_code})" \
raise ProcessFailed.new("#{program} failed (exit code #{exit_code})" \
". Full command was:#{args.inspect}")
end
return success
Expand All @@ -97,29 +213,16 @@ def safesystemout(*args)
raise ExecutableNotFound.new(program)
end

logger.debug("Running command", :args => args)

stdout_r, stdout_w = IO.pipe
stderr_r, stderr_w = IO.pipe

process = ChildProcess.build(*args)
process.io.stdout = stdout_w
process.io.stderr = stderr_w

process.start
stdout_w.close; stderr_w.close
stdout_r_str = stdout_r.read
stdout_r.close; stderr_r.close
logger.debug("Process is running", :pid => process.pid)

process.wait
success = (process.exit_code == 0)
stdout_r_str = nil
exit_code = execmd(args, :stdin=>false, :stderr=>false) do |stdout|
stdout_r_str = stdout.read
end
success = (exit_code == 0)

if !success
raise ProcessFailed.new("#{program} failed (exit code #{process.exit_code})" \
raise ProcessFailed.new("#{program} failed (exit code #{exit_code})" \
". Full command was:#{args.inspect}")
end

return stdout_r_str
end # def safesystemout

Expand Down
28 changes: 28 additions & 0 deletions spec/fixtures/python/easy_install_default.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# The following python code helps predicting easy_install's default behavior.
# See: http://stackoverflow.com/a/9155056
from setuptools.command.easy_install import easy_install
class _easy_install_default(easy_install):
""" class easy_install had problems with the fist parameter not being
an instance of Distribution, even though it was. This is due to
some import-related mess.
"""

def __init__(self):
from distutils.dist import Distribution
dist = Distribution()
self.distribution = dist
self.initialize_options()
self._dry_run = None
self.verbose = dist.verbose
self.force = None
self.help = 0
self.finalized = 0

default_options = _easy_install_default()
import distutils.errors
try:
default_options.finalize_options()
except distutils.errors.DistutilsError:
pass

__all__=[default_options]
32 changes: 16 additions & 16 deletions spec/fpm/package/python_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ def python_usable?
"'python' and/or 'easy_install' isn't in your PATH")
end

# Determine default value of a given easy_install's option
def easy_install_default(option)
result = nil
execmd({:PYTHONPATH=>"#{example_dir}"}, 'python', :stderr=>false) do |stdin,stdout|
stdin.write("from easy_install_default import default_options\n" \
"print default_options.#{option}\n")
stdin.close
result = stdout.read.chomp
end
return result
end

describe FPM::Package::Python, :if => python_usable? do
let (:example_dir) do
File.expand_path("../../fixtures/python/", File.dirname(__FILE__))
Expand Down Expand Up @@ -164,25 +176,13 @@ def python_usable?

context "python_scripts_executable is set" do
it "should have scripts with a custom hashbang line" do
#subject.attributes[:python_install_bin] = '/usr/bin'
subject.attributes[:python_scripts_executable] = "fancypants"
subject.input("django")

# Get the default scripts install directory and use it to find django-admin.py from Django
# Then let's make sure the scripts executable setting worked!
if /darwin/ =~ RUBY_PLATFORM
# OSX
django_bindir = %x{python -c 'from distutils.sysconfig import get_config_var; print(get_config_var("BINDIR"))'}.chomp
elsif /cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM
# Windows
# (ptomulik) I'm not sure about this
django_bindir = %x{python -c 'from distutils.sysconfig import get_config_var; print(get_config_var("BINDIR"))'}.chomp
else
# Unix
#django_bindir = '/usr/local/bin'
django_bindir = %x{python -c 'from distutils.sysconfig import get_config_var; print(get_config_var("BINDIR"))'}.chomp
end
path = subject.staging_path(File.join(django_bindir, "django-admin.py"))
# Determine, where 'easy_install' is going to install scripts
script_dir = easy_install_default('script_dir')

path = subject.staging_path(File.join(script_dir, "django-admin.py"))

# Read the first line (the hashbang line) of the django-admin.py script
fd = File.new(path, "r")
Expand Down

0 comments on commit 15497b2

Please sign in to comment.