Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve gem build -C flag #3983

Merged
merged 7 commits into from
Nov 12, 2020
Merged

Improve gem build -C flag #3983

merged 7 commits into from
Nov 12, 2020

Conversation

bronzdoc
Copy link
Member

@bronzdoc bronzdoc commented Sep 30, 2020

Description:

Fixes #3953.
Closes #3954.

Consolidate the current behavior of "gem build" with gem build -C [PATH]


I will abide by the code of conduct.

@bronzdoc bronzdoc changed the title Improve --C flag Improve -C flag Sep 30, 2020
@bronzdoc bronzdoc changed the title Improve -C flag Improve gem build -C flag Sep 30, 2020
@deivid-rodriguez
Copy link
Member

Ok, thanks @bronzdoc, I'll have a look at this PR vs #3954 now 👍.

Copy link
Member

@deivid-rodriguez deivid-rodriguez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice @bronzdoc, this looks very good. I made some changes locally:

  • gem build does support building multiple gemspecs in a single invokation. However, in this patch, we're taking the first matching gemspec and ignoring the rest, so the user might be left wondering why the glob argument was happily accepted but only one gem got built. So I reused find_gemspec, which raises if multiple matching gemspecs are found.

  • I noticed that the two errors about the gemspec not being found were essentially the same thing and could be made more clear. So I merged all that logic into a single message that a gemspec could not be inferred from the argument given to gem build, or that it couldn't find any gemspecs at all if no argument was given.

This is the patch:

diff --git a/lib/rubygems/commands/build_command.rb b/lib/rubygems/commands/build_command.rb
index d8aeb49223..b70d21fe60 100644
--- a/lib/rubygems/commands/build_command.rb
+++ b/lib/rubygems/commands/build_command.rb
@@ -63,20 +63,18 @@ def usage # :nodoc:
   def execute
     if build_path = options[:build_path]
       Dir.chdir(build_path) do
-        gem_name = get_one_optional_argument || find_gemspec
-        build_gem(gem_name)
+        build_gem
       end
       return
     end
 
-    gem_name = get_one_optional_argument || find_gemspec
-    build_gem(gem_name)
+    build_gem
   end
 
   private
 
-  def find_gemspec
-    gemspecs = Dir.glob("*.gemspec").sort
+  def find_gemspec(glob = "*.gemspec")
+    gemspecs = Dir.glob(glob).sort
 
     if gemspecs.size > 1
       alert_error "Multiple gemspecs found: #{gemspecs}, please specify one"
@@ -86,22 +84,19 @@ def find_gemspec
     gemspecs.first
   end
 
-  def build_gem(gem_name)
-    gemspec = Dir.glob(gem_name).sort.first || "#{gem_name}.gemspec"
+  def build_gem
+    gemspec = resolve_gem_name
 
-    if File.exist?(gemspec)
-      spec = Gem::Specification.load(gemspec)
-      build_package(spec)
-    elsif gemspec.scan(".gemspec").size > 1
-      alert_error "No Gemspec in #{Dir.pwd}"
-      terminate_interaction(1)
+    if gemspec
+      build_package(gemspec)
     else
-      alert_error "Gemspec file not found: #{gemspec}"
+      alert_error error_message
       terminate_interaction(1)
     end
   end
 
-  def build_package(spec)
+  def build_package(gemspec)
+    spec = Gem::Specification.load(gemspec)
     if spec
       Gem::Package.build(
         spec,
@@ -114,4 +109,26 @@ def build_package(spec)
       terminate_interaction 1
     end
   end
+
+  def resolve_gem_name
+    return find_gemspec unless gem_name
+
+    if File.exist?(gem_name)
+      gem_name
+    else
+      find_gemspec("#{gem_name}.gemspec") || find_gemspec(gem_name)
+    end
+  end
+
+  def error_message
+    if gem_name
+      "Couldn't find a gemspec file matching '#{gem_name}' in #{Dir.pwd}"
+    else
+      "Couldn't find a gemspec file in #{Dir.pwd}"
+    end
+  end
+
+  def gem_name
+    get_one_optional_argument
+  end
 end
diff --git a/test/rubygems/test_gem_commands_build_command.rb b/test/rubygems/test_gem_commands_build_command.rb
index 43a312eb16..2acc41abfa 100644
--- a/test/rubygems/test_gem_commands_build_command.rb
+++ b/test/rubygems/test_gem_commands_build_command.rb
@@ -231,7 +231,7 @@ def test_execute_missing_file
     end
 
     assert_equal '', @ui.output
-    assert_equal "ERROR:  Gemspec file not found: some_gem.gemspec\n", @ui.error
+    assert_equal "ERROR:  Couldn't find a gemspec file matching 'some_gem' in #{@tempdir}\n", @ui.error
   end
 
   def test_execute_outside_dir
@@ -335,7 +335,7 @@ def test_execute_outside_dir_no_gemspec_present
     end
 
     assert_equal "", @ui.output
-    assert_equal "ERROR:  No Gemspec in #{gemspec_dir}\n", @ui.error
+    assert_equal "ERROR:  Couldn't find a gemspec file matching '*.gemspec' in #{gemspec_dir}\n", @ui.error
 
     gem_file = File.join gemspec_dir, File.basename(@gem.cache_file)
     refute File.exist?(gem_file)

However, I feel the way we are interpreting -C might not be the use case @voxik had in mind, so we might want to wait for feedback from him.

@voxik
Copy link
Contributor

voxik commented Oct 1, 2020

I think this proposal is/keeps breaking the initial premise which was there from beginning, i.e. the gem build accepts path to .gemspec file and it does not play with it. It might be relative path or absolute path. It might be even shell expanded wild card. So if the -C steps into some directory and then evaluates whatever .gemspec was specified, it diverges from the original interface.

But I agree that Run as if gem build was started in instead of the current working directory. has been always ambiguous and that was part or my complaint in #3953

@deivid-rodriguez
Copy link
Member

deivid-rodriguez commented Oct 1, 2020

@voxik I think you are correct 👍. We should fix this your way, and change the docs for -C to read something like:

-C <PATH> Look for the code of the gem to be built in <PATH>. The GEMSPEC file given as an argument is still resolved from the CWD.

Would that make sense?

@deivid-rodriguez
Copy link
Member

Actually, not really convinced after a second thought. Thinking of git -C or other -C flags. Isn't this PR is exactly how those works:thinking:. What is it that you don't like about the current -C behavior?

I think this proposal is/keeps breaking the initial premise which was there from beginning, i.e. the gem build accepts path to .gemspec file and it does not play with it.

If we introduce a new flag, shouldn't it be able to define its own behaviour? I mean, the behaviour you propose might be more useful, but I don't see how a flag shouldn't modify the way arguments are interpreted. Specially -C whose behaviour is quite standard: switch directory, and then run the original command without -C.

@voxik
Copy link
Contributor

voxik commented Oct 2, 2020

TBH, I have never really cared about having -C flag on the first place. I was always fine with doing something like:

$ gem fetch json
$ gem unpack json-2.3.1.gem
$ gem spec json-2.3.1.gem --ruby > json.gemspec
$ pushd json-2.3.1/
$ gem build ../json.gemspec
$ popd

This behavior (available since RubyGems inception) was broken by 90e6768 and I complained about it in #2587, where the idea with -C came to the life.

If the above can be shortened to:

$ gem fetch json
$ gem unpack json-2.3.1.gem
$ gem spec json-2.3.1.gem --ruby > json.gemspec
$ gem build ../json.gemspec -C json-2.3.1/

that is fine, but please don't make any assumption about where the ".gemspec" file lives. Guessing if the "json.gemspec" - when the -C is specified - is relative the the current directory (which was always the case) or to the directory where the -C points won't help. Neither any assumption like ".gemspec is in the same directory as the rest of the gem code" were never true (actually I understand Bundler does some assumptions like this, but that is different case). Also assumptions like "there is always only one .gemspec" are false.

@voxik
Copy link
Contributor

voxik commented Oct 2, 2020

Also, I should mention that this is what current help says:

  Arguments:
    GEMSPEC_FILE  gemspec file name to build a gem for

It does not make any assumptions about where the GEMSPEC_FILE is. It can be basically anywhere. That is how I always used it.

But again, this was broken by #2859 / #2887 and neither #1454 helped here, where the assumption about "file" was relaxed for the first time, but nobody updated the specification.

@deivid-rodriguez
Copy link
Member

Yes, I'm aware that we introduced the -C because we break something you were using. So it should definitely work for you.

And you're also right the the GEMSPEC_FILE argument becomes misleading with our interpretation of -C.

Would #3983 (comment) work for you? That's the approach your PR takes, right?

@voxik
Copy link
Contributor

voxik commented Oct 2, 2020

Would #3983 (comment) work for you? That's the approach your PR takes, right?

Yes to both 👍

@voxik
Copy link
Contributor

voxik commented Oct 2, 2020

Or possibly

The GEMSPEC_FILE given as an argument is not influenced by this parameter

@voxik
Copy link
Contributor

voxik commented Oct 2, 2020

s/parameter/option/ for consistency

@deivid-rodriguez
Copy link
Member

deivid-rodriguez commented Oct 2, 2020

Ok. @bronzdoc I think we should take @voxik's suggested path, since it's the traditional expectation for gem build, also documented in the case of the GEMSPEC_FILE option, and it's the most useful behaviour for packagers who would be the main target audience of the -C flag.

It deviates a bit from standard -C flags, but as long as we document it properly, I don't think it's a problem.

@bronzdoc
Copy link
Member Author

bronzdoc commented Oct 2, 2020

I need to catchup with all comments. I will give them a look later tonight

@deivid-rodriguez
Copy link
Member

Something else I was thinking is that if we had gone with #2587 (comment), this shouldn't be an issue for you and you wouldn't need -C? So maybe we should actually restore a working gem build ../json.gemspec, and also use this PR to get a standard behavior for -C?

@bronzdoc
Copy link
Member Author

bronzdoc commented Oct 5, 2020

I like this idea the most

Something else I was thinking is that if we had gone with #2587 (comment), this shouldn't be an issue for you and you wouldn't need -C? So maybe we should actually restore a working gem build ../json.gemspec, and also use this PR to get a standard behavior for -C?

@voxik
Copy link
Contributor

voxik commented Oct 5, 2020

I like this idea the most

Something else I was thinking is that if we had gone with #2587 (comment), this shouldn't be an issue for you and you wouldn't need -C? So maybe we should actually restore a working gem build ../json.gemspec, and also use this PR to get a standard behavior for -C?

I'm not sure I understand your idea. Does this mean that you propose to try to find the gemspec relative to the -C specified directory and fallback to the CWD? Of course absolute path is absolute, that should be easy.

If that is the proposal, I just worry, that the implementation will get even more complex and error prone with no benefit.

@deivid-rodriguez
Copy link
Member

deivid-rodriguez commented Oct 5, 2020

No, I'm proposing that we move forward with the approach in this PR.

The thing is, I misunderstood the history of these issues. In #3983 (comment) you said the following (talking about gem build ../json.gemspec):

This behaviour (available since RubyGems inception) was broken by 90e6768 and I complained about it in #2587, where the idea with -C came to the life.

This is correct, but as far as I understand, we considered this as an accidental regression and restored the original behaviour, so gem build ../json.gemspec should be working fine at the moment.

The -C idea was born in #2587 (comment) as a different way of solving #2204 without breaking your use case. The specification for the flag in #2587 (comment) is perfectly clear about where every given file is resolved from if -C is given, and you seemed happy with the idea.

In addition to that, you just said in #3983 (comment) that shortening things to gem build ../json.gemspec -C json-2.3.1/ would be nice, and that's exactly how the flag is supposed to work, and how @bronzdoc is fixing it to work on this PR.

So, it seems to me that this PR is the way to go, with the only extra addition of improving the GEMSPEC_FILE documentation to mention how the folder given as -C is used as the base folder to resolve the GEMSPEC_FILE argument as well.

@voxik
Copy link
Contributor

voxik commented Oct 8, 2020

The problem is that you tried to take everything I said while trying to provide you examples close to the real life scenarios, and you applies it to the provided patch. So let me share two real life scenarios.

How the #2587 started

Here is snippet from Fedora .spec

... snip ...

%prep
%setup -q -n %{gem_name}-%{version} -b 1

%build
# Create the gem as gem install only works on a gem file
gem build ../%{gem_name}-%{version}.gemspec

... snip ...

And now how does it look under the hood:

... snip ...

Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.AiXddi
+ umask 022
+ cd /builddir/build/BUILD
+ cd /builddir/build/BUILD
+ rm -rf abrt-0.4.0
+ /usr/bin/gzip -dc /builddir/build/SOURCES/abrt-0.4.0-spec.tar.gz
+ /usr/bin/tar -xof -
+ STATUS=0
+ '[' 0 -ne 0 ']'
+ /usr/bin/gem unpack /builddir/build/SOURCES/abrt-0.4.0.gem
Unpacked gem: '/builddir/build/BUILD/abrt-0.4.0'
+ /usr/bin/gem spec /builddir/build/SOURCES/abrt-0.4.0.gem --ruby
+ STATUS=0
+ '[' 0 -ne 0 ']'
+ cd abrt-0.4.0
+ /usr/bin/chmod -Rf a+rX,u+w,g-w,o-w .
+ RPM_EC=0
++ jobs -p
+ exit 0
Executing(%build): /bin/sh -e /var/tmp/rpm-tmp.gDUC9j
+ umask 022
+ cd /builddir/build/BUILD
+ cd abrt-0.4.0
+ gem build ../abrt-0.4.0.gemspec
  Successfully built RubyGem
  Name: abrt
  Version: 0.4.0
  File: abrt-0.4.0.gem

... snip ...

This is the patch adding RPM support for .gem packages, where you can see the way abrt-0.4.0.gemspec is stored on the disk.

This use case is not related to the -C option what so ever, although it could be possibly used somewhere. But it requires relative/absolute path to gemspec, which is stored on some arbitrary location.

Now the story of #3953

I have received voxik/abrt-ruby#10. The idea behind this PR is to test upstream package in the context of Fedora. Specifically, this is line of the interest:

  -  bash -c "gem build *.gemspec; mv -f *.gem ci/; ls ci/*.gem"

This unexpectedly moved all the *.gem files into the ci/ directory. So I remembered the -C option and I was thinking if could do it differently to preserve the directory original conent, e.g.:

$ cd ci/
$ gem build *.gemspec -C ..

and the rest is in #3953.

Actually, re-reading my initial comment, the surprise was not just about where the *.gemspec is located and expected, but also where the resulting *.gem package is created. And frankly, the whole idea of using -C was that I would not need to mess with the content of the source directory.

@voxik
Copy link
Contributor

voxik commented Oct 8, 2020

The rubygem-abrt was probably too simplistic, because somebody might wonder, why we repack the gems. So please take a look on rubygem-idn instead:

https://src.fedoraproject.org/rpms/rubygem-idn/blob/master/f/rubygem-idn.spec
https://kojipkgs.fedoraproject.org//packages/rubygem-idn/0.0.2/31.fc33/data/logs/x86_64/build.log

I.e. we have to fix the C extension to be able to gem install the package.

@voxik
Copy link
Contributor

voxik commented Oct 8, 2020

Also, I think that there should also be taken in account what is the influence of shell expansion. Please note that there was used gem build *.gemspec. That for shell means submit all .gemspec files from current repository, but not from the directory where -C moves.

@deivid-rodriguez
Copy link
Member

Ok, I understand this better now. Everything regarding passing relative paths in a different folder to gem build was working fine for you, but now you tried to use -C for a different use case, and it didn't work as you expected.

I'm happy to review your new use case and give it some thought and see what we can add/change to make things work better for you. But I don't think the solution is changing the specification of -C <target_dir>, since it works in the standard way of all -C options (cd <target_dir> && run original command without -C).

Rmember that I was very clear about this when I proposed the specification for the option:

Add -C argument to gem build that behaves similarly to git -C. If given, files are picked up from the given directory, and also the gemspec is resolved from that directory if given relatively.

And you intuitively liked the idea:

And similarly to make -C. That would be nice.

The fact that you found a new use case where you thought -C could be applied, but that turns out it can't, doesn't justify that we change its intended meaning. I'd rather add a different feature if necessary.

Also, I think that there should also be taken in account what is the influence of shell expansion. Please note that there was used gem build *.gemspec. That for shell means submit all .gemspec files from current repository.

Shell expansion (if your shell is configured to do it) happens before us, so I'm not sure what we could do about it. git -C works in the same way:

$ pwd
/home/deivid/Code/ruby/ruby

$ git ls-files vm_*.c
vm_args.c
vm_backtrace.c
vm_dump.c
vm_eval.c
vm_exec.c
vm_insnhelper.c
vm_method.c
vm_sync.c
vm_trace.c

$ git -C ../../rubygems/rubygems rm vm_*.c
fatal: pathspec 'vm_args.c' did not match any files

@voxik
Copy link
Contributor

voxik commented Oct 9, 2020

First of all, I am glad for this discussion, because if nothing else, I hope that the behavior will be better specified.

And you intuitively liked the idea:

And similarly to make -C. That would be nice.

And possibly not considering all implications and consequences 😉

I get your argument with shell expansion.

Therefore, while it is a bit out of scope and should have been covered probably elsewhere but, could this PR be extended by two (actually four) test cases? Given that the CWD is the gem directory:

  1. The gemspec is relative path, for the scenario with -C and without -C.
  2. The gemspec is absolute path, for the scenario with -C and without -C.

@deivid-rodriguez
Copy link
Member

Sure that seems like a good idea.

@bronzdoc Could you add tests for those cases (or unify them if some of the cases is already tested), when you are able to loop back to this PR?

@voxik
Copy link
Contributor

voxik commented Oct 9, 2020

Looking at the test suite, the irony is that almost all test are executed with absolute path to the .gemspec file. I don't have the feeling this represents the real life usage.

This could be the two test cases BTW:

diff --git a/test/rubygems/test_gem_commands_build_command.rb b/test/rubygems/test_gem_commands_build_command.rb
index 167752eec..3669a3edd 100644
--- a/test/rubygems/test_gem_commands_build_command.rb
+++ b/test/rubygems/test_gem_commands_build_command.rb
@@ -272,6 +272,88 @@ def test_execute_outside_dir
     assert_equal "this is a summary", spec.summary
   end
 
+  def test_execute_outside_dir_with_external_gemspec
+    gemspec_dir = File.join @tempdir, 'gemspec_dir'
+    gemspec_file = File.join gemspec_dir, @gem.spec_name
+
+    gemcode_dir = File.join @tempdir, 'build_command_gem'
+    readme_file = File.join gemcode_dir, 'README.md'
+
+    FileUtils.mkdir_p gemspec_dir
+    FileUtils.mkdir_p gemcode_dir
+
+    File.open readme_file, 'w' do |f|
+      f.write "My awesome gem in nested directory"
+    end
+
+    File.open gemspec_file, 'w' do |gs|
+      gs.write @gem.to_ruby
+    end
+
+    @cmd.options[:build_path] = gemcode_dir
+    @cmd.options[:args] = [gemspec_file]
+
+    use_ui @ui do
+      @cmd.execute
+    end
+
+    output = @ui.output.split "\n"
+    assert_equal "  Successfully built RubyGem", output.shift
+    assert_equal "  Name: some_gem", output.shift
+    assert_equal "  Version: 2", output.shift
+    assert_equal "  File: some_gem-2.gem", output.shift
+    assert_equal [], output
+
+    gem_file = File.join gemcode_dir, File.basename(@gem.cache_file)
+    assert File.exist?(gem_file)
+
+    spec = Gem::Package.new(gem_file).spec
+
+    assert_equal "some_gem", spec.name
+    assert_equal "this is a summary", spec.summary
+  end
+
+  def test_execute_outside_dir_with_external_relative_gemspec
+    gemspec_dir = File.join @tempdir, 'gemspec_dir'
+    gemspec_file = File.join gemspec_dir, @gem.spec_name
+
+    gemcode_dir = File.join @tempdir, 'build_command_gem'
+    readme_file = File.join gemcode_dir, 'README.md'
+
+    FileUtils.mkdir_p gemspec_dir
+    FileUtils.mkdir_p gemcode_dir
+
+    File.open readme_file, 'w' do |f|
+      f.write "My awesome gem in nested directory"
+    end
+
+    File.open gemspec_file, 'w' do |gs|
+      gs.write @gem.to_ruby
+    end
+
+    @cmd.options[:build_path] = gemcode_dir
+    @cmd.options[:args] = [File.join("..", "gemspec_dir", @gem.spec_name)]
+
+    use_ui @ui do
+      @cmd.execute
+    end
+
+    output = @ui.output.split "\n"
+    assert_equal "  Successfully built RubyGem", output.shift
+    assert_equal "  Name: some_gem", output.shift
+    assert_equal "  Version: 2", output.shift
+    assert_equal "  File: some_gem-2.gem", output.shift
+    assert_equal [], output
+
+    gem_file = File.join gemcode_dir, File.basename(@gem.cache_file)
+    assert File.exist?(gem_file)
+
+    spec = Gem::Package.new(gem_file).spec
+
+    assert_equal "some_gem", spec.name
+    assert_equal "this is a summary", spec.summary
+  end
+
   def test_execute_outside_dir_with_glob_argument
     gemspec_dir = File.join @tempdir, 'build_command_gem'
     gemspec_file = File.join gemspec_dir, @gem.spec_name

The first is taken from my #3954. The @cmd.options[:args] = [File.join("..", "gemspec_dir", @gem.spec_name)] could be probably constructed in some better way.

@deivid-rodriguez
Copy link
Member

Great! I think it makes sense to include your test in this PR and close it in favour of this one?

Copy link
Member

@deivid-rodriguez deivid-rodriguez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me!

@voxik What do you think?

By the way, I think we should move the -C flag to a global option since I believe it would be useful outside of a gem build context. See #448 (comment). If we agree on it, I can open a separate ticket to remember about it.

@deivid-rodriguez
Copy link
Member

I'll go ahead and merge this. We can always iterate for further improvements.

@deivid-rodriguez deivid-rodriguez merged commit 2ad192e into master Nov 12, 2020
@deivid-rodriguez deivid-rodriguez deleted the improve-c-flag branch November 12, 2020 09:06
deivid-rodriguez added a commit that referenced this pull request Dec 7, 2020
Improve gem build -C flag

(cherry picked from commit 2ad192e)
deivid-rodriguez added a commit that referenced this pull request Dec 7, 2020
Improve gem build -C flag

(cherry picked from commit 2ad192e)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

The gem build -C does not work as expected
4 participants