summaryrefslogtreecommitdiff
blob: 7e7e71d960b30f179f38b33878c6a16734469ea8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
From 096a74009cea9c79bcc2729d18a3cbcb99783aeb Mon Sep 17 00:00:00 2001
From: Michael Haubenwallner <michael.haubenwallner@salomon.at>
Date: Wed, 6 Nov 2013 12:40:05 +0100
Subject: [PATCH] Add ebuildshell feature, bug#155161.

---
 bin/ebuild.sh                        | 102 ++++++++++++++++++++++++++++++++++-
 bin/filter-bash-environment.py       |  65 ++++++++++++++++------
 bin/save-ebuild-env.sh               |   2 +-
 man/make.conf.5                      |   6 +++
 pym/_emerge/AbstractEbuildProcess.py |   1 +
 pym/portage/const.py                 |   1 +
 6 files changed, 159 insertions(+), 18 deletions(-)

diff --git a/bin/ebuild.sh b/bin/ebuild.sh
index f1586b2..06c90df 100755
--- a/bin/ebuild.sh
+++ b/bin/ebuild.sh
@@ -130,7 +130,7 @@ __qa_source() {
 __qa_call() {
 	local shopts=$(shopt) OLDIFS="$IFS"
 	local retval
-	"$@"
+	__call-ebuildshell "$@"
 	retval=$?
 	set +e
 	[[ $shopts != $(shopt) ]] &&
@@ -537,6 +537,106 @@ if [[ -n ${QA_INTERCEPTORS} ]] ; then
 	unset BIN_PATH BIN BODY FUNC_SRC
 fi
 
+__call-ebuildshell() {
+	if ! has ebuildshell ${FEATURES}; then
+		"$@"
+		return $?
+	fi
+	local __ebuildshell_args=( "$@" )
+	# These are the variables I have seen 'bash -i' maintaining the values for:
+	local __ebuildshell_bash_i_vars="__ebuildshell_.*
+		_ BASH_ARGC BASH_ARGV BASH_COMMAND BASH_LINENO BASH_SOURCE
+		BASH_VERSINFO BASH_SUBSHELL BASHOPTS BASHPID COMP_WORDBREAKS
+		DIRSTACK EUID FUNCNAME GROUPS HISTCMD HISTFILE LINENO
+		PIPESTATUS PPID PWD RANDOM SECONDS SHELLOPTS UID"
+	# Allow recursive ebuildshell, for use in multibuild.eclass and similar:
+	local __ebuildshell_pid=${BASHPID:-$(__bashpid)}
+	local __ebuildshell_tmpf="${T}/ebuildshell.${__ebuildshell_pid}"
+	rm -f "${__ebuildshell_tmpf}."{ebuild,return}-{env,rovars}
+	(
+		(
+			declare -p
+			declare -fp
+			shopt -p
+			[[ ${BASH_VERSINFO[0]} == 3 ]] && export
+		) |
+		(
+			# we need everything but the bash vars after 'env -i'
+			2>"${__ebuildshell_tmpf}.ebuild-rovars" \
+			"${PORTAGE_PYTHON:-/tools/haubi/gentoo/s01en24/usr/bin/python}" \
+				"${PORTAGE_BIN_PATH}"/filter-bash-environment.py \
+					--report-readonly-variables \
+					--preserve-readonly-attribute \
+					"${__ebuildshell_bash_i_vars}" \
+				|| die "filter-bash-environment.py failed"
+		)
+		# The already readonly variables, without bash maintained ones:
+		__ebuildshell_ro_ebuild_vars=$(<"${__ebuildshell_tmpf}.ebuild-rovars")
+		cat <<-EOE
+			# properly quote the function arguments
+			$(declare -p __ebuildshell_args)
+			set -- "\${__ebuildshell_args[@]}"
+			unset __ebuildshell_args
+			# be informative about what to do
+			PS1="EBUILD ${PN} $1 \$ "
+			type $1
+			echo "WANTED: \$@"
+			echo "or use: \"\\\$@\""
+			# use bash history, but not the user's real one
+			HISTFILE=~/.bash_history
+			# for copy&paste function body lines containing: local
+			alias local=declare
+			# for copy&paste function body lines containing: !
+			set +H
+			# at exit, dump the current environment
+			trap "
+				rm -f '${__ebuildshell_tmpf}.return-'*
+				(
+					(
+						declare -p
+						declare -fp
+						shopt -p | grep -v 'extdebug$'
+						$([[ ${BASH_VERSINFO[0]} == 3 ]] && echo export)
+					) |
+					(
+						# We may have more readonly variables now, but we
+						# need to filter variables that are readonly already.
+						2>"${__ebuildshell_tmpf}.return-rovars" \
+						'${PORTAGE_PYTHON:-/tools/haubi/gentoo/s01en24/usr/bin/python}' \
+							'${PORTAGE_BIN_PATH}'/filter-bash-environment.py \\
+								--report-readonly-variables \
+								--preserve-readonly-attribute \
+								--export-into-global-scope \
+								'${__ebuildshell_bash_i_vars} ${__ebuildshell_ro_ebuild_vars}' \\
+							|| die 'filter-bash-environment.py failed'
+					)
+				) > '${__ebuildshell_tmpf}.return-env'
+				" EXIT
+			# this is a debugging shell already
+			shopt -u extdebug
+			trap - DEBUG
+			# can do some cleanup already
+			rm -f '${__ebuildshell_tmpf}.ebuild-'*
+		EOE
+	) > "${__ebuildshell_tmpf}.ebuild-env"
+
+	# pre-fill the history with "$@"
+	echo '"$@"' >> ~/.bash_history
+
+	env -i ${BASH} --rcfile "${__ebuildshell_tmpf}.ebuild-env" -i
+
+	# The environment- and exit-status handling after leaving the ebuildshell
+	# prompt is expected to be identical as without the ebuildshell prompt.
+	local __ebuildshell_status=$?
+	source "${__ebuildshell_tmpf}.return-env"
+	# Portage does whitelist readonly variables. If an ebuild defines
+	# more readonly variables, their readonly attribute is removed.
+	# If we ever want to preserve additional readonly variables across
+	# phases, their names are in "${__ebuildshell_tmpf}.return-rovars".
+	rm -f "${__ebuildshell_tmpf}."{ebuild,return}-{env,rovars}
+	return ${__ebuildshell_status}
+}
+
 # Subshell/helper die support (must export for the die helper).
 export EBUILD_MASTER_PID=${BASHPID:-$(__bashpid)}
 trap 'exit 1' SIGTERM
diff --git a/bin/filter-bash-environment.py b/bin/filter-bash-environment.py
index a4cdc54..a710e93 100755
--- a/bin/filter-bash-environment.py
+++ b/bin/filter-bash-environment.py
@@ -14,7 +14,8 @@ func_end_re = re.compile(r'^\}$')
 
 var_assign_re = re.compile(r'(^|^declare\s+-\S+\s+|^declare\s+|^export\s+)([^=\s]+)=("|\')?.*$')
 close_quote_re = re.compile(r'(\\"|"|\')\s*$')
-readonly_re = re.compile(r'^declare\s+-(\S*)r(\S*)\s+')
+readonly_re = re.compile(r'^declare\s+-(\S*)r(\S*)\s+([^=\s]+)')
+export_re = re.compile(r'^declare\s+-(\S*x\S*)\s+')
 # declare without assignment
 var_declare_re = re.compile(r'^declare(\s+-\S+)?\s+([^=\s]+)\s*$')
 
@@ -29,7 +30,7 @@ def have_end_quote(quote, line):
 	return close_quote_match is not None and \
 		close_quote_match.group(1) == quote
 
-def filter_declare_readonly_opt(line):
+def filter_declare_readonly_opt(line, options):
 	readonly_match = readonly_re.match(line)
 	if readonly_match is not None:
 		declare_opts = ''
@@ -37,14 +38,29 @@ def filter_declare_readonly_opt(line):
 			group = readonly_match.group(i)
 			if group is not None:
 				declare_opts += group
+		var = readonly_match.group(3)
+		if '--report-readonly-variables' in options:
+			sys.stderr.write(var + "\n")
+		if '--preserve-readonly-attribute' in options:
+			declare_opts += 'r'
 		if declare_opts:
-			line = 'declare -%s %s' % \
-				(declare_opts, line[readonly_match.end():])
+			line = 'declare -%s %s%s' % \
+				(declare_opts, var, line[readonly_match.end():])
 		else:
-			line = 'declare ' + line[readonly_match.end():]
+			line = 'declare ' + var + line[readonly_match.end():]
 	return line
 
-def filter_bash_environment(pattern, file_in, file_out):
+def add_global_export_opt(line, options):
+	export_match = export_re.match(line)
+	if export_match is not None:
+		declare_opts = export_match.group(1)
+		if 'g' not in declare_opts and '--export-into-global-scope' in options:
+			declare_opts += 'g'
+		line = 'declare -%s %s' % \
+			(declare_opts, line[export_match.end():])
+	return line
+
+def filter_bash_environment(pattern, file_in, file_out, options):
 	# Filter out any instances of the \1 character from variable values
 	# since this character multiplies each time that the environment
 	# is saved (strange bash behavior). This can eventually result in
@@ -77,7 +93,8 @@ def filter_bash_environment(pattern, file_in, file_out):
 					multi_line_quote = quote
 					multi_line_quote_filter = filter_this
 				if not filter_this:
-					line = filter_declare_readonly_opt(line)
+					line = filter_declare_readonly_opt(line, options)
+					line = add_global_export_opt(line, options)
 					file_out.write(line.replace("\1", ""))
 				continue
 			else:
@@ -87,7 +104,8 @@ def filter_bash_environment(pattern, file_in, file_out):
 					filter_this = pattern.match(declare_match.group(2)) \
 						is not None
 					if not filter_this:
-						line = filter_declare_readonly_opt(line)
+						line = filter_declare_readonly_opt(line, options)
+						line = add_global_export_opt(line, options)
 						file_out.write(line)
 					continue
 
@@ -124,13 +142,28 @@ if __name__ == "__main__":
 		"while leaving bash function definitions and here-documents " + \
 		"intact. The PATTERN is a space separated list of variable names" + \
 		" and it supports python regular expression syntax."
-	usage = "usage: %s PATTERN" % os.path.basename(sys.argv[0])
-	args = sys.argv[1:]
-
-	if '-h' in args or '--help' in args:
-		sys.stdout.write(usage + "\n")
-		sys.stdout.flush()
-		sys.exit(os.EX_OK)
+	usage = "usage: %s [-h|<options>] PATTERN" % os.path.basename(sys.argv[0])
+	args = []
+	known_options = {
+		'--report-readonly-variables':
+			"Write names of readonly variables to stderr.",
+		'--preserve-readonly-attribute':
+			"Preserve the '-r' flag in 'declare -r'.",
+		'--export-into-global-scope':
+			"Add the '-g' flag to 'declare -x'.",
+	}
+	options = {}
+	for arg in sys.argv[1:]:
+		if arg in known_options.keys():
+			options[arg] = True
+			continue
+		if '-h' == arg or '--help' == arg:
+			sys.stdout.write(usage + "\n\nKnown <options>:\n\n")
+			for option, descr in known_options.items():
+				sys.stdout.write("  " + option + "\t" + descr + "\n")
+			sys.stdout.flush()
+			sys.exit(os.EX_OK)
+		args.append(arg)
 
 	if len(args) != 1:
 		sys.stderr.write(usage + "\n")
@@ -154,5 +187,5 @@ if __name__ == "__main__":
 
 	var_pattern = "^(%s)$" % "|".join(var_pattern)
 	filter_bash_environment(
-		re.compile(var_pattern), file_in, file_out)
+		re.compile(var_pattern), file_in, file_out, options)
 	file_out.flush()
diff --git a/bin/save-ebuild-env.sh b/bin/save-ebuild-env.sh
index ddef1fd..7264ced 100644
--- a/bin/save-ebuild-env.sh
+++ b/bin/save-ebuild-env.sh
@@ -53,7 +53,7 @@ __save_ebuild_env() {
 		einfo einfon ewarn eerror ebegin __eend eend KV_major \
 		KV_minor KV_micro KV_to_int get_KV has \
 		__has_phase_defined_up_to \
-		hasv hasq __qa_source __qa_call \
+		hasv hasq __qa_source __qa_call __call-ebuildshell \
 		addread addwrite adddeny addpredict __sb_append_var \
 		use usev useq has_version portageq \
 		best_version use_with use_enable register_die_hook \
diff --git a/man/make.conf.5 b/man/make.conf.5
index 26bbf06..865ede9 100644
--- a/man/make.conf.5
+++ b/man/make.conf.5
@@ -382,6 +382,12 @@ exist). Also see the related \fIunmerge\-backup\fR feature.
 Use locks to ensure that unsandboxed ebuild phases never execute
 concurrently. Also see \fIparallel\-install\fR.
 .TP
+.B ebuildshell
+Drop into an interactive shell for each phase function, meant for
+debugging.  Because the shell would normally be used to execute the
+phase function, commands like src_unpack or epatch are available in the
+interactive shell.  Use `die` to terminate the merge.
+.TP
 .B fail\-clean
 Clean up temporary files after a build failure. This is particularly useful
 if you have \fBPORTAGE_TMPDIR\fR on tmpfs. If this feature is enabled, you
diff --git a/pym/_emerge/AbstractEbuildProcess.py b/pym/_emerge/AbstractEbuildProcess.py
index 8bd30a6..4ff78b4 100644
--- a/pym/_emerge/AbstractEbuildProcess.py
+++ b/pym/_emerge/AbstractEbuildProcess.py
@@ -161,6 +161,7 @@ class AbstractEbuildProcess(SpawnProcess):
 			self.fd_pipes = {}
 		null_fd = None
 		if 0 not in self.fd_pipes and \
+			"ebuildshell" not in self.settings.features and \
 			self.phase not in self._phases_interactive_whitelist and \
 			"interactive" not in self.settings.get("PROPERTIES", "").split():
 			null_fd = os.open('/dev/null', os.O_RDONLY)
diff --git a/pym/portage/const.py b/pym/portage/const.py
index 814d7f4..d84f9bf 100644
--- a/pym/portage/const.py
+++ b/pym/portage/const.py
@@ -142,6 +142,7 @@ SUPPORTED_FEATURES       = frozenset([
 	"distlocks",
 	"downgrade-backup",
 	"ebuild-locks",
+	"ebuildshell",
 	"fail-clean",
 	"fakeroot",
 	"fixlafiles",
-- 
2.7.3